Set Rules Episode IX: The Rise of Testing

This commit is contained in:
Laurenz 2021-12-20 14:18:29 +01:00
parent 958f74f777
commit 11565a40b3
54 changed files with 611 additions and 316 deletions

View File

@ -2,8 +2,6 @@ use std::path::Path;
use iai::{black_box, main, Iai}; use iai::{black_box, main, Iai};
use typst::eval::eval;
use typst::layout::layout;
use typst::loading::MemLoader; use typst::loading::MemLoader;
use typst::parse::{parse, Scanner, TokenMode, Tokens}; use typst::parse::{parse, Scanner, TokenMode, Tokens};
use typst::source::SourceId; use typst::source::SourceId;
@ -53,20 +51,14 @@ fn bench_parse(iai: &mut Iai) {
fn bench_eval(iai: &mut Iai) { fn bench_eval(iai: &mut Iai) {
let (mut ctx, id) = context(); let (mut ctx, id) = context();
let ast = ctx.sources.get(id).ast().unwrap(); iai.run(|| ctx.evaluate(id).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());
} }
fn bench_layout(iai: &mut Iai) { fn bench_layout(iai: &mut Iai) {
let (mut ctx, id) = context(); let (mut ctx, id) = context();
let tree = ctx.execute(id).unwrap(); let module = ctx.evaluate(id).unwrap();
iai.run(|| layout(&mut ctx, &tree)); let tree = module.into_root();
iai.run(|| tree.layout(&mut ctx));
} }
main!( main!(
@ -75,6 +67,5 @@ main!(
bench_tokenize, bench_tokenize,
bench_parse, bench_parse,
bench_eval, bench_eval,
bench_to_tree,
bench_layout bench_layout
); );

View File

@ -34,9 +34,10 @@ use std::path::PathBuf;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult}; 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::image::ImageStore;
use crate::library::{GridNode, TextNode, TrackSizing}; use crate::layout::RootNode;
use crate::library::{self, TextNode};
use crate::loading::Loader; use crate::loading::Loader;
use crate::source::{SourceId, SourceStore}; use crate::source::{SourceId, SourceStore};
use crate::syntax::ast::*; use crate::syntax::ast::*;
@ -44,15 +45,9 @@ use crate::syntax::{Span, Spanned};
use crate::util::{EcoString, RefMutExt}; use crate::util::{EcoString, RefMutExt};
use crate::Context; use crate::Context;
/// Evaluate a parsed source file into a module. /// An evaluated module, ready for importing or conversion to a root layout
pub fn eval(ctx: &mut Context, source: SourceId, markup: &Markup) -> TypResult<Module> { /// tree.
let mut ctx = EvalContext::new(ctx, source); #[derive(Debug, Default, Clone)]
let node = markup.eval(&mut ctx)?;
Ok(Module { scope: ctx.scopes.top, node })
}
/// An evaluated module, ready for importing or instantiation.
#[derive(Debug, Clone)]
pub struct Module { pub struct Module {
/// The top-level definitions that were bound in this module. /// The top-level definitions that were bound in this module.
pub scope: Scope, pub scope: Scope,
@ -60,6 +55,22 @@ pub struct Module {
pub node: Node, 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. /// The context for evaluation.
pub struct EvalContext<'a> { pub struct EvalContext<'a> {
/// The loader from which resources (files and images) are loaded. /// The loader from which resources (files and images) are loaded.
@ -124,7 +135,7 @@ impl<'a> EvalContext<'a> {
self.route.push(id); self.route.push(id);
// Evaluate the module. // 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. // Restore the old context.
let new_scopes = mem::replace(&mut self.scopes, prev_scopes); let new_scopes = mem::replace(&mut self.scopes, prev_scopes);
@ -132,7 +143,7 @@ impl<'a> EvalContext<'a> {
self.route.pop().unwrap(); self.route.pop().unwrap();
// Save the evaluated module. // 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); self.modules.insert(id, module);
Ok(id) 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 { impl Eval for Markup {
type Output = Node; type Output = Node;
@ -231,13 +233,10 @@ impl Eval for HeadingNode {
type Output = Node; type Output = Node;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let upscale = (1.6 - 0.1 * self.level() as f64).max(0.75); Ok(Node::block(library::HeadingNode {
let mut styles = Styles::new(); child: self.body().eval(ctx)?.into_block(),
styles.set(TextNode::STRONG, true); level: self.level(),
styles.set(TextNode::SIZE, Relative::new(upscale).into()); }))
Ok(Node::Block(
self.body().eval(ctx)?.into_block().styled(styles),
))
} }
} }
@ -245,8 +244,10 @@ impl Eval for ListNode {
type Output = Node; type Output = Node;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let body = self.body().eval(ctx)?; Ok(Node::block(library::ListNode {
labelled(ctx, '•'.into(), body) child: self.body().eval(ctx)?.into_block(),
labelling: library::Unordered,
}))
} }
} }
@ -254,24 +255,13 @@ impl Eval for EnumNode {
type Output = Node; type Output = Node;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let body = self.body().eval(ctx)?; Ok(Node::block(library::ListNode {
let label = format_eco!("{}.", self.number().unwrap_or(1)); child: self.body().eval(ctx)?.into_block(),
labelled(ctx, label, body) labelling: library::Ordered(self.number()),
}))
} }
} }
/// 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()],
}))
}
impl Eval for Expr { impl Eval for Expr {
type Output = Value; type Output = Value;

View File

@ -8,17 +8,18 @@ use std::ops::{Add, AddAssign};
use super::Styles; use super::Styles;
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::SpecAxis; use crate::geom::SpecAxis;
use crate::layout::{Layout, PackedNode}; use crate::layout::{Layout, PackedNode, RootNode};
use crate::library::{ use crate::library::{
DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, SpacingKind,
SpacingKind, SpacingNode, TextNode, SpacingNode, TextNode,
}; };
use crate::util::EcoString; use crate::util::EcoString;
/// A partial representation of a layout node. /// A partial representation of a layout node.
/// ///
/// A node is a composable intermediate representation that can be converted /// 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)] #[derive(Debug, PartialEq, Clone, Hash)]
pub enum Node { pub enum Node {
/// A word space. /// A word space.
@ -90,19 +91,19 @@ impl Node {
} }
} }
/// Lift to a document node, the root of the layout tree. /// Lift to a root layout tree node.
pub fn into_document(self) -> DocumentNode { pub fn into_root(self) -> RootNode {
let mut packer = Packer::new(true); let mut packer = Packer::new(true);
packer.walk(self, Styles::new()); 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> { pub fn repeat(&self, n: i64) -> StrResult<Self> {
let count = usize::try_from(n) let count = usize::try_from(n)
.map_err(|_| format!("cannot repeat this template {} times", 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])) Ok(Self::Sequence(vec![(self.clone(), Styles::new()); count]))
} }
} }
@ -117,7 +118,7 @@ impl Add for Node {
type Output = Self; type Output = Self;
fn add(self, rhs: Self) -> Self::Output { 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())]) 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 { struct Packer {
/// Whether this packer produces the top-level document. /// Whether this packer produces a root node.
top: bool, top: bool,
/// The accumulated page nodes. /// The accumulated page nodes.
pages: Vec<PageNode>, pages: Vec<PageNode>,
@ -163,10 +164,10 @@ impl Packer {
FlowNode(self.flow.children).pack() FlowNode(self.flow.children).pack()
} }
/// Finish up and return the resulting document. /// Finish up and return the resulting root node.
fn into_document(mut self) -> DocumentNode { fn into_root(mut self) -> RootNode {
self.pagebreak(); self.pagebreak();
DocumentNode(self.pages) RootNode(self.pages)
} }
/// Consider a node with the given styles. /// Consider a node with the given styles.

View File

@ -20,6 +20,11 @@ impl Styles {
Self { map: vec![] } 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. /// Create a style map with a single property-value pair.
pub fn one<P: Property>(key: P, value: P::Value) -> Self { pub fn one<P: Property>(key: P, value: P::Value) -> Self {
let mut styles = Self::new(); let mut styles = Self::new();
@ -27,11 +32,6 @@ impl Styles {
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. /// Set the value for a style property.
pub fn set<P: Property>(&mut self, key: P, value: P::Value) { pub fn set<P: Property>(&mut self, key: P, value: P::Value) {
let id = StyleId::of::<P>(); let id = StyleId::of::<P>();
@ -47,6 +47,13 @@ impl Styles {
self.map.push((id, Entry::new(key, value))); 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. /// Toggle a boolean style property.
pub fn toggle<P: Property<Value = bool>>(&mut self, key: P) { pub fn toggle<P: Property<Value = bool>>(&mut self, key: P) {
let id = StyleId::of::<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). /// 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 self.map
.iter() .iter()
.find(|pair| pair.0 == StyleId::of::<P>()) .find(|pair| pair.0 == StyleId::of::<P>())
.and_then(|pair| pair.1.downcast()) .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. /// Apply styles from `outer` in-place.
/// ///
/// Properties from `self` take precedence over the ones from `outer`. /// Properties from `self` take precedence over the ones from `outer`.
@ -105,13 +121,9 @@ impl Styles {
} }
} }
/// Create new styles combining `self` with `outer`. /// Keep only those styles that are not also in `other`.
/// pub fn erase(&mut self, other: &Self) {
/// Properties from `self` take precedence over the ones from `outer`. self.map.retain(|a| other.map.iter().all(|b| a != b));
pub fn chain(&self, outer: &Self) -> Self {
let mut styles = self.clone();
styles.apply(outer);
styles
} }
/// Keep only those styles that are also in `other`. /// 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)); 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 /// Whether two style maps are equal when filtered down to the given
/// properties. /// properties.
pub fn compatible<F>(&self, other: &Self, filter: F) -> bool pub fn compatible<F>(&self, other: &Self, filter: F) -> bool
where where
F: Fn(StyleId) -> bool, 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); let f = |e: &&(StyleId, Entry)| filter(e.0);
self.map.iter().filter(f).all(|pair| other.map.contains(pair)) self.map.iter().filter(f).all(|pair| other.map.contains(pair))
&& other.map.iter().filter(f).all(|pair| self.map.contains(pair)) && other.map.iter().filter(f).all(|pair| self.map.contains(pair))
@ -177,7 +184,7 @@ impl Entry {
impl Debug for Entry { impl Debug for Entry {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { 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 { trait Bounds: 'static {
fn as_any(&self) -> &dyn Any; 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 dyn_eq(&self, other: &Entry) -> bool;
fn hash64(&self) -> u64; fn hash64(&self) -> u64;
fn combine(&self, outer: &Entry) -> Entry; fn combine(&self, outer: &Entry) -> Entry;
} }
impl<P> Bounds for (P, P::Value) // `P` is always zero-sized. We only implement the trait for a pair of key and
where // associated value so that `P` is a constrained type parameter that we can use
P: Property, // in `dyn_fmt` to access the property's name. This way, we can effectively
P::Value: Debug + Hash + PartialEq + 'static, // 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 { fn as_any(&self) -> &dyn Any {
&self.1 &self.1
} }
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() { if f.alternate() {
write!(f, "#[{} = {:?}]", P::NAME, self.1) write!(f, "#[{} = {:?}]", P::NAME, self.1)
} else { } else {
@ -242,11 +250,12 @@ where
/// Style property keys. /// Style property keys.
/// ///
/// This trait is not intended to be implemented manually, but rather through /// This trait is not intended to be implemented manually, but rather through
/// the `properties!` macro. /// the `#[properties]` proc-macro.
pub trait Property: Copy + 'static { pub trait Property: Copy + 'static {
/// The type of this property, for example, this could be /// The type of value that is returned when getting this property from a
/// [`Length`](crate::geom::Length) for a `WIDTH` property. /// style map. For example, this could be [`Length`](crate::geom::Length)
type Value: Debug + Clone + Hash + PartialEq + 'static; /// for a `WIDTH` property.
type Value: Debug + Clone + PartialEq + Hash + 'static;
/// The name of the property, used for debug printing. /// The name of the property, used for debug printing.
const NAME: &'static str; const NAME: &'static str;
@ -257,12 +266,16 @@ pub trait Property: Copy + 'static {
/// A static reference to the default value of the property. /// A static reference to the default value of the property.
/// ///
/// This is automatically implemented through lazy-initialization in the /// 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. /// recreated all the time.
fn default_ref() -> &'static Self::Value; fn default_ref() -> &'static Self::Value;
/// Combine the property with an outer value. /// Fold the property with an outer value.
fn combine(inner: Self::Value, _: Self::Value) -> Self::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 inner
} }
} }
@ -277,12 +290,3 @@ impl StyleId {
Self(TypeId::of::<P>()) 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);
}
};
}

View File

@ -20,43 +20,25 @@ use crate::font::FontStore;
use crate::frame::Frame; use crate::frame::Frame;
use crate::geom::{Align, Linear, Point, Sides, Size, Spec, Transform}; use crate::geom::{Align, Linear, Point, Sides, Size, Spec, Transform};
use crate::image::ImageStore; use crate::image::ImageStore;
use crate::library::{AlignNode, DocumentNode, PadNode, SizedNode, TransformNode}; use crate::library::{AlignNode, PadNode, PageNode, SizedNode, TransformNode};
use crate::Context; use crate::Context;
/// Layout a document node into a collection of frames. /// The root layout node, a document consisting of top-level page runs.
pub fn layout(ctx: &mut Context, node: &DocumentNode) -> Vec<Rc<Frame>> { #[derive(Hash)]
let mut ctx = LayoutContext::new(ctx); pub struct RootNode(pub Vec<PageNode>);
node.layout(&mut ctx)
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()
}
} }
/// The context for layouting. impl Debug for RootNode {
pub struct LayoutContext<'a> { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
/// Stores parsed font faces. f.write_str("Root ")?;
pub fonts: &'a mut FontStore, f.debug_list().entries(&self.0).finish()
/// 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,
}
} }
} }
@ -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<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)]
}
}
/// A packed layouting node with precomputed hash. /// A packed layouting node with precomputed hash.
#[derive(Clone)] #[derive(Clone)]
pub struct PackedNode { pub struct PackedNode {
@ -288,22 +321,3 @@ where
state.finish() 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)]
}
}

View File

@ -2,30 +2,32 @@
//! //!
//! # Steps //! # Steps
//! - **Parsing:** The parsing step first transforms a plain string into an //! - **Parsing:** The parsing step first transforms a plain string into an
//! [iterator of tokens][tokens]. This token stream is [parsed] into [markup]. //! [iterator of tokens][tokens]. This token stream is [parsed] into a
//! The syntactical structures describing markup and embedded code can be //! [green tree]. The green tree itself is untyped, but a typed layer over it
//! found in the [syntax] module. //! is provided in the [AST] module.
//! - **Evaluation:** The next step is to [evaluate] the markup. This produces a //! - **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 //! [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 //! and a [node] with the contents of the module. This node can be converted
//! instantiated with a style to produce a layout tree, a high-level, fully //! into a [layout tree], a hierarchical, styled representation of the
//! styled representation, rooted in the [document node]. The nodes of this //! document. The nodes of this tree are well structured and order-independent
//! tree are self-contained and order-independent and thus much better suited //! and thus much better suited for layouting than the raw markup.
//! for layouting than the raw markup.
//! - **Layouting:** Next, the tree is [layouted] into a portable version of the //! - **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 //! 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 //! - **Exporting:** The finished layout can be exported into a supported
//! format. Currently, the only supported output format is [PDF]. //! format. Currently, the only supported output format is [PDF].
//! //!
//! [tokens]: parse::Tokens //! [tokens]: parse::Tokens
//! [parsed]: parse::parse //! [parsed]: parse::parse
//! [markup]: syntax::ast::Markup //! [green tree]: syntax::GreenNode
//! [evaluate]: eval::eval //! [AST]: syntax::ast
//! [evaluate]: Context::evaluate
//! [module]: eval::Module //! [module]: eval::Module
//! [layout tree]: layout::LayoutTree //! [node]: eval::Node
//! [document node]: library::DocumentNode //! [layout tree]: layout::RootNode
//! [layouted]: layout::layout //! [layouted]: layout::RootNode::layout
//! [cache]: layout::LayoutCache
//! [PDF]: export::pdf //! [PDF]: export::pdf
#[macro_use] #[macro_use]
@ -49,13 +51,12 @@ pub mod syntax;
use std::rc::Rc; use std::rc::Rc;
use crate::diag::TypResult; use crate::diag::TypResult;
use crate::eval::{Module, Scope, Styles}; use crate::eval::{Eval, EvalContext, Module, Scope, Styles};
use crate::font::FontStore; use crate::font::FontStore;
use crate::frame::Frame; use crate::frame::Frame;
use crate::image::ImageStore; use crate::image::ImageStore;
#[cfg(feature = "layout-cache")] #[cfg(feature = "layout-cache")]
use crate::layout::{EvictionPolicy, LayoutCache}; use crate::layout::{EvictionPolicy, LayoutCache};
use crate::library::DocumentNode;
use crate::loading::Loader; use crate::loading::Loader;
use crate::source::{SourceId, SourceStore}; use crate::source::{SourceId, SourceStore};
@ -100,15 +101,15 @@ impl Context {
} }
/// Evaluate a source file and return the resulting module. /// 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> { pub fn evaluate(&mut self, id: SourceId) -> TypResult<Module> {
let ast = self.sources.get(id).ast()?; let markup = self.sources.get(id).ast()?;
eval::eval(self, id, &ast) let mut ctx = EvalContext::new(self, id);
} let node = markup.eval(&mut ctx)?;
Ok(Module { scope: ctx.scopes.top, node })
/// 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())
} }
/// Typeset a source file into a collection of layouted frames. /// 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 /// diagnostics in the form of a vector of error message with file and span
/// information. /// information.
pub fn typeset(&mut self, id: SourceId) -> TypResult<Vec<Rc<Frame>>> { pub fn typeset(&mut self, id: SourceId) -> TypResult<Vec<Rc<Frame>>> {
let tree = self.execute(id)?; let module = self.evaluate(id)?;
let frames = layout::layout(self, &tree); let tree = module.into_root();
let frames = tree.layout(self);
Ok(frames) Ok(frames)
} }

View File

@ -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()
}
}

View File

@ -10,7 +10,7 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Value::Relative(v) => vec![TrackSizing::Linear(v.into())], Value::Relative(v) => vec![TrackSizing::Linear(v.into())],
Value::Linear(v) => vec![TrackSizing::Linear(v)], Value::Linear(v) => vec![TrackSizing::Linear(v)],
Value::Fractional(v) => vec![TrackSizing::Fractional(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 Value::Array(values) => values
.into_iter() .into_iter()
.filter_map(|v| v.cast().ok()) .filter_map(|v| v.cast().ok())

63
src/library/heading.rs Normal file
View File

@ -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)
}
}

102
src/library/list.rs Normal file
View File

@ -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))
}
}

View File

@ -4,11 +4,12 @@
//! definitions. //! definitions.
mod align; mod align;
mod document;
mod flow; mod flow;
mod grid; mod grid;
mod heading;
mod image; mod image;
mod link; mod link;
mod list;
mod pad; mod pad;
mod page; mod page;
mod par; mod par;
@ -41,10 +42,11 @@ mod prelude {
pub use self::image::*; pub use self::image::*;
pub use align::*; pub use align::*;
pub use document::*;
pub use flow::*; pub use flow::*;
pub use grid::*; pub use grid::*;
pub use heading::*;
pub use link::*; pub use link::*;
pub use list::*;
pub use pad::*; pub use pad::*;
pub use page::*; pub use page::*;
pub use par::*; pub use par::*;
@ -68,21 +70,29 @@ pub fn new() -> Scope {
std.def_class::<PageNode>("page"); std.def_class::<PageNode>("page");
std.def_class::<ParNode>("par"); std.def_class::<ParNode>("par");
std.def_class::<TextNode>("text"); std.def_class::<TextNode>("text");
std.def_class::<HeadingNode>("heading");
std.def_class::<ListNode<Unordered>>("list");
std.def_class::<ListNode<Ordered>>("enum");
// Text functions. // Text functions.
// TODO(style): These should be classes, once that works for inline nodes.
std.def_func("strike", strike); std.def_func("strike", strike);
std.def_func("underline", underline); std.def_func("underline", underline);
std.def_func("overline", overline); std.def_func("overline", overline);
std.def_func("link", link); std.def_func("link", link);
// Layout functions. // Break and spacing functions.
std.def_func("h", h);
std.def_func("v", v);
std.def_func("box", box_);
std.def_func("block", block);
std.def_func("pagebreak", pagebreak); std.def_func("pagebreak", pagebreak);
std.def_func("parbreak", parbreak); std.def_func("parbreak", parbreak);
std.def_func("linebreak", linebreak); 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("stack", stack);
std.def_func("grid", grid); std.def_func("grid", grid);
std.def_func("pad", pad); std.def_func("pad", pad);
@ -91,8 +101,6 @@ pub fn new() -> Scope {
std.def_func("move", move_); std.def_func("move", move_);
std.def_func("scale", scale); std.def_func("scale", scale);
std.def_func("rotate", rotate); std.def_func("rotate", rotate);
// Element functions.
std.def_func("image", image); std.def_func("image", image);
std.def_func("rect", rect); std.def_func("rect", rect);
std.def_func("square", square); std.def_func("square", square);
@ -118,6 +126,7 @@ pub fn new() -> Scope {
std.def_func("sorted", sorted); std.def_func("sorted", sorted);
// Predefined colors. // Predefined colors.
// TODO: More colors.
std.def_const("white", RgbaColor::WHITE); std.def_const("white", RgbaColor::WHITE);
std.def_const("black", RgbaColor::BLACK); std.def_const("black", RgbaColor::BLACK);
std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF));
@ -151,3 +160,15 @@ castable! {
Expected: "color", Expected: "color",
Value::Color(color) => Paint::Solid(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(),
}

View File

@ -44,8 +44,6 @@ impl PageNode {
impl Construct for PageNode { impl Construct for PageNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { 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())) Ok(Node::Page(args.expect::<Node>("body")?.into_block()))
} }
} }
@ -69,13 +67,12 @@ impl Set for PageNode {
} }
let margins = args.named("margins")?; let margins = args.named("margins")?;
styles.set_opt(Self::FLIPPED, args.named("flipped")?);
set!(styles, Self::FLIPPED => args.named("flipped")?); styles.set_opt(Self::LEFT, args.named("left")?.or(margins));
set!(styles, Self::LEFT => args.named("left")?.or(margins)); styles.set_opt(Self::TOP, args.named("top")?.or(margins));
set!(styles, Self::TOP => args.named("top")?.or(margins)); styles.set_opt(Self::RIGHT, args.named("right")?.or(margins));
set!(styles, Self::RIGHT => args.named("right")?.or(margins)); styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(margins));
set!(styles, Self::BOTTOM => args.named("bottom")?.or(margins)); styles.set_opt(Self::FILL, args.named("fill")?);
set!(styles, Self::FILL => args.named("fill")?);
Ok(()) Ok(())
} }

View File

@ -74,10 +74,10 @@ impl Set for ParNode {
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right }); align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
} }
set!(styles, Self::DIR => dir); styles.set_opt(Self::DIR, dir);
set!(styles, Self::ALIGN => align); styles.set_opt(Self::ALIGN, align);
set!(styles, Self::LEADING => leading); styles.set_opt(Self::LEADING, leading);
set!(styles, Self::SPACING => spacing); styles.set_opt(Self::SPACING, spacing);
Ok(()) Ok(())
} }
@ -93,8 +93,7 @@ impl Layout for ParNode {
let text = self.collect_text(); let text = self.collect_text();
// Find out the BiDi embedding levels. // Find out the BiDi embedding levels.
let default_level = Level::from_dir(ctx.styles.get(Self::DIR)); let bidi = BidiInfo::new(&text, Level::from_dir(ctx.styles.get(Self::DIR)));
let bidi = BidiInfo::new(&text, default_level);
// Prepare paragraph layout by building a representation on which we can // Prepare paragraph layout by building a representation on which we can
// do line breaking without layouting each and every line from scratch. // do line breaking without layouting each and every line from scratch.

View File

@ -56,11 +56,11 @@ impl TextNode {
/// A prioritized sequence of font families. /// A prioritized sequence of font families.
pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif]; pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif];
/// The serif font family/families. /// 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. /// 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. /// 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 /// Whether to allow font fallback when the primary font list contains no
/// match. /// match.
pub const FALLBACK: bool = true; pub const FALLBACK: bool = true;
@ -139,32 +139,38 @@ impl Set for TextNode {
(!families.is_empty()).then(|| families) (!families.is_empty()).then(|| families)
}); });
set!(styles, Self::FAMILY_LIST => list); styles.set_opt(Self::FAMILY_LIST, list);
set!(styles, Self::SERIF_LIST => args.named("serif")?); styles.set_opt(Self::SERIF_LIST, args.named("serif")?);
set!(styles, Self::SANS_SERIF_LIST => args.named("sans-serif")?); styles.set_opt(Self::SANS_SERIF_LIST, args.named("sans-serif")?);
set!(styles, Self::MONOSPACE_LIST => args.named("monospace")?); styles.set_opt(Self::MONOSPACE_LIST, args.named("monospace")?);
set!(styles, Self::FALLBACK => args.named("fallback")?); styles.set_opt(Self::FALLBACK, args.named("fallback")?);
set!(styles, Self::STYLE => args.named("style")?); styles.set_opt(Self::STYLE, args.named("style")?);
set!(styles, Self::WEIGHT => args.named("weight")?); styles.set_opt(Self::WEIGHT, args.named("weight")?);
set!(styles, Self::STRETCH => args.named("stretch")?); styles.set_opt(Self::STRETCH, args.named("stretch")?);
set!(styles, Self::FILL => args.named("fill")?.or_else(|| args.find())); styles.set_opt(Self::FILL, args.named("fill")?.or_else(|| args.find()));
set!(styles, Self::SIZE => args.named("size")?.or_else(|| args.find())); styles.set_opt(Self::SIZE, args.named("size")?.or_else(|| args.find()));
set!(styles, Self::TRACKING => args.named("tracking")?.map(Em::new)); styles.set_opt(Self::TRACKING, args.named("tracking")?.map(Em::new));
set!(styles, Self::TOP_EDGE => args.named("top-edge")?); styles.set_opt(Self::TOP_EDGE, args.named("top-edge")?);
set!(styles, Self::BOTTOM_EDGE => args.named("bottom-edge")?); styles.set_opt(Self::BOTTOM_EDGE, args.named("bottom-edge")?);
set!(styles, Self::KERNING => args.named("kerning")?); styles.set_opt(Self::KERNING, args.named("kerning")?);
set!(styles, Self::SMALLCAPS => args.named("smallcaps")?); styles.set_opt(Self::SMALLCAPS, args.named("smallcaps")?);
set!(styles, Self::ALTERNATES => args.named("alternates")?); styles.set_opt(Self::ALTERNATES, args.named("alternates")?);
set!(styles, Self::STYLISTIC_SET => args.named("stylistic-set")?); styles.set_opt(Self::STYLISTIC_SET, args.named("stylistic-set")?);
set!(styles, Self::LIGATURES => args.named("ligatures")?); styles.set_opt(Self::LIGATURES, args.named("ligatures")?);
set!(styles, Self::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?); styles.set_opt(
set!(styles, Self::HISTORICAL_LIGATURES => args.named("historical-ligatures")?); Self::DISCRETIONARY_LIGATURES,
set!(styles, Self::NUMBER_TYPE => args.named("number-type")?); args.named("discretionary-ligatures")?,
set!(styles, Self::NUMBER_WIDTH => args.named("number-width")?); );
set!(styles, Self::NUMBER_POSITION => args.named("number-position")?); styles.set_opt(
set!(styles, Self::SLASHED_ZERO => args.named("slashed-zero")?); Self::HISTORICAL_LIGATURES,
set!(styles, Self::FRACTIONS => args.named("fractions")?); args.named("historical-ligatures")?,
set!(styles, Self::FEATURES => args.named("features")?); );
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(()) Ok(())
} }
@ -188,8 +194,15 @@ pub enum FontFamily {
SansSerif, SansSerif,
/// A family in which (almost) all glyphs are of equal width. /// A family in which (almost) all glyphs are of equal width.
Monospace, Monospace,
/// A specific family with a name. /// A specific font family like "Arial".
Named(String), 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 { 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! { dynamic! {
FontFamily: "font family", FontFamily: "font family",
Value::Str(string) => Self::Named(string.to_lowercase().into()), Value::Str(string) => Self::named(&string),
} }
castable! { castable! {
Vec<FontFamily>, Vec<FontFamily>,
Expected: "string, generic family or array thereof", 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) => { Value::Array(values) => {
values.into_iter().filter_map(|v| v.cast().ok()).collect() values.into_iter().filter_map(|v| v.cast().ok()).collect()
}, },
@ -219,13 +254,13 @@ castable! {
} }
castable! { castable! {
Vec<String>, Vec<NamedFamily>,
Expected: "string or array of strings", 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 Value::Array(values) => values
.into_iter() .into_iter()
.filter_map(|v| v.cast().ok()) .filter_map(|v| v.cast().ok())
.map(|string: EcoString| string.to_lowercase().into()) .map(|string: EcoString| NamedFamily::new(&string))
.collect(), .collect(),
} }
@ -243,7 +278,10 @@ castable! {
castable! { castable! {
FontWeight, FontWeight,
Expected: "integer or string", 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() { Value::Str(string) => match string.as_str() {
"thin" => Self::THIN, "thin" => Self::THIN,
"extralight" => Self::EXTRALIGHT, "extralight" => Self::EXTRALIGHT,
@ -681,7 +719,7 @@ fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone {
head.iter() head.iter()
.chain(core) .chain(core)
.map(String::as_str) .map(|named| named.as_str())
.chain(tail.iter().copied()) .chain(tail.iter().copied())
} }
@ -770,7 +808,7 @@ pub struct ShapedText<'a> {
/// The text direction. /// The text direction.
pub dir: Dir, pub dir: Dir,
/// The text's style properties. /// The text's style properties.
// TODO(set): Go back to reference. // TODO(style): Go back to reference.
pub styles: Styles, pub styles: Styles,
/// The font size. /// The font size.
pub size: Size, pub size: Size,

View File

@ -149,12 +149,12 @@ impl SourceFile {
Self::new(SourceId(0), Path::new(""), src.into()) 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> { pub fn root(&self) -> &Rc<GreenNode> {
&self.root &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> { pub fn ast(&self) -> TypResult<Markup> {
let red = RedNode::from_root(self.root.clone(), self.id); let red = RedNode::from_root(self.root.clone(), self.id);
let errors = red.errors(); let errors = red.errors();

View File

@ -1,4 +1,6 @@
//! A typed layer over the red-green tree. //! A typed layer over the red-green tree.
//!
//! The AST is rooted in the [`Markup`] node.
use std::ops::Deref; use std::ops::Deref;
@ -283,6 +285,7 @@ impl Expr {
Self::Ident(_) Self::Ident(_)
| Self::Call(_) | Self::Call(_)
| Self::Let(_) | Self::Let(_)
| Self::Set(_)
| Self::If(_) | Self::If(_)
| Self::While(_) | Self::While(_)
| Self::For(_) | Self::For(_)

View File

@ -187,7 +187,7 @@ impl From<GreenData> for Green {
impl Debug for GreenData { impl Debug for GreenData {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{:?}: {}", self.kind, self.len()) write!(f, "{:?}: {}", self.kind, self.len)
} }
} }

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

BIN
tests/ref/text/em.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -18,7 +18,17 @@ Add #h(10pt) #h(10pt) up
| #h(1fr) | #h(2fr) | #h(1fr) | | #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 A[#set par(align: right);#h(1cm)]B
[#set page(height: 20pt);#v(1cm)] [#set page(height: 20pt);#v(1cm)]

View File

@ -39,3 +39,12 @@ is not.
= A { = A {
"B" "B"
} }
---
// Test styling.
= Heading
#set heading(family: "Roboto", fill: eastern)
===== Heading 🌍
#heading(level: 5)[Heading]

View File

@ -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

View File

@ -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)

View File

@ -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

17
tests/typ/text/em.typ Normal file
View File

@ -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

View File

@ -15,6 +15,16 @@ To the right! Where the sunlight peeks behind the mountain.
Third 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 // Test that paragraph break due to incompatibility respects
// spacing defined by the two adjacent paragraphs. // spacing defined by the two adjacent paragraphs.

View File

@ -1,32 +1,21 @@
// Test whitespace handling. // Test whitespace handling.
--- ---
// Spacing around let. // Spacing around code constructs.
A#let x = 1;B #test(x, 1) \ A#let x = 1;B #test(x, 1) \
A #let x = 2;B #test(x, 2) \ C #let x = 2;D #test(x, 2) \
A#let x = 3; B #test(x, 3) 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. // Test spacing with comments.
A#if true [B]C \ A/**/B/**/C \
A#if true [B] C \ A /**/ B/**/C \
A #if true{"B"}C \ A /**/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 that a run consisting only of whitespace isn't trimmed. // 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]. 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 A{"\n"} B
--- ---

View File

@ -17,9 +17,9 @@ use typst::font::Face;
use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Size, Transform}; use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Size, Transform};
use typst::image::{Image, RasterImage, Svg}; use typst::image::{Image, RasterImage, Svg};
use typst::layout::layout;
#[cfg(feature = "layout-cache")] #[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::loading::FsLoader;
use typst::parse::Scanner; use typst::parse::Scanner;
use typst::source::SourceFile; use typst::source::SourceFile;
@ -254,16 +254,17 @@ fn test_part(
let compare_ref = local_compare_ref.unwrap_or(compare_ref); let compare_ref = local_compare_ref.unwrap_or(compare_ref);
let mut ok = true; let mut ok = true;
let (frames, mut errors) = match ctx.execute(id) { let (frames, mut errors) = match ctx.evaluate(id) {
Ok(document) => { Ok(module) => {
let tree = module.into_root();
if debug { if debug {
println!("{:#?}", document); println!("{:#?}", tree);
} }
let mut frames = layout(ctx, &document); let mut frames = tree.layout(ctx);
#[cfg(feature = "layout-cache")] #[cfg(feature = "layout-cache")]
(ok &= test_incremental(ctx, i, &document, &frames)); (ok &= test_incremental(ctx, i, &tree, &frames));
if !compare_ref { if !compare_ref {
frames.clear(); frames.clear();
@ -311,7 +312,7 @@ fn test_part(
fn test_incremental( fn test_incremental(
ctx: &mut Context, ctx: &mut Context,
i: usize, i: usize,
document: &DocumentNode, tree: &RootNode,
frames: &[Rc<Frame>], frames: &[Rc<Frame>],
) -> bool { ) -> bool {
let mut ok = true; let mut ok = true;
@ -326,7 +327,7 @@ fn test_incremental(
ctx.layouts.turnaround(); ctx.layouts.turnaround();
let cached = silenced(|| layout(ctx, document)); let cached = silenced(|| tree.layout(ctx));
let misses = ctx let misses = ctx
.layouts .layouts
.entries() .entries()