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 6c7a4e63d..09f7d6d90 100644 Binary files a/tests/ref/layout/spacing.png and b/tests/ref/layout/spacing.png differ diff --git a/tests/ref/markup/heading.png b/tests/ref/markup/heading.png index 55c681b0b..52911d662 100644 Binary files a/tests/ref/markup/heading.png and b/tests/ref/markup/heading.png differ diff --git a/tests/ref/style/set-block.png b/tests/ref/style/set-block.png new file mode 100644 index 000000000..8ee5cfb68 Binary files /dev/null and b/tests/ref/style/set-block.png differ diff --git a/tests/ref/style/set-site.png b/tests/ref/style/set-site.png new file mode 100644 index 000000000..affe2e1c7 Binary files /dev/null and b/tests/ref/style/set-site.png differ diff --git a/tests/ref/style/set-toggle.png b/tests/ref/style/set-toggle.png new file mode 100644 index 000000000..ae8101cab Binary files /dev/null and b/tests/ref/style/set-toggle.png differ diff --git a/tests/ref/text/em.png b/tests/ref/text/em.png new file mode 100644 index 000000000..4c168db65 Binary files /dev/null and b/tests/ref/text/em.png differ diff --git a/tests/ref/text/par.png b/tests/ref/text/par.png index 900c668ce..03117e673 100644 Binary files a/tests/ref/text/par.png and b/tests/ref/text/par.png differ diff --git a/tests/ref/text/whitespace.png b/tests/ref/text/whitespace.png index 7e79c1772..36fb24752 100644 Binary files a/tests/ref/text/whitespace.png and b/tests/ref/text/whitespace.png differ diff --git a/tests/typ/layout/containers.typ b/tests/typ/layout/box-block.typ similarity index 100% rename from tests/typ/layout/containers.typ rename to tests/typ/layout/box-block.typ diff --git a/tests/typ/elements/image.typ b/tests/typ/layout/image.typ similarity index 100% rename from tests/typ/elements/image.typ rename to tests/typ/layout/image.typ diff --git a/tests/typ/layout/background.typ b/tests/typ/layout/place-background.typ similarity index 100% rename from tests/typ/layout/background.typ rename to tests/typ/layout/place-background.typ diff --git a/tests/typ/layout/placed.typ b/tests/typ/layout/place.typ similarity index 100% rename from tests/typ/layout/placed.typ rename to tests/typ/layout/place.typ diff --git a/tests/typ/layout/aspect.typ b/tests/typ/layout/shape-aspect.typ similarity index 100% rename from tests/typ/layout/aspect.typ rename to tests/typ/layout/shape-aspect.typ diff --git a/tests/typ/elements/circle.typ b/tests/typ/layout/shape-circle.typ similarity index 100% rename from tests/typ/elements/circle.typ rename to tests/typ/layout/shape-circle.typ diff --git a/tests/typ/elements/ellipse.typ b/tests/typ/layout/shape-ellipse.typ similarity index 100% rename from tests/typ/elements/ellipse.typ rename to tests/typ/layout/shape-ellipse.typ diff --git a/tests/typ/elements/fill-stroke.typ b/tests/typ/layout/shape-fill-stroke.typ similarity index 100% rename from tests/typ/elements/fill-stroke.typ rename to tests/typ/layout/shape-fill-stroke.typ diff --git a/tests/typ/elements/rect.typ b/tests/typ/layout/shape-rect.typ similarity index 100% rename from tests/typ/elements/rect.typ rename to tests/typ/layout/shape-rect.typ diff --git a/tests/typ/elements/square.typ b/tests/typ/layout/shape-square.typ similarity index 100% rename from tests/typ/elements/square.typ rename to tests/typ/layout/shape-square.typ diff --git a/tests/typ/layout/spacing.typ b/tests/typ/layout/spacing.typ index 98a6100ca..37aa8eaaf 100644 --- a/tests/typ/layout/spacing.typ +++ b/tests/typ/layout/spacing.typ @@ -18,7 +18,17 @@ Add #h(10pt) #h(10pt) up | #h(1fr) | #h(2fr) | #h(1fr) | --- -// Test that spacing has style properties. +// Test spacing collapsing with parbreaks. +#v(0pt) +A +#v(0pt) +B +#v(0pt) + +C #parbreak() D + +--- +// Test that spacing can carry paragraph and page style properties. A[#set par(align: right);#h(1cm)]B [#set page(height: 20pt);#v(1cm)] diff --git a/tests/typ/markup/heading.typ b/tests/typ/markup/heading.typ index cb0226173..2ae97aa80 100644 --- a/tests/typ/markup/heading.typ +++ b/tests/typ/markup/heading.typ @@ -39,3 +39,12 @@ is not. = A { "B" } + +--- +// Test styling. += Heading + +#set heading(family: "Roboto", fill: eastern) + +===== Heading 🌍 +#heading(level: 5)[Heading] diff --git a/tests/typ/style/set-block.typ b/tests/typ/style/set-block.typ new file mode 100644 index 000000000..f260acdc4 --- /dev/null +++ b/tests/typ/style/set-block.typ @@ -0,0 +1,10 @@ +// Test set in code blocks. + +--- +// Test that template in block is not affected by set +// rule in block ... +A{set text(fill: eastern); [B]}C + +--- +// ... no matter the order. +A{[B]; set text(fill: eastern)}C diff --git a/tests/typ/style/set-site.typ b/tests/typ/style/set-site.typ new file mode 100644 index 000000000..97a5672d3 --- /dev/null +++ b/tests/typ/style/set-site.typ @@ -0,0 +1,30 @@ +// Test that set affects the instantiation site and not the +// definition site of a template. + +--- +// Test that text is affected by instantion-site bold. +#let x = [World] +Hello *{x}* + +--- +// Test that lists are affected by correct indents. +#set par(spacing: 4pt) +#let fruit = [ + - Apple + - Orange + #set list(body-indent: 10pt) + - Pear +] + +- Fruit +[#set list(label-indent: 10pt) + #fruit] +- No more fruit + +--- +// Test that that par spacing and text style are respected from +// the outside, but the more specific fill is respected. +#set par(spacing: 4pt) +#set text(style: "italic", fill: eastern) +#let x = [And the forest #parbreak() lay silent!] +#text(fill: forest, x) diff --git a/tests/typ/style/set-toggle.typ b/tests/typ/style/set-toggle.typ new file mode 100644 index 000000000..9f26bdf7e --- /dev/null +++ b/tests/typ/style/set-toggle.typ @@ -0,0 +1,10 @@ +// Test set rules for toggleable booleans. + +--- +// Test toggling and untoggling. +*AB_C*DE +*_* + +--- +// Test toggling and nested templates. +*A[B*[_C]]D*E diff --git a/tests/typ/text/em.typ b/tests/typ/text/em.typ new file mode 100644 index 000000000..d9b00f068 --- /dev/null +++ b/tests/typ/text/em.typ @@ -0,0 +1,17 @@ +// Test font-relative sizing. + +--- +#set text(size: 5pt) +A // 5pt +[ + #set text(size: 200%) + B // 10pt + [ + #set text(size: 150% + 1pt) + C // 16pt + #text(size: 200%)[D] // 32pt + E // 16pt + ] + F // 10pt +] +G // 5pt diff --git a/tests/typ/text/par.typ b/tests/typ/text/par.typ index 771a62180..8bd43deb0 100644 --- a/tests/typ/text/par.typ +++ b/tests/typ/text/par.typ @@ -15,6 +15,16 @@ To the right! Where the sunlight peeks behind the mountain. Third +--- +// Test that paragraph spacing uses correct set rule. +Hello + +#set par(spacing: 100pt) +World +#set par(spacing: 0pt) + +You + --- // Test that paragraph break due to incompatibility respects // spacing defined by the two adjacent paragraphs. diff --git a/tests/typ/text/whitespace.typ b/tests/typ/text/whitespace.typ index 4c3d4db5d..831a55438 100644 --- a/tests/typ/text/whitespace.typ +++ b/tests/typ/text/whitespace.typ @@ -1,32 +1,21 @@ // Test whitespace handling. --- -// Spacing around let. +// Spacing around code constructs. A#let x = 1;B #test(x, 1) \ -A #let x = 2;B #test(x, 2) \ -A#let x = 3; B #test(x, 3) +C #let x = 2;D #test(x, 2) \ +E#if true [F]G \ +H #if true{"I"} J \ +K #if true [L] else []M \ +#let c = true; N#while c [{c = false}O] P \ +#let c = true; Q #while c { c = false; "R" } S \ +T#for _ in (none,) {"U"}V --- -// Spacing around if-else. -A#if true [B]C \ -A#if true [B] C \ -A #if true{"B"}C \ -A #if true{"B"} C \ -A#if false [] else [B]C \ -A#if true [B] else [] C - ---- -// Spacing around while loop. -#let c = true; A#while c [{c = false}B]C \ -#let c = true; A#while c [{c = false}B] C \ -#let c = true; A #while c { c = false; "B" }C \ -#let c = true; A #while c { c = false; "B" } C - ---- -// Spacing around for loop. -A#for _ in (none,) [B]C \ -A#for _ in (none,) [B] C \ -A #for _ in (none,) {"B"}C +// Test spacing with comments. +A/**/B/**/C \ +A /**/ B/**/C \ +A /**/B/**/ C --- // Test that a run consisting only of whitespace isn't trimmed. @@ -37,7 +26,11 @@ A[#set text(serif); ]B Left [#set text(serif);Right]. --- -// Test that space at start of line is not trimmed. +// Test that linebreak consumed surrounding spaces. +#align(center)[A \ B \ C] + +--- +// Test that space at start of non-backslash-linebreak line isn't trimmed. A{"\n"} B --- diff --git a/tests/typeset.rs b/tests/typeset.rs index 03bd99200..c53250b34 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -17,9 +17,9 @@ use typst::font::Face; use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Size, Transform}; use typst::image::{Image, RasterImage, Svg}; -use typst::layout::layout; #[cfg(feature = "layout-cache")] -use typst::library::{DocumentNode, PageNode, TextNode}; +use typst::layout::RootNode; +use typst::library::{PageNode, TextNode}; use typst::loading::FsLoader; use typst::parse::Scanner; use typst::source::SourceFile; @@ -254,16 +254,17 @@ fn test_part( let compare_ref = local_compare_ref.unwrap_or(compare_ref); let mut ok = true; - let (frames, mut errors) = match ctx.execute(id) { - Ok(document) => { + 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()