summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/mod.rs84
-rw-r--r--src/eval/node.rs31
-rw-r--r--src/eval/styles.rs90
-rw-r--r--src/layout/mod.rs100
-rw-r--r--src/lib.rs54
-rw-r--r--src/library/document.rs20
-rw-r--r--src/library/grid.rs2
-rw-r--r--src/library/heading.rs63
-rw-r--r--src/library/list.rs102
-rw-r--r--src/library/mod.rs37
-rw-r--r--src/library/page.rs15
-rw-r--r--src/library/par.rs11
-rw-r--r--src/library/text.rs116
-rw-r--r--src/source.rs4
-rw-r--r--src/syntax/ast.rs3
-rw-r--r--src/syntax/mod.rs2
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)]
- }
-}
diff --git a/src/lib.rs b/src/lib.rs
index 62452655..79b1a8f2 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<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)
}
}