From 11565a40b315212474f52eb576a9fd92b11f1132 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 20 Dec 2021 14:18:29 +0100 Subject: [PATCH] Set Rules Episode IX: The Rise of Testing --- benches/oneshot.rs | 17 +-- src/eval/mod.rs | 82 ++++++------ src/eval/node.rs | 31 ++--- src/eval/styles.rs | 90 ++++++------- src/layout/mod.rs | 118 ++++++++++-------- src/lib.rs | 54 ++++---- src/library/document.rs | 20 --- src/library/grid.rs | 2 +- src/library/heading.rs | 63 ++++++++++ src/library/list.rs | 102 +++++++++++++++ src/library/mod.rs | 39 ++++-- src/library/page.rs | 15 +-- src/library/par.rs | 11 +- src/library/text.rs | 116 +++++++++++------ src/source.rs | 4 +- src/syntax/ast.rs | 3 + src/syntax/mod.rs | 2 +- .../layout/{containers.png => box-block.png} | Bin tests/ref/{elements => layout}/image.png | Bin .../{background.png => place-background.png} | Bin tests/ref/layout/{placed.png => place.png} | Bin .../layout/{aspect.png => shape-aspect.png} | Bin .../circle.png => layout/shape-circle.png} | Bin .../ellipse.png => layout/shape-ellipse.png} | Bin .../shape-fill-stroke.png} | Bin .../rect.png => layout/shape-rect.png} | Bin .../square.png => layout/shape-square.png} | Bin tests/ref/layout/spacing.png | Bin 2412 -> 2927 bytes tests/ref/markup/heading.png | Bin 6474 -> 9141 bytes tests/ref/style/set-block.png | Bin 0 -> 812 bytes tests/ref/style/set-site.png | Bin 0 -> 4252 bytes tests/ref/style/set-toggle.png | Bin 0 -> 968 bytes tests/ref/text/em.png | Bin 0 -> 878 bytes tests/ref/text/par.png | Bin 7388 -> 8169 bytes tests/ref/text/whitespace.png | Bin 5322 -> 4674 bytes .../layout/{containers.typ => box-block.typ} | 0 tests/typ/{elements => layout}/image.typ | 0 .../{background.typ => place-background.typ} | 0 tests/typ/layout/{placed.typ => place.typ} | 0 .../layout/{aspect.typ => shape-aspect.typ} | 0 .../circle.typ => layout/shape-circle.typ} | 0 .../ellipse.typ => layout/shape-ellipse.typ} | 0 .../shape-fill-stroke.typ} | 0 .../rect.typ => layout/shape-rect.typ} | 0 .../square.typ => layout/shape-square.typ} | 0 tests/typ/layout/spacing.typ | 12 +- tests/typ/markup/heading.typ | 9 ++ tests/typ/style/set-block.typ | 10 ++ tests/typ/style/set-site.typ | 30 +++++ tests/typ/style/set-toggle.typ | 10 ++ tests/typ/text/em.typ | 17 +++ tests/typ/text/par.typ | 10 ++ tests/typ/text/whitespace.typ | 41 +++--- tests/typeset.rs | 19 +-- 54 files changed, 611 insertions(+), 316 deletions(-) delete mode 100644 src/library/document.rs create mode 100644 src/library/heading.rs create mode 100644 src/library/list.rs rename tests/ref/layout/{containers.png => box-block.png} (100%) rename tests/ref/{elements => layout}/image.png (100%) rename tests/ref/layout/{background.png => place-background.png} (100%) rename tests/ref/layout/{placed.png => place.png} (100%) rename tests/ref/layout/{aspect.png => shape-aspect.png} (100%) rename tests/ref/{elements/circle.png => layout/shape-circle.png} (100%) rename tests/ref/{elements/ellipse.png => layout/shape-ellipse.png} (100%) rename tests/ref/{elements/fill-stroke.png => layout/shape-fill-stroke.png} (100%) rename tests/ref/{elements/rect.png => layout/shape-rect.png} (100%) rename tests/ref/{elements/square.png => layout/shape-square.png} (100%) create mode 100644 tests/ref/style/set-block.png create mode 100644 tests/ref/style/set-site.png create mode 100644 tests/ref/style/set-toggle.png create mode 100644 tests/ref/text/em.png rename tests/typ/layout/{containers.typ => box-block.typ} (100%) rename tests/typ/{elements => layout}/image.typ (100%) rename tests/typ/layout/{background.typ => place-background.typ} (100%) rename tests/typ/layout/{placed.typ => place.typ} (100%) rename tests/typ/layout/{aspect.typ => shape-aspect.typ} (100%) rename tests/typ/{elements/circle.typ => layout/shape-circle.typ} (100%) rename tests/typ/{elements/ellipse.typ => layout/shape-ellipse.typ} (100%) rename tests/typ/{elements/fill-stroke.typ => layout/shape-fill-stroke.typ} (100%) rename tests/typ/{elements/rect.typ => layout/shape-rect.typ} (100%) rename tests/typ/{elements/square.typ => layout/shape-square.typ} (100%) create mode 100644 tests/typ/style/set-block.typ create mode 100644 tests/typ/style/set-site.typ create mode 100644 tests/typ/style/set-toggle.typ create mode 100644 tests/typ/text/em.typ diff --git a/benches/oneshot.rs b/benches/oneshot.rs index 8f245b779..d3e2ff8e4 100644 --- a/benches/oneshot.rs +++ b/benches/oneshot.rs @@ -2,8 +2,6 @@ use std::path::Path; use iai::{black_box, main, Iai}; -use typst::eval::eval; -use typst::layout::layout; use typst::loading::MemLoader; use typst::parse::{parse, Scanner, TokenMode, Tokens}; use typst::source::SourceId; @@ -53,20 +51,14 @@ fn bench_parse(iai: &mut Iai) { fn bench_eval(iai: &mut Iai) { let (mut ctx, id) = context(); - let ast = ctx.sources.get(id).ast().unwrap(); - iai.run(|| eval(&mut ctx, id, &ast).unwrap()); -} - -fn bench_to_tree(iai: &mut Iai) { - let (mut ctx, id) = context(); - let module = ctx.evaluate(id).unwrap(); - iai.run(|| module.node.clone().into_document()); + iai.run(|| ctx.evaluate(id).unwrap()); } fn bench_layout(iai: &mut Iai) { let (mut ctx, id) = context(); - let tree = ctx.execute(id).unwrap(); - iai.run(|| layout(&mut ctx, &tree)); + let module = ctx.evaluate(id).unwrap(); + let tree = module.into_root(); + iai.run(|| tree.layout(&mut ctx)); } main!( @@ -75,6 +67,5 @@ main!( bench_tokenize, bench_parse, bench_eval, - bench_to_tree, bench_layout ); diff --git a/src/eval/mod.rs b/src/eval/mod.rs index ae3301349..d05f2ddf7 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -34,9 +34,10 @@ use std::path::PathBuf; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult}; -use crate::geom::{Angle, Fractional, Length, Relative, Spec}; +use crate::geom::{Angle, Fractional, Length, Relative}; use crate::image::ImageStore; -use crate::library::{GridNode, TextNode, TrackSizing}; +use crate::layout::RootNode; +use crate::library::{self, TextNode}; use crate::loading::Loader; use crate::source::{SourceId, SourceStore}; use crate::syntax::ast::*; @@ -44,15 +45,9 @@ use crate::syntax::{Span, Spanned}; use crate::util::{EcoString, RefMutExt}; use crate::Context; -/// Evaluate a parsed source file into a module. -pub fn eval(ctx: &mut Context, source: SourceId, markup: &Markup) -> TypResult { - let mut ctx = EvalContext::new(ctx, source); - let node = markup.eval(&mut ctx)?; - Ok(Module { scope: ctx.scopes.top, node }) -} - -/// An evaluated module, ready for importing or instantiation. -#[derive(Debug, Clone)] +/// An evaluated module, ready for importing or conversion to a root layout +/// tree. +#[derive(Debug, Default, Clone)] pub struct Module { /// The top-level definitions that were bound in this module. pub scope: Scope, @@ -60,6 +55,22 @@ pub struct Module { pub node: Node, } +impl Module { + /// Convert this module's node into a layout tree. + pub fn into_root(self) -> RootNode { + self.node.into_root() + } +} + +/// Evaluate an expression. +pub trait Eval { + /// The output of evaluating the expression. + type Output; + + /// Evaluate the expression to the output value. + fn eval(&self, ctx: &mut EvalContext) -> TypResult; +} + /// The context for evaluation. pub struct EvalContext<'a> { /// The loader from which resources (files and images) are loaded. @@ -124,7 +135,7 @@ impl<'a> EvalContext<'a> { self.route.push(id); // Evaluate the module. - let template = ast.eval(self).trace(|| Tracepoint::Import, span)?; + let node = ast.eval(self).trace(|| Tracepoint::Import, span)?; // Restore the old context. let new_scopes = mem::replace(&mut self.scopes, prev_scopes); @@ -132,7 +143,7 @@ impl<'a> EvalContext<'a> { self.route.pop().unwrap(); // Save the evaluated module. - let module = Module { scope: new_scopes.top, node: template }; + let module = Module { scope: new_scopes.top, node }; self.modules.insert(id, module); Ok(id) @@ -151,15 +162,6 @@ impl<'a> EvalContext<'a> { } } -/// Evaluate an expression. -pub trait Eval { - /// The output of evaluating the expression. - type Output; - - /// Evaluate the expression to the output value. - fn eval(&self, ctx: &mut EvalContext) -> TypResult; -} - impl Eval for Markup { type Output = Node; @@ -231,13 +233,10 @@ impl Eval for HeadingNode { type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let upscale = (1.6 - 0.1 * self.level() as f64).max(0.75); - let mut styles = Styles::new(); - styles.set(TextNode::STRONG, true); - styles.set(TextNode::SIZE, Relative::new(upscale).into()); - Ok(Node::Block( - self.body().eval(ctx)?.into_block().styled(styles), - )) + Ok(Node::block(library::HeadingNode { + child: self.body().eval(ctx)?.into_block(), + level: self.level(), + })) } } @@ -245,8 +244,10 @@ impl Eval for ListNode { type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let body = self.body().eval(ctx)?; - labelled(ctx, '•'.into(), body) + Ok(Node::block(library::ListNode { + child: self.body().eval(ctx)?.into_block(), + labelling: library::Unordered, + })) } } @@ -254,24 +255,13 @@ impl Eval for EnumNode { type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let body = self.body().eval(ctx)?; - let label = format_eco!("{}.", self.number().unwrap_or(1)); - labelled(ctx, label, body) + Ok(Node::block(library::ListNode { + child: self.body().eval(ctx)?.into_block(), + labelling: library::Ordered(self.number()), + })) } } -/// Evaluate a labelled list / enum. -fn labelled(_: &mut EvalContext, label: EcoString, body: Node) -> TypResult { - // Create a grid containing the label, a bit of gutter space and then - // the item's body. - // TODO: Switch to em units for gutter once available. - Ok(Node::block(GridNode { - tracks: Spec::new(vec![TrackSizing::Auto; 2], vec![]), - gutter: Spec::new(vec![TrackSizing::Linear(Length::pt(5.0).into())], vec![]), - children: vec![Node::Text(label).into_block(), body.into_block()], - })) -} - impl Eval for Expr { type Output = Value; diff --git a/src/eval/node.rs b/src/eval/node.rs index acdf4ed69..e2b02955f 100644 --- a/src/eval/node.rs +++ b/src/eval/node.rs @@ -8,17 +8,18 @@ use std::ops::{Add, AddAssign}; use super::Styles; use crate::diag::StrResult; use crate::geom::SpecAxis; -use crate::layout::{Layout, PackedNode}; +use crate::layout::{Layout, PackedNode, RootNode}; use crate::library::{ - DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, - SpacingKind, SpacingNode, TextNode, + FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, SpacingKind, + SpacingNode, TextNode, }; use crate::util::EcoString; /// A partial representation of a layout node. /// /// A node is a composable intermediate representation that can be converted -/// into a proper layout node by lifting it to a block-level or document node. +/// into a proper layout node by lifting it to a [block-level](PackedNode) or +/// [root node](RootNode). #[derive(Debug, PartialEq, Clone, Hash)] pub enum Node { /// A word space. @@ -90,19 +91,19 @@ impl Node { } } - /// Lift to a document node, the root of the layout tree. - pub fn into_document(self) -> DocumentNode { + /// Lift to a root layout tree node. + pub fn into_root(self) -> RootNode { let mut packer = Packer::new(true); packer.walk(self, Styles::new()); - packer.into_document() + packer.into_root() } - /// Repeat this template `n` times. + /// Repeat this node `n` times. pub fn repeat(&self, n: i64) -> StrResult { let count = usize::try_from(n) .map_err(|_| format!("cannot repeat this template {} times", n))?; - // TODO(set): Make more efficient. + // TODO(style): Make more efficient. Ok(Self::Sequence(vec![(self.clone(), Styles::new()); count])) } } @@ -117,7 +118,7 @@ impl Add for Node { type Output = Self; fn add(self, rhs: Self) -> Self::Output { - // TODO(set): Make more efficient. + // TODO(style): Make more efficient. Self::Sequence(vec![(self, Styles::new()), (rhs, Styles::new())]) } } @@ -134,9 +135,9 @@ impl Sum for Node { } } -/// Packs a [`Node`] into a flow or whole document. +/// Packs a [`Node`] into a flow or root node. struct Packer { - /// Whether this packer produces the top-level document. + /// Whether this packer produces a root node. top: bool, /// The accumulated page nodes. pages: Vec, @@ -163,10 +164,10 @@ impl Packer { FlowNode(self.flow.children).pack() } - /// Finish up and return the resulting document. - fn into_document(mut self) -> DocumentNode { + /// Finish up and return the resulting root node. + fn into_root(mut self) -> RootNode { self.pagebreak(); - DocumentNode(self.pages) + RootNode(self.pages) } /// Consider a node with the given styles. diff --git a/src/eval/styles.rs b/src/eval/styles.rs index 2396646fa..5304e0ada 100644 --- a/src/eval/styles.rs +++ b/src/eval/styles.rs @@ -20,6 +20,11 @@ impl Styles { Self { map: vec![] } } + /// Whether this map contains no styles. + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + /// Create a style map with a single property-value pair. pub fn one(key: P, value: P::Value) -> Self { let mut styles = Self::new(); @@ -27,11 +32,6 @@ impl Styles { styles } - /// Whether this map contains no styles. - pub fn is_empty(&self) -> bool { - self.map.is_empty() - } - /// Set the value for a style property. pub fn set(&mut self, key: P, value: P::Value) { let id = StyleId::of::

(); @@ -47,6 +47,13 @@ impl Styles { self.map.push((id, Entry::new(key, value))); } + /// Set a value for a style property if it is `Some(_)`. + pub fn set_opt(&mut self, key: P, value: Option) { + if let Some(value) = value { + self.set(key, value); + } + } + /// Toggle a boolean style property. pub fn toggle>(&mut self, key: P) { let id = StyleId::of::

(); @@ -82,13 +89,22 @@ impl Styles { } /// Get a reference to a style directly in this map (no default value). - pub fn get_direct(&self, _: P) -> Option<&P::Value> { + fn get_direct(&self, _: P) -> Option<&P::Value> { self.map .iter() .find(|pair| pair.0 == StyleId::of::

()) .and_then(|pair| pair.1.downcast()) } + /// Create new styles combining `self` with `outer`. + /// + /// Properties from `self` take precedence over the ones from `outer`. + pub fn chain(&self, outer: &Self) -> Self { + let mut styles = self.clone(); + styles.apply(outer); + styles + } + /// Apply styles from `outer` in-place. /// /// Properties from `self` take precedence over the ones from `outer`. @@ -105,13 +121,9 @@ impl Styles { } } - /// Create new styles combining `self` with `outer`. - /// - /// Properties from `self` take precedence over the ones from `outer`. - pub fn chain(&self, outer: &Self) -> Self { - let mut styles = self.clone(); - styles.apply(outer); - styles + /// Keep only those styles that are not also in `other`. + pub fn erase(&mut self, other: &Self) { + self.map.retain(|a| other.map.iter().all(|b| a != b)); } /// Keep only those styles that are also in `other`. @@ -119,18 +131,13 @@ impl Styles { self.map.retain(|a| other.map.iter().any(|b| a == b)); } - /// Keep only those styles that are not also in `other`. - pub fn erase(&mut self, other: &Self) { - self.map.retain(|a| other.map.iter().all(|b| a != b)); - } - /// Whether two style maps are equal when filtered down to the given /// properties. pub fn compatible(&self, other: &Self, filter: F) -> bool where F: Fn(StyleId) -> bool, { - // TODO(set): Filtered length + one direction equal should suffice. + // TODO(style): Filtered length + one direction equal should suffice. let f = |e: &&(StyleId, Entry)| filter(e.0); self.map.iter().filter(f).all(|pair| other.map.contains(pair)) && other.map.iter().filter(f).all(|pair| self.map.contains(pair)) @@ -177,7 +184,7 @@ impl Entry { impl Debug for Entry { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) + self.0.dyn_fmt(f) } } @@ -195,22 +202,23 @@ impl Hash for Entry { trait Bounds: 'static { fn as_any(&self) -> &dyn Any; - fn fmt(&self, f: &mut Formatter) -> fmt::Result; + fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result; fn dyn_eq(&self, other: &Entry) -> bool; fn hash64(&self) -> u64; fn combine(&self, outer: &Entry) -> Entry; } -impl

Bounds for (P, P::Value) -where - P: Property, - P::Value: Debug + Hash + PartialEq + 'static, -{ +// `P` is always zero-sized. We only implement the trait for a pair of key and +// associated value so that `P` is a constrained type parameter that we can use +// in `dyn_fmt` to access the property's name. This way, we can effectively +// store the property's name in its vtable instead of having an actual runtime +// string somewhere in `Entry`. +impl Bounds for (P, P::Value) { fn as_any(&self) -> &dyn Any { &self.1 } - fn fmt(&self, f: &mut Formatter) -> fmt::Result { + fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result { if f.alternate() { write!(f, "#[{} = {:?}]", P::NAME, self.1) } else { @@ -242,11 +250,12 @@ where /// Style property keys. /// /// This trait is not intended to be implemented manually, but rather through -/// the `properties!` macro. +/// the `#[properties]` proc-macro. pub trait Property: Copy + 'static { - /// The type of this property, for example, this could be - /// [`Length`](crate::geom::Length) for a `WIDTH` property. - type Value: Debug + Clone + Hash + PartialEq + 'static; + /// The type of value that is returned when getting this property from a + /// style map. For example, this could be [`Length`](crate::geom::Length) + /// for a `WIDTH` property. + type Value: Debug + Clone + PartialEq + Hash + 'static; /// The name of the property, used for debug printing. const NAME: &'static str; @@ -257,12 +266,16 @@ pub trait Property: Copy + 'static { /// A static reference to the default value of the property. /// /// This is automatically implemented through lazy-initialization in the - /// `properties!` macro. This way, expensive defaults don't need to be + /// `#[properties]` macro. This way, expensive defaults don't need to be /// recreated all the time. fn default_ref() -> &'static Self::Value; - /// Combine the property with an outer value. - fn combine(inner: Self::Value, _: Self::Value) -> Self::Value { + /// Fold the property with an outer value. + /// + /// For example, this would combine a relative font size with an outer + /// absolute font size. + #[allow(unused_variables)] + fn combine(inner: Self::Value, outer: Self::Value) -> Self::Value { inner } } @@ -277,12 +290,3 @@ impl StyleId { Self(TypeId::of::

()) } } - -/// Set a style property to a value if the value is `Some`. -macro_rules! set { - ($styles:expr, $target:expr => $value:expr) => { - if let Some(v) = $value { - $styles.set($target, v); - } - }; -} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index cf714f888..bc28e8938 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -20,43 +20,25 @@ use crate::font::FontStore; use crate::frame::Frame; use crate::geom::{Align, Linear, Point, Sides, Size, Spec, Transform}; use crate::image::ImageStore; -use crate::library::{AlignNode, DocumentNode, PadNode, SizedNode, TransformNode}; +use crate::library::{AlignNode, PadNode, PageNode, SizedNode, TransformNode}; use crate::Context; -/// Layout a document node into a collection of frames. -pub fn layout(ctx: &mut Context, node: &DocumentNode) -> Vec> { - let mut ctx = LayoutContext::new(ctx); - node.layout(&mut ctx) +/// The root layout node, a document consisting of top-level page runs. +#[derive(Hash)] +pub struct RootNode(pub Vec); + +impl RootNode { + /// Layout the document into a sequence of frames, one per page. + pub fn layout(&self, ctx: &mut Context) -> Vec> { + let mut ctx = LayoutContext::new(ctx); + self.0.iter().flat_map(|node| node.layout(&mut ctx)).collect() + } } -/// The context for layouting. -pub struct LayoutContext<'a> { - /// Stores parsed font faces. - pub fonts: &'a mut FontStore, - /// Stores decoded images. - pub images: &'a mut ImageStore, - /// Caches layouting artifacts. - #[cfg(feature = "layout-cache")] - pub layouts: &'a mut LayoutCache, - /// The inherited style properties. - pub styles: Styles, - /// How deeply nested the current layout tree position is. - #[cfg(feature = "layout-cache")] - pub level: usize, -} - -impl<'a> LayoutContext<'a> { - /// Create a new layout context. - pub fn new(ctx: &'a mut Context) -> Self { - Self { - fonts: &mut ctx.fonts, - images: &mut ctx.images, - #[cfg(feature = "layout-cache")] - layouts: &mut ctx.layouts, - styles: ctx.styles.clone(), - #[cfg(feature = "layout-cache")] - level: 0, - } +impl Debug for RootNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Root ")?; + f.debug_list().entries(&self.0).finish() } } @@ -86,6 +68,57 @@ pub trait Layout { } } +/// The context for layouting. +pub struct LayoutContext<'a> { + /// Stores parsed font faces. + pub fonts: &'a mut FontStore, + /// Stores decoded images. + pub images: &'a mut ImageStore, + /// Caches layouting artifacts. + #[cfg(feature = "layout-cache")] + pub layouts: &'a mut LayoutCache, + /// The inherited style properties. + // TODO(style): This probably shouldn't be here. + pub styles: Styles, + /// How deeply nested the current layout tree position is. + #[cfg(feature = "layout-cache")] + pub level: usize, +} + +impl<'a> LayoutContext<'a> { + /// Create a new layout context. + pub fn new(ctx: &'a mut Context) -> Self { + Self { + fonts: &mut ctx.fonts, + images: &mut ctx.images, + #[cfg(feature = "layout-cache")] + layouts: &mut ctx.layouts, + styles: ctx.styles.clone(), + #[cfg(feature = "layout-cache")] + level: 0, + } + } +} + +/// A layout node that produces an empty frame. +/// +/// The packed version of this is returned by [`PackedNode::default`]. +#[derive(Debug, Hash)] +pub struct EmptyNode; + +impl Layout for EmptyNode { + fn layout( + &self, + _: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + let size = regions.expand.select(regions.current, Size::zero()); + let mut cts = Constraints::new(regions.expand); + cts.exact = regions.current.filter(regions.expand); + vec![Frame::new(size).constrain(cts)] + } +} + /// A packed layouting node with precomputed hash. #[derive(Clone)] pub struct PackedNode { @@ -288,22 +321,3 @@ where state.finish() } } - -/// A layout node that produces an empty frame. -/// -/// The packed version of this is returned by [`PackedNode::default`]. -#[derive(Debug, Hash)] -pub struct EmptyNode; - -impl Layout for EmptyNode { - fn layout( - &self, - _: &mut LayoutContext, - regions: &Regions, - ) -> Vec>> { - let size = regions.expand.select(regions.current, Size::zero()); - let mut cts = Constraints::new(regions.expand); - cts.exact = regions.current.filter(regions.expand); - vec![Frame::new(size).constrain(cts)] - } -} diff --git a/src/lib.rs b/src/lib.rs index 624526554..79b1a8f26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,30 +2,32 @@ //! //! # Steps //! - **Parsing:** The parsing step first transforms a plain string into an -//! [iterator of tokens][tokens]. This token stream is [parsed] into [markup]. -//! The syntactical structures describing markup and embedded code can be -//! found in the [syntax] module. +//! [iterator of tokens][tokens]. This token stream is [parsed] into a +//! [green tree]. The green tree itself is untyped, but a typed layer over it +//! is provided in the [AST] module. //! - **Evaluation:** The next step is to [evaluate] the markup. This produces a //! [module], consisting of a scope of values that were exported by the code -//! and a template with the contents of the module. This template can be -//! instantiated with a style to produce a layout tree, a high-level, fully -//! styled representation, rooted in the [document node]. The nodes of this -//! tree are self-contained and order-independent and thus much better suited -//! for layouting than the raw markup. +//! and a [node] with the contents of the module. This node can be converted +//! into a [layout tree], a hierarchical, styled representation of the +//! document. The nodes of this tree are well structured and order-independent +//! and thus much better suited for layouting than the raw markup. //! - **Layouting:** Next, the tree 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. +//! per page), ready for exporting. This step is supported by an incremental +//! [cache] that enables reuse of intermediate layouting results. //! - **Exporting:** The finished layout can be exported into a supported //! format. Currently, the only supported output format is [PDF]. //! //! [tokens]: parse::Tokens //! [parsed]: parse::parse -//! [markup]: syntax::ast::Markup -//! [evaluate]: eval::eval +//! [green tree]: syntax::GreenNode +//! [AST]: syntax::ast +//! [evaluate]: Context::evaluate //! [module]: eval::Module -//! [layout tree]: layout::LayoutTree -//! [document node]: library::DocumentNode -//! [layouted]: layout::layout +//! [node]: eval::Node +//! [layout tree]: layout::RootNode +//! [layouted]: layout::RootNode::layout +//! [cache]: layout::LayoutCache //! [PDF]: export::pdf #[macro_use] @@ -49,13 +51,12 @@ pub mod syntax; use std::rc::Rc; use crate::diag::TypResult; -use crate::eval::{Module, Scope, Styles}; +use crate::eval::{Eval, EvalContext, Module, Scope, Styles}; use crate::font::FontStore; use crate::frame::Frame; use crate::image::ImageStore; #[cfg(feature = "layout-cache")] use crate::layout::{EvictionPolicy, LayoutCache}; -use crate::library::DocumentNode; use crate::loading::Loader; use crate::source::{SourceId, SourceStore}; @@ -100,15 +101,15 @@ impl Context { } /// Evaluate a source file and return the resulting module. + /// + /// Returns either a module containing a scope with top-level bindings and a + /// layoutable node or diagnostics in the form of a vector of error message + /// with file and span information. pub fn evaluate(&mut self, id: SourceId) -> TypResult { - let ast = self.sources.get(id).ast()?; - eval::eval(self, id, &ast) - } - - /// Execute a source file and produce the resulting page nodes. - pub fn execute(&mut self, id: SourceId) -> TypResult { - let module = self.evaluate(id)?; - Ok(module.node.into_document()) + let markup = self.sources.get(id).ast()?; + let mut ctx = EvalContext::new(self, id); + let node = markup.eval(&mut ctx)?; + Ok(Module { scope: ctx.scopes.top, node }) } /// Typeset a source file into a collection of layouted frames. @@ -117,8 +118,9 @@ impl Context { /// diagnostics in the form of a vector of error message with file and span /// information. pub fn typeset(&mut self, id: SourceId) -> TypResult>> { - let tree = self.execute(id)?; - let frames = layout::layout(self, &tree); + let module = self.evaluate(id)?; + let tree = module.into_root(); + let frames = tree.layout(self); Ok(frames) } diff --git a/src/library/document.rs b/src/library/document.rs deleted file mode 100644 index 84673270e..000000000 --- a/src/library/document.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::prelude::*; -use super::PageNode; - -/// The root layout node, a document consisting of top-level page runs. -#[derive(Hash)] -pub struct DocumentNode(pub Vec); - -impl DocumentNode { - /// Layout the document into a sequence of frames, one per page. - pub fn layout(&self, ctx: &mut LayoutContext) -> Vec> { - self.0.iter().flat_map(|node| node.layout(ctx)).collect() - } -} - -impl Debug for DocumentNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Document ")?; - f.debug_list().entries(&self.0).finish() - } -} diff --git a/src/library/grid.rs b/src/library/grid.rs index e82f09ef0..d341cf5b8 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -10,7 +10,7 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult { Value::Relative(v) => vec![TrackSizing::Linear(v.into())], Value::Linear(v) => vec![TrackSizing::Linear(v)], Value::Fractional(v) => vec![TrackSizing::Fractional(v)], - Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize], + Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?], Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) diff --git a/src/library/heading.rs b/src/library/heading.rs new file mode 100644 index 000000000..c9777577b --- /dev/null +++ b/src/library/heading.rs @@ -0,0 +1,63 @@ +use super::prelude::*; +use super::{FontFamily, TextNode}; + +/// A section heading. +#[derive(Debug, Hash)] +pub struct HeadingNode { + /// The node that produces the heading's contents. + pub child: PackedNode, + /// The logical nesting depth of the section, starting from one. In the + /// default style, this controls the text size of the heading. + pub level: usize, +} + +#[properties] +impl HeadingNode { + /// The heading's font family. + pub const FAMILY: Smart = Smart::Auto; + /// The fill color of heading in the text. Just the surrounding text color + /// if `auto`. + pub const FILL: Smart = Smart::Auto; +} + +impl Construct for HeadingNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(Node::block(Self { + child: args.expect::("body")?.into_block(), + level: args.named("level")?.unwrap_or(1), + })) + } +} + +impl Set for HeadingNode { + fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> { + styles.set_opt(Self::FAMILY, args.named("family")?); + styles.set_opt(Self::FILL, args.named("fill")?); + Ok(()) + } +} + +impl Layout for HeadingNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + let upscale = (1.6 - 0.1 * self.level as f64).max(0.75); + ctx.styles.set(TextNode::STRONG, true); + ctx.styles.set(TextNode::SIZE, Relative::new(upscale).into()); + + if let Smart::Custom(family) = ctx.styles.get_ref(Self::FAMILY) { + let list: Vec<_> = std::iter::once(FontFamily::named(family)) + .chain(ctx.styles.get_ref(TextNode::FAMILY_LIST).iter().cloned()) + .collect(); + ctx.styles.set(TextNode::FAMILY_LIST, list); + } + + if let Smart::Custom(fill) = ctx.styles.get(Self::FILL) { + ctx.styles.set(TextNode::FILL, fill); + } + + self.child.layout(ctx, regions) + } +} diff --git a/src/library/list.rs b/src/library/list.rs new file mode 100644 index 000000000..74f0abe8a --- /dev/null +++ b/src/library/list.rs @@ -0,0 +1,102 @@ +use std::hash::Hash; + +use super::prelude::*; +use super::{GridNode, TextNode, TrackSizing}; + +/// An unordered or ordered list. +#[derive(Debug, Hash)] +pub struct ListNode { + /// The node that produces the item's body. + pub child: PackedNode, + /// The list labelling style -- unordered or ordered. + pub labelling: L, +} + +#[properties] +impl ListNode { + /// The indentation of each item's label. + pub const LABEL_INDENT: Linear = Relative::new(0.0).into(); + /// The space between the label and the body of each item. + pub const BODY_INDENT: Linear = Relative::new(0.5).into(); +} + +impl Construct for ListNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(args + .all() + .map(|node: Node| { + Node::block(Self { + child: node.into_block(), + labelling: L::default(), + }) + }) + .sum()) + } +} + +impl Set for ListNode { + fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> { + styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?); + styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?); + Ok(()) + } +} + +impl Layout for ListNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + let em = ctx.styles.get(TextNode::SIZE).abs; + let label_indent = ctx.styles.get(Self::LABEL_INDENT).resolve(em); + let body_indent = ctx.styles.get(Self::BODY_INDENT).resolve(em); + + let columns = vec![ + TrackSizing::Linear(label_indent.into()), + TrackSizing::Auto, + TrackSizing::Linear(body_indent.into()), + TrackSizing::Auto, + ]; + + let children = vec![ + PackedNode::default(), + Node::Text(self.labelling.label()).into_block(), + PackedNode::default(), + self.child.clone(), + ]; + + GridNode { + tracks: Spec::new(columns, vec![]), + gutter: Spec::default(), + children, + } + .layout(ctx, regions) + } +} + +/// How to label a list. +pub trait Labelling: Debug + Default + Hash + 'static { + /// Return the item's label. + fn label(&self) -> EcoString; +} + +/// Unordered list labelling style. +#[derive(Debug, Default, Hash)] +pub struct Unordered; + +impl Labelling for Unordered { + fn label(&self) -> EcoString { + '•'.into() + } +} + +/// Ordered list labelling style. +#[derive(Debug, Default, Hash)] +pub struct Ordered(pub Option); + +impl Labelling for Ordered { + fn label(&self) -> EcoString { + format_eco!("{}.", self.0.unwrap_or(1)) + } +} diff --git a/src/library/mod.rs b/src/library/mod.rs index 5852f2bb6..b2dd0dbe5 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -4,11 +4,12 @@ //! definitions. mod align; -mod document; mod flow; mod grid; +mod heading; mod image; mod link; +mod list; mod pad; mod page; mod par; @@ -41,10 +42,11 @@ mod prelude { pub use self::image::*; pub use align::*; -pub use document::*; pub use flow::*; pub use grid::*; +pub use heading::*; pub use link::*; +pub use list::*; pub use pad::*; pub use page::*; pub use par::*; @@ -68,21 +70,29 @@ pub fn new() -> Scope { std.def_class::("page"); std.def_class::("par"); std.def_class::("text"); + std.def_class::("heading"); + std.def_class::>("list"); + std.def_class::>("enum"); // Text functions. + // TODO(style): These should be classes, once that works for inline nodes. std.def_func("strike", strike); std.def_func("underline", underline); std.def_func("overline", overline); std.def_func("link", link); - // Layout functions. - std.def_func("h", h); - std.def_func("v", v); - std.def_func("box", box_); - std.def_func("block", block); + // Break and spacing functions. std.def_func("pagebreak", pagebreak); std.def_func("parbreak", parbreak); std.def_func("linebreak", linebreak); + std.def_func("h", h); + std.def_func("v", v); + + // Layout functions. + // TODO(style): Decide which of these should be classes + // (and which of their properties should be settable). + std.def_func("box", box_); + std.def_func("block", block); std.def_func("stack", stack); std.def_func("grid", grid); std.def_func("pad", pad); @@ -91,8 +101,6 @@ pub fn new() -> Scope { std.def_func("move", move_); std.def_func("scale", scale); std.def_func("rotate", rotate); - - // Element functions. std.def_func("image", image); std.def_func("rect", rect); std.def_func("square", square); @@ -118,6 +126,7 @@ pub fn new() -> Scope { std.def_func("sorted", sorted); // Predefined colors. + // TODO: More colors. std.def_const("white", RgbaColor::WHITE); std.def_const("black", RgbaColor::BLACK); std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); @@ -151,3 +160,15 @@ castable! { Expected: "color", Value::Color(color) => Paint::Solid(color), } + +castable! { + usize, + Expected: "non-negative integer", + Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?, +} + +castable! { + String, + Expected: "string", + Value::Str(string) => string.into(), +} diff --git a/src/library/page.rs b/src/library/page.rs index 3bb5cbd33..7fbcd058f 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -44,8 +44,6 @@ impl PageNode { impl Construct for PageNode { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { - // TODO(set): Make sure it's really a page so that it doesn't merge - // with adjacent pages. Ok(Node::Page(args.expect::("body")?.into_block())) } } @@ -69,13 +67,12 @@ impl Set for PageNode { } let margins = args.named("margins")?; - - set!(styles, Self::FLIPPED => args.named("flipped")?); - set!(styles, Self::LEFT => args.named("left")?.or(margins)); - set!(styles, Self::TOP => args.named("top")?.or(margins)); - set!(styles, Self::RIGHT => args.named("right")?.or(margins)); - set!(styles, Self::BOTTOM => args.named("bottom")?.or(margins)); - set!(styles, Self::FILL => args.named("fill")?); + styles.set_opt(Self::FLIPPED, args.named("flipped")?); + styles.set_opt(Self::LEFT, args.named("left")?.or(margins)); + styles.set_opt(Self::TOP, args.named("top")?.or(margins)); + styles.set_opt(Self::RIGHT, args.named("right")?.or(margins)); + styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(margins)); + styles.set_opt(Self::FILL, args.named("fill")?); Ok(()) } diff --git a/src/library/par.rs b/src/library/par.rs index 9a70b2c71..5dffd1c01 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -74,10 +74,10 @@ impl Set for ParNode { align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right }); } - set!(styles, Self::DIR => dir); - set!(styles, Self::ALIGN => align); - set!(styles, Self::LEADING => leading); - set!(styles, Self::SPACING => spacing); + styles.set_opt(Self::DIR, dir); + styles.set_opt(Self::ALIGN, align); + styles.set_opt(Self::LEADING, leading); + styles.set_opt(Self::SPACING, spacing); Ok(()) } @@ -93,8 +93,7 @@ impl Layout for ParNode { let text = self.collect_text(); // Find out the BiDi embedding levels. - let default_level = Level::from_dir(ctx.styles.get(Self::DIR)); - let bidi = BidiInfo::new(&text, default_level); + let bidi = BidiInfo::new(&text, Level::from_dir(ctx.styles.get(Self::DIR))); // Prepare paragraph layout by building a representation on which we can // do line breaking without layouting each and every line from scratch. diff --git a/src/library/text.rs b/src/library/text.rs index e0cbb1ad3..4ff9b5cd9 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -56,11 +56,11 @@ impl TextNode { /// A prioritized sequence of font families. pub const FAMILY_LIST: Vec = vec![FontFamily::SansSerif]; /// The serif font family/families. - pub const SERIF_LIST: Vec = vec!["ibm plex serif".into()]; + pub const SERIF_LIST: Vec = vec![NamedFamily::new("IBM Plex Serif")]; /// The sans-serif font family/families. - pub const SANS_SERIF_LIST: Vec = vec!["ibm plex sans".into()]; + pub const SANS_SERIF_LIST: Vec = vec![NamedFamily::new("IBM Plex Sans")]; /// The monospace font family/families. - pub const MONOSPACE_LIST: Vec = vec!["ibm plex mono".into()]; + pub const MONOSPACE_LIST: Vec = vec![NamedFamily::new("IBM Plex Mono")]; /// Whether to allow font fallback when the primary font list contains no /// match. pub const FALLBACK: bool = true; @@ -139,32 +139,38 @@ impl Set for TextNode { (!families.is_empty()).then(|| families) }); - set!(styles, Self::FAMILY_LIST => list); - set!(styles, Self::SERIF_LIST => args.named("serif")?); - set!(styles, Self::SANS_SERIF_LIST => args.named("sans-serif")?); - set!(styles, Self::MONOSPACE_LIST => args.named("monospace")?); - set!(styles, Self::FALLBACK => args.named("fallback")?); - set!(styles, Self::STYLE => args.named("style")?); - set!(styles, Self::WEIGHT => args.named("weight")?); - set!(styles, Self::STRETCH => args.named("stretch")?); - set!(styles, Self::FILL => args.named("fill")?.or_else(|| args.find())); - set!(styles, Self::SIZE => args.named("size")?.or_else(|| args.find())); - set!(styles, Self::TRACKING => args.named("tracking")?.map(Em::new)); - set!(styles, Self::TOP_EDGE => args.named("top-edge")?); - set!(styles, Self::BOTTOM_EDGE => args.named("bottom-edge")?); - set!(styles, Self::KERNING => args.named("kerning")?); - set!(styles, Self::SMALLCAPS => args.named("smallcaps")?); - set!(styles, Self::ALTERNATES => args.named("alternates")?); - set!(styles, Self::STYLISTIC_SET => args.named("stylistic-set")?); - set!(styles, Self::LIGATURES => args.named("ligatures")?); - set!(styles, Self::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?); - set!(styles, Self::HISTORICAL_LIGATURES => args.named("historical-ligatures")?); - set!(styles, Self::NUMBER_TYPE => args.named("number-type")?); - set!(styles, Self::NUMBER_WIDTH => args.named("number-width")?); - set!(styles, Self::NUMBER_POSITION => args.named("number-position")?); - set!(styles, Self::SLASHED_ZERO => args.named("slashed-zero")?); - set!(styles, Self::FRACTIONS => args.named("fractions")?); - set!(styles, Self::FEATURES => args.named("features")?); + styles.set_opt(Self::FAMILY_LIST, list); + styles.set_opt(Self::SERIF_LIST, args.named("serif")?); + styles.set_opt(Self::SANS_SERIF_LIST, args.named("sans-serif")?); + styles.set_opt(Self::MONOSPACE_LIST, args.named("monospace")?); + styles.set_opt(Self::FALLBACK, args.named("fallback")?); + styles.set_opt(Self::STYLE, args.named("style")?); + styles.set_opt(Self::WEIGHT, args.named("weight")?); + styles.set_opt(Self::STRETCH, args.named("stretch")?); + styles.set_opt(Self::FILL, args.named("fill")?.or_else(|| args.find())); + styles.set_opt(Self::SIZE, args.named("size")?.or_else(|| args.find())); + styles.set_opt(Self::TRACKING, args.named("tracking")?.map(Em::new)); + styles.set_opt(Self::TOP_EDGE, args.named("top-edge")?); + styles.set_opt(Self::BOTTOM_EDGE, args.named("bottom-edge")?); + styles.set_opt(Self::KERNING, args.named("kerning")?); + styles.set_opt(Self::SMALLCAPS, args.named("smallcaps")?); + styles.set_opt(Self::ALTERNATES, args.named("alternates")?); + styles.set_opt(Self::STYLISTIC_SET, args.named("stylistic-set")?); + styles.set_opt(Self::LIGATURES, args.named("ligatures")?); + styles.set_opt( + Self::DISCRETIONARY_LIGATURES, + args.named("discretionary-ligatures")?, + ); + styles.set_opt( + Self::HISTORICAL_LIGATURES, + args.named("historical-ligatures")?, + ); + styles.set_opt(Self::NUMBER_TYPE, args.named("number-type")?); + styles.set_opt(Self::NUMBER_WIDTH, args.named("number-width")?); + styles.set_opt(Self::NUMBER_POSITION, args.named("number-position")?); + styles.set_opt(Self::SLASHED_ZERO, args.named("slashed-zero")?); + styles.set_opt(Self::FRACTIONS, args.named("fractions")?); + styles.set_opt(Self::FEATURES, args.named("features")?); Ok(()) } @@ -188,8 +194,15 @@ pub enum FontFamily { SansSerif, /// A family in which (almost) all glyphs are of equal width. Monospace, - /// A specific family with a name. - Named(String), + /// A specific font family like "Arial". + Named(NamedFamily), +} + +impl FontFamily { + /// Create a named font family variant, directly from a string. + pub fn named(string: &str) -> Self { + Self::Named(NamedFamily::new(string)) + } } impl Debug for FontFamily { @@ -203,15 +216,37 @@ impl Debug for FontFamily { } } +/// A specific font family like "Arial". +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct NamedFamily(String); + +impl NamedFamily { + /// Create a named font family variant. + pub fn new(string: &str) -> Self { + Self(string.to_lowercase()) + } + + /// The lowercased family name. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Debug for NamedFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + dynamic! { FontFamily: "font family", - Value::Str(string) => Self::Named(string.to_lowercase().into()), + Value::Str(string) => Self::named(&string), } castable! { Vec, Expected: "string, generic family or array thereof", - Value::Str(string) => vec![FontFamily::Named(string.to_lowercase().into())], + Value::Str(string) => vec![FontFamily::named(&string)], Value::Array(values) => { values.into_iter().filter_map(|v| v.cast().ok()).collect() }, @@ -219,13 +254,13 @@ castable! { } castable! { - Vec, + Vec, Expected: "string or array of strings", - Value::Str(string) => vec![string.to_lowercase().into()], + Value::Str(string) => vec![NamedFamily::new(&string)], Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| string.to_lowercase().into()) + .map(|string: EcoString| NamedFamily::new(&string)) .collect(), } @@ -243,7 +278,10 @@ castable! { castable! { FontWeight, Expected: "integer or string", - Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number), + Value::Int(v) => Value::Int(v) + .cast::()? + .try_into() + .map_or(Self::BLACK, Self::from_number), Value::Str(string) => match string.as_str() { "thin" => Self::THIN, "extralight" => Self::EXTRALIGHT, @@ -681,7 +719,7 @@ fn families(styles: &Styles) -> impl Iterator + Clone { head.iter() .chain(core) - .map(String::as_str) + .map(|named| named.as_str()) .chain(tail.iter().copied()) } @@ -770,7 +808,7 @@ pub struct ShapedText<'a> { /// The text direction. pub dir: Dir, /// The text's style properties. - // TODO(set): Go back to reference. + // TODO(style): Go back to reference. pub styles: Styles, /// The font size. pub size: Size, diff --git a/src/source.rs b/src/source.rs index 5fd85ed9e..432688a0b 100644 --- a/src/source.rs +++ b/src/source.rs @@ -149,12 +149,12 @@ impl SourceFile { Self::new(SourceId(0), Path::new(""), src.into()) } - /// The root node of the untyped green tree. + /// The root node of the file's untyped green tree. pub fn root(&self) -> &Rc { &self.root } - /// The file's abstract syntax tree. + /// The root node of the file's typed abstract syntax tree. pub fn ast(&self) -> TypResult { let red = RedNode::from_root(self.root.clone(), self.id); let errors = red.errors(); diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 9190953f9..ae8ecdc99 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -1,4 +1,6 @@ //! A typed layer over the red-green tree. +//! +//! The AST is rooted in the [`Markup`] node. use std::ops::Deref; @@ -283,6 +285,7 @@ impl Expr { Self::Ident(_) | Self::Call(_) | Self::Let(_) + | Self::Set(_) | Self::If(_) | Self::While(_) | Self::For(_) diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index b9b00487f..d9ad42a88 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -187,7 +187,7 @@ impl From for Green { impl Debug for GreenData { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{:?}: {}", self.kind, self.len()) + write!(f, "{:?}: {}", self.kind, self.len) } } diff --git a/tests/ref/layout/containers.png b/tests/ref/layout/box-block.png similarity index 100% rename from tests/ref/layout/containers.png rename to tests/ref/layout/box-block.png diff --git a/tests/ref/elements/image.png b/tests/ref/layout/image.png similarity index 100% rename from tests/ref/elements/image.png rename to tests/ref/layout/image.png diff --git a/tests/ref/layout/background.png b/tests/ref/layout/place-background.png similarity index 100% rename from tests/ref/layout/background.png rename to tests/ref/layout/place-background.png diff --git a/tests/ref/layout/placed.png b/tests/ref/layout/place.png similarity index 100% rename from tests/ref/layout/placed.png rename to tests/ref/layout/place.png diff --git a/tests/ref/layout/aspect.png b/tests/ref/layout/shape-aspect.png similarity index 100% rename from tests/ref/layout/aspect.png rename to tests/ref/layout/shape-aspect.png diff --git a/tests/ref/elements/circle.png b/tests/ref/layout/shape-circle.png similarity index 100% rename from tests/ref/elements/circle.png rename to tests/ref/layout/shape-circle.png diff --git a/tests/ref/elements/ellipse.png b/tests/ref/layout/shape-ellipse.png similarity index 100% rename from tests/ref/elements/ellipse.png rename to tests/ref/layout/shape-ellipse.png diff --git a/tests/ref/elements/fill-stroke.png b/tests/ref/layout/shape-fill-stroke.png similarity index 100% rename from tests/ref/elements/fill-stroke.png rename to tests/ref/layout/shape-fill-stroke.png diff --git a/tests/ref/elements/rect.png b/tests/ref/layout/shape-rect.png similarity index 100% rename from tests/ref/elements/rect.png rename to tests/ref/layout/shape-rect.png diff --git a/tests/ref/elements/square.png b/tests/ref/layout/shape-square.png similarity index 100% rename from tests/ref/elements/square.png rename to tests/ref/layout/shape-square.png diff --git a/tests/ref/layout/spacing.png b/tests/ref/layout/spacing.png index 6c7a4e63d48953355c3e2a612485b3fdc6cafdd4..09f7d6d90a2c499bcf31e2c20a04d4a01872bebd 100644 GIT binary patch literal 2927 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 literal 2412 zcmbVOc{J2}8)r=XvP?6!F|v%oSgvI($(m`#Fm85=;);-UkR>inwn6qKTS$p(-Zw&v zU0%i|Lye3?mMry#YD}^dU3EI=zW2TL{_&pk-Oh8K^L@V0^ZlI9=Si`#G85nx=jGtw z5HKg2*l}=hg7@b%uy5eFTD+yt!EsQ_+~l}JOyrZ=)lYfXBda%48(-T^Nnz< zL8s&u>WW_kR^KfGsc&;I@222$GPF2F>u5CYvX#W$=L4Cf(u32cxPmn<#nfA0M!!LK z0s+85^J`I3F<(&`zpU z=Nq0$sqA*p=~UW1b{N)LEcXKbtLwqu?3!w^ZP&Y?h!g)|4_K zMm&~P=zKHr;Os@js0(xXwN-14_F^aF9EF?b3KuwuOF?Ny;sVx!Xc+~HB)D@$9p_6!1D}X=%@)^2IE6%>mgtkpEVZB4T0FOw&4<&^QKqTWK=`5FGblKCb>4VTl{=(oaq#1$2+QPR^QV^UGj`sB+#$Ff zZ^Q{ab1uwT4GjnL92HhnF=rfb`{aPg#dA7@`f1!tke%n9$a!!eRi;i~COluGG7way zz!qX&b*`8zzu$E3u_g{mbEUY*{tdt1Q2j}p<}#=*Jsu@b+BV8M#`F3iASV#p#E{Qq z5@ifF4Pov6;!WPqE@uM1^<~gT+knCq)$GgZ(+wF~qZi~`GFe-C*vQUE-z$02}gLWiOyZf`Vxg?$KBggxUzuR_zy z0a$r8Xwec^F9y=9um(hh=u1Or>?MA8w0W3sl>LjK>7n2BWo3TnTry&cJ!Tk+Kqk%&Dbur10f$XGJ-kt5(uh_Hl?rd}_lALz5PM6}JF&A;U9_NZTE$83vy{$`rRr5Vj|`3yC?L9vr&ASUxiI@rrL7Y$U=>qEN-8j*1FNFW&$tsgO1hR9W*RoGS z(fZ|B%{r(P(vnvWFWy%G9fL1xP`NW#C1dx7_@CYQJEQz3&(QG5O7^LkE$}!Q2zRkw zdJ0I5?Dof-nTYYm7|$(S0lWs4^OK!-R@-H23fJF9$TaIFkxGe~_RS5}D?!JLW_1Iq zF=aN_{ID+c!x?c?32wvURdViD8A`!>@&K8zts9_Ts~C~->MpM}_lhmJGpGE~Cj)4I zoM$MC-fA8Wns>5%EKRsVP;PH!8!F=@ib8velV*E_h-rRgNL*FxBXo_^AF)zq#~Lj1Qf( z*s{@Eq0nMV(a&z*d)>OhCX8JSLeg)eM{l)Hs!izx?r6Vcv_XA>J2KEH?ep!|hbU%d zNWGGh5v2cp8j_e>(fzzj^^vfWtXQ%9-M z3fVlalHo4c`AFALl|#|c)>N1MolQU@Fxdfb@v;_9=B~3@=}$>@5VL)wTTR3BQ!V+) zMu1~8YkIQ3I~qnTyS>Ry-TMdNV*g8Lm7y}o`|8hKo*_RSdTae>aepA2|Jz}<5JmAi zT_8W;yR!eFsr9+VK6{X4)O=|hs_6xD2Rc`9wL8bVTOQ1}pHoO5>E)+ml$Cob13*bh z*UL7p*w8>Y?PXg+XkcJJ9-iz3$uzXomC45J0|y`IuSy2b#xM2(;S-Id6!g)A4OI4O zub78L;Y9e&y&v)8zUoiM-Tc-N#AFpe`Tz2wLce~Pt?<9uw)ao}0FSuMGZfPN)5&%7 z%8QVxw0f4n(eh*3@fRT@jB<6@b_H9aRb_x+kPM1sGRnvG@}7NJ9So8?OmwBm^G_Q| zInMbXa<1cfSQ5qFKY}7y=8|gs*Y+m$qyA?uBZ+ diff --git a/tests/ref/markup/heading.png b/tests/ref/markup/heading.png index 55c681b0bf899605e4e1115a5668fb1bf745a57d..52911d66245d3c511109a71f85630d6b62134dab 100644 GIT binary patch literal 9141 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 literal 6474 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 literal 0 HcmV?d00001 diff --git a/tests/ref/style/set-site.png b/tests/ref/style/set-site.png new file mode 100644 index 0000000000000000000000000000000000000000..affe2e1c7c83ea320032e3e43c4108b36e8833dd GIT binary patch literal 4252 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 literal 0 HcmV?d00001 diff --git a/tests/ref/style/set-toggle.png b/tests/ref/style/set-toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..ae8101cab40b28e2d947384e751325b022ee7b4a GIT binary patch literal 968 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZJ2`*^!+R^q6b1(7eoq(2kcwMx?_4ZgY#_n% z;4MQbgX|1G7j{4PCCrx^Uo^aK5PHM+gfE0ibw$fw_K)Z1*q)nfJNabdrr(nuB%FU@ z{A;89mWD+dT_9v5@oxi%BZreflR%3Ciy|uXzjWivN%c%lOb<@2-#6#wy&%hTiE_U13iVW<)`vuVuYcfQV}#5KXCfm7m>; z5uZ?H`EC(<)8!irZqH{p@Jl#|{Xp%6g*Nwke!lu@zPJ3JW((tny&KH`a;k@)H;C)=7V{&wxw9|O+O0m@=ac9w;dxFT4m;lZYaHc_4b}Un zdP~`x_e`}5NBM7IwxURVekt#pyf40Y0Ie(7IY&CAb573XJ&F^0Qrq6~KCG*E+O(`C zy5*CV{>O`t16f}CZ@ee8n4g6ZJXfe^sAw@4oAbZ`)-4-J4&~ov_!s+WA3C zQRemSe>o@qjS@BNZ9DLytg?H{zZN+O5B@E&*G;qQofJ;CC`&S2FXg>-u}HdgT$0&7dsm>FVdQ&MBb@0K*`hL;wH) literal 0 HcmV?d00001 diff --git a/tests/ref/text/em.png b/tests/ref/text/em.png new file mode 100644 index 0000000000000000000000000000000000000000..4c168db657ef3e347ef8f9047e81518985ec942e GIT binary patch literal 878 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 J%8?eK{{`hFI&%O3 literal 7388 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 YVSZ(Cyp?_VT?qi_Y8q%%sXdPTA1xW=*8l(j diff --git a/tests/ref/text/whitespace.png b/tests/ref/text/whitespace.png index 7e79c1772296ad124130cb0bde8d9901379d9a72..36fb24752dcc6115ea1f44b8fcfcb44a35e7ca9f 100644 GIT binary patch literal 4674 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 { + let (frames, mut errors) = match ctx.evaluate(id) { + Ok(module) => { + let tree = module.into_root(); if debug { - println!("{:#?}", document); + println!("{:#?}", tree); } - let mut frames = layout(ctx, &document); + let mut frames = tree.layout(ctx); #[cfg(feature = "layout-cache")] - (ok &= test_incremental(ctx, i, &document, &frames)); + (ok &= test_incremental(ctx, i, &tree, &frames)); if !compare_ref { frames.clear(); @@ -311,7 +312,7 @@ fn test_part( fn test_incremental( ctx: &mut Context, i: usize, - document: &DocumentNode, + tree: &RootNode, frames: &[Rc], ) -> bool { let mut ok = true; @@ -326,7 +327,7 @@ fn test_incremental( ctx.layouts.turnaround(); - let cached = silenced(|| layout(ctx, document)); + let cached = silenced(|| tree.layout(ctx)); let misses = ctx .layouts .entries()