diff options
| author | Laurenz <laurmaedje@gmail.com> | 2021-12-20 14:18:29 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2021-12-21 00:20:24 +0100 |
| commit | 11565a40b315212474f52eb576a9fd92b11f1132 (patch) | |
| tree | c6b7afb35103065bc92b407094ca905bb75cfc73 /src | |
| parent | 958f74f77707340f34ee36d09492bdb74523aa2a (diff) | |
Set Rules Episode IX: The Rise of Testing
Diffstat (limited to 'src')
| -rw-r--r-- | src/eval/mod.rs | 84 | ||||
| -rw-r--r-- | src/eval/node.rs | 31 | ||||
| -rw-r--r-- | src/eval/styles.rs | 90 | ||||
| -rw-r--r-- | src/layout/mod.rs | 100 | ||||
| -rw-r--r-- | src/lib.rs | 54 | ||||
| -rw-r--r-- | src/library/document.rs | 20 | ||||
| -rw-r--r-- | src/library/grid.rs | 2 | ||||
| -rw-r--r-- | src/library/heading.rs | 63 | ||||
| -rw-r--r-- | src/library/list.rs | 102 | ||||
| -rw-r--r-- | src/library/mod.rs | 37 | ||||
| -rw-r--r-- | src/library/page.rs | 15 | ||||
| -rw-r--r-- | src/library/par.rs | 11 | ||||
| -rw-r--r-- | src/library/text.rs | 116 | ||||
| -rw-r--r-- | src/source.rs | 4 | ||||
| -rw-r--r-- | src/syntax/ast.rs | 3 | ||||
| -rw-r--r-- | src/syntax/mod.rs | 2 |
16 files changed, 474 insertions, 260 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs index ae330134..d05f2ddf 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<Module> { - 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<Self::Output>; +} + /// 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<Self::Output>; -} - 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<Self::Output> { - 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<Self::Output> { - 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,22 +255,11 @@ impl Eval for EnumNode { type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { - let body = self.body().eval(ctx)?; - let label = format_eco!("{}.", self.number().unwrap_or(1)); - labelled(ctx, label, body) - } -} - -/// Evaluate a labelled list / enum. -fn labelled(_: &mut EvalContext, label: EcoString, body: Node) -> TypResult<Node> { - // 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()], - })) + Ok(Node::block(library::ListNode { + child: self.body().eval(ctx)?.into_block(), + labelling: library::Ordered(self.number()), + })) + } } impl Eval for Expr { diff --git a/src/eval/node.rs b/src/eval/node.rs index acdf4ed6..e2b02955 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<Self> { 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<PageNode>, @@ -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 2396646f..5304e0ad 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<P: Property>(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<P: Property>(&mut self, key: P, value: P::Value) { let id = StyleId::of::<P>(); @@ -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<P: Property>(&mut self, key: P, value: Option<P::Value>) { + if let Some(value) = value { + self.set(key, value); + } + } + /// Toggle a boolean style property. pub fn toggle<P: Property<Value = bool>>(&mut self, key: P) { let id = StyleId::of::<P>(); @@ -82,13 +89,22 @@ impl Styles { } /// Get a reference to a style directly in this map (no default value). - pub fn get_direct<P: Property>(&self, _: P) -> Option<&P::Value> { + fn get_direct<P: Property>(&self, _: P) -> Option<&P::Value> { self.map .iter() .find(|pair| pair.0 == StyleId::of::<P>()) .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<F>(&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<P> 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<P: Property> 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::<P>()) } } - -/// 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 cf714f88..bc28e893 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -20,13 +20,52 @@ 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<Rc<Frame>> { - 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<PageNode>); + +impl RootNode { + /// Layout the document into a sequence of frames, one per page. + pub fn layout(&self, ctx: &mut Context) -> Vec<Rc<Frame>> { + let mut ctx = LayoutContext::new(ctx); + self.0.iter().flat_map(|node| node.layout(&mut ctx)).collect() + } +} + +impl Debug for RootNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Root ")?; + f.debug_list().entries(&self.0).finish() + } +} + +/// A node that can be layouted into a sequence of regions. +/// +/// Layout return one frame per used region alongside constraints that define +/// whether the result is reusable in other regions. +pub trait Layout { + /// Layout the node into the given regions, producing constrained frames. + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>>; + + /// Convert to a packed node. + fn pack(self) -> PackedNode + where + Self: Debug + Hash + Sized + 'static, + { + PackedNode { + #[cfg(feature = "layout-cache")] + hash: self.hash64(), + node: Rc::new(self), + styles: Styles::new(), + } + } } /// The context for layouting. @@ -39,6 +78,7 @@ pub struct LayoutContext<'a> { #[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")] @@ -60,29 +100,22 @@ impl<'a> LayoutContext<'a> { } } -/// A node that can be layouted into a sequence of regions. +/// A layout node that produces an empty frame. /// -/// Layout return one frame per used region alongside constraints that define -/// whether the result is reusable in other regions. -pub trait Layout { - /// Layout the node into the given regions, producing constrained frames. +/// The packed version of this is returned by [`PackedNode::default`]. +#[derive(Debug, Hash)] +pub struct EmptyNode; + +impl Layout for EmptyNode { fn layout( &self, - ctx: &mut LayoutContext, + _: &mut LayoutContext, regions: &Regions, - ) -> Vec<Constrained<Rc<Frame>>>; - - /// Convert to a packed node. - fn pack(self) -> PackedNode - where - Self: Debug + Hash + Sized + 'static, - { - PackedNode { - #[cfg(feature = "layout-cache")] - hash: self.hash64(), - node: Rc::new(self), - styles: Styles::new(), - } + ) -> Vec<Constrained<Rc<Frame>>> { + 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)] } } @@ -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<Constrained<Rc<Frame>>> { - 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)] - } -} @@ -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<Module> { - 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<DocumentNode> { - 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<Vec<Rc<Frame>>> { - 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 84673270..00000000 --- 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<PageNode>); - -impl DocumentNode { - /// Layout the document into a sequence of frames, one per page. - pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { - 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 e82f09ef..d341cf5b 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> { 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 00000000..c9777577 --- /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<String> = Smart::Auto; + /// The fill color of heading in the text. Just the surrounding text color + /// if `auto`. + pub const FILL: Smart<Paint> = Smart::Auto; +} + +impl Construct for HeadingNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + Ok(Node::block(Self { + child: args.expect::<Node>("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<Constrained<Rc<Frame>>> { + 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 00000000..74f0abe8 --- /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<L> { + /// The node that produces the item's body. + pub child: PackedNode, + /// The list labelling style -- unordered or ordered. + pub labelling: L, +} + +#[properties] +impl<L: Labelling> ListNode<L> { + /// 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<L: Labelling> Construct for ListNode<L> { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + Ok(args + .all() + .map(|node: Node| { + Node::block(Self { + child: node.into_block(), + labelling: L::default(), + }) + }) + .sum()) + } +} + +impl<L: Labelling> Set for ListNode<L> { + 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<L: Labelling> Layout for ListNode<L> { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>> { + 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<usize>); + +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 5852f2bb..b2dd0dbe 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::<PageNode>("page"); std.def_class::<ParNode>("par"); std.def_class::<TextNode>("text"); + std.def_class::<HeadingNode>("heading"); + std.def_class::<ListNode<Unordered>>("list"); + std.def_class::<ListNode<Ordered>>("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. + // 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("pagebreak", pagebreak); - std.def_func("parbreak", parbreak); - std.def_func("linebreak", linebreak); 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 3bb5cbd3..7fbcd058 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<Node> { - // TODO(set): Make sure it's really a page so that it doesn't merge - // with adjacent pages. Ok(Node::Page(args.expect::<Node>("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 9a70b2c7..5dffd1c0 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 e0cbb1ad..4ff9b5cd 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<FontFamily> = vec![FontFamily::SansSerif]; /// The serif font family/families. - pub const SERIF_LIST: Vec<String> = vec!["ibm plex serif".into()]; + pub const SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")]; /// The sans-serif font family/families. - pub const SANS_SERIF_LIST: Vec<String> = vec!["ibm plex sans".into()]; + pub const SANS_SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")]; /// The monospace font family/families. - pub const MONOSPACE_LIST: Vec<String> = vec!["ibm plex mono".into()]; + pub const MONOSPACE_LIST: Vec<NamedFamily> = 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<FontFamily>, 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<String>, + Vec<NamedFamily>, 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::<usize>()? + .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<Item = &str> + 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 5fd85ed9..432688a0 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<GreenNode> { &self.root } - /// The file's abstract syntax tree. + /// The root node of the file's typed abstract syntax tree. pub fn ast(&self) -> TypResult<Markup> { 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 9190953f..ae8ecdc9 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 b9b00487..d9ad42a8 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -187,7 +187,7 @@ impl From<GreenData> 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) } } |
