summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin <mhaug@live.de>2021-12-22 20:37:34 +0100
committerGitHub <noreply@github.com>2021-12-22 20:37:34 +0100
commitf6c7a8292dc1ab0560408fca9d74505e9d7cf13a (patch)
treebadd3076f6146cec34c55764600df5124c408521
parent738ff7e1f573bef678932b313be9969a17af8d22 (diff)
parent438255519e88bb790480306b9a9b452aaf054519 (diff)
Merge pull request #51 from typst/set-rules
Set rules
-rw-r--r--Cargo.toml6
-rw-r--r--benches/bench.typ2
-rw-r--r--benches/oneshot.rs17
-rw-r--r--macros/Cargo.toml13
-rw-r--r--macros/src/lib.rs171
-rw-r--r--src/eval/class.rs139
-rw-r--r--src/eval/mod.rs194
-rw-r--r--src/eval/node.rs453
-rw-r--r--src/eval/ops.rs18
-rw-r--r--src/eval/scope.rs30
-rw-r--r--src/eval/styles.rs292
-rw-r--r--src/eval/template.rs547
-rw-r--r--src/eval/value.rs61
-rw-r--r--src/eval/walk.rs141
-rw-r--r--src/font.rs8
-rw-r--r--src/frame.rs14
-rw-r--r--src/geom/em.rs2
-rw-r--r--src/geom/linear.rs15
-rw-r--r--src/layout/mod.rs186
-rw-r--r--src/lib.rs78
-rw-r--r--src/library/align.rs44
-rw-r--r--src/library/deco.rs132
-rw-r--r--src/library/document.rs16
-rw-r--r--src/library/flow.rs118
-rw-r--r--src/library/grid.rs16
-rw-r--r--src/library/heading.rs63
-rw-r--r--src/library/image.rs12
-rw-r--r--src/library/link.rs28
-rw-r--r--src/library/list.rs102
-rw-r--r--src/library/mod.rs69
-rw-r--r--src/library/pad.rs6
-rw-r--r--src/library/page.rs450
-rw-r--r--src/library/par.rs262
-rw-r--r--src/library/placed.rs26
-rw-r--r--src/library/shape.rs32
-rw-r--r--src/library/sized.rs14
-rw-r--r--src/library/spacing.rs36
-rw-r--r--src/library/stack.rs90
-rw-r--r--src/library/text.rs1101
-rw-r--r--src/library/transform.rs8
-rw-r--r--src/parse/mod.rs65
-rw-r--r--src/parse/parser.rs3
-rw-r--r--src/parse/tokens.rs1
-rw-r--r--src/source.rs7
-rw-r--r--src/style/mod.rs419
-rw-r--r--src/style/paper.rs233
-rw-r--r--src/syntax/ast.rs26
-rw-r--r--src/syntax/highlight.rs29
-rw-r--r--src/syntax/mod.rs101
-rw-r--r--src/syntax/pretty.rs12
-rw-r--r--src/util/mod.rs12
-rw-r--r--tests/README.md35
-rw-r--r--tests/ref/elements/circle.pngbin18873 -> 0 bytes
-rw-r--r--tests/ref/elements/ellipse.pngbin9597 -> 0 bytes
-rw-r--r--tests/ref/layout/box-block.png (renamed from tests/ref/layout/containers.png)bin5194 -> 5194 bytes
-rw-r--r--tests/ref/layout/image.png (renamed from tests/ref/elements/image.png)bin186196 -> 186196 bytes
-rw-r--r--tests/ref/layout/pagebreak.pngbin3145 -> 3788 bytes
-rw-r--r--tests/ref/layout/place-background.png (renamed from tests/ref/layout/background.png)bin80160 -> 80160 bytes
-rw-r--r--tests/ref/layout/place.png (renamed from tests/ref/layout/placed.png)bin31901 -> 33917 bytes
-rw-r--r--tests/ref/layout/shape-aspect.png (renamed from tests/ref/layout/aspect.png)bin3601 -> 3601 bytes
-rw-r--r--tests/ref/layout/shape-circle.pngbin0 -> 18861 bytes
-rw-r--r--tests/ref/layout/shape-ellipse.pngbin0 -> 9455 bytes
-rw-r--r--tests/ref/layout/shape-fill-stroke.png (renamed from tests/ref/elements/fill-stroke.png)bin1942 -> 1942 bytes
-rw-r--r--tests/ref/layout/shape-rect.png (renamed from tests/ref/elements/rect.png)bin2914 -> 2914 bytes
-rw-r--r--tests/ref/layout/shape-square.png (renamed from tests/ref/elements/square.png)bin6585 -> 6585 bytes
-rw-r--r--tests/ref/layout/spacing.pngbin1827 -> 2927 bytes
-rw-r--r--tests/ref/layout/transform.pngbin54558 -> 54712 bytes
-rw-r--r--tests/ref/markup/heading.pngbin6406 -> 9141 bytes
-rw-r--r--tests/ref/style/set-block.pngbin0 -> 812 bytes
-rw-r--r--tests/ref/style/set-site.pngbin0 -> 4252 bytes
-rw-r--r--tests/ref/style/set-toggle.pngbin0 -> 968 bytes
-rw-r--r--tests/ref/text/em.pngbin0 -> 878 bytes
-rw-r--r--tests/ref/text/links.pngbin10708 -> 22561 bytes
-rw-r--r--tests/ref/text/par.pngbin5732 -> 8169 bytes
-rw-r--r--tests/ref/text/whitespace.pngbin5322 -> 4674 bytes
-rw-r--r--tests/typ/code/call.typ11
-rw-r--r--tests/typ/code/include.typ2
-rw-r--r--tests/typ/code/ops-invalid.typ2
-rw-r--r--tests/typ/code/ops.typ10
-rw-r--r--tests/typ/code/spread.typ10
-rw-r--r--tests/typ/coma.typ5
-rw-r--r--tests/typ/layout/align.typ2
-rw-r--r--tests/typ/layout/box-block.typ (renamed from tests/typ/layout/containers.typ)2
-rw-r--r--tests/typ/layout/grid-1.typ4
-rw-r--r--tests/typ/layout/grid-2.typ2
-rw-r--r--tests/typ/layout/grid-3.typ10
-rw-r--r--tests/typ/layout/grid-4.typ2
-rw-r--r--tests/typ/layout/grid-5.typ4
-rw-r--r--tests/typ/layout/image.typ (renamed from tests/typ/elements/image.typ)6
-rw-r--r--tests/typ/layout/pad.typ2
-rw-r--r--tests/typ/layout/page.typ28
-rw-r--r--tests/typ/layout/pagebreak.typ15
-rw-r--r--tests/typ/layout/place-background.typ (renamed from tests/typ/layout/background.typ)4
-rw-r--r--tests/typ/layout/place.typ (renamed from tests/typ/layout/placed.typ)15
-rw-r--r--tests/typ/layout/shape-aspect.typ (renamed from tests/typ/layout/aspect.typ)10
-rw-r--r--tests/typ/layout/shape-circle.typ (renamed from tests/typ/elements/circle.typ)4
-rw-r--r--tests/typ/layout/shape-ellipse.typ (renamed from tests/typ/elements/ellipse.typ)2
-rw-r--r--tests/typ/layout/shape-fill-stroke.typ (renamed from tests/typ/elements/fill-stroke.typ)0
-rw-r--r--tests/typ/layout/shape-rect.typ (renamed from tests/typ/elements/rect.typ)2
-rw-r--r--tests/typ/layout/shape-square.typ (renamed from tests/typ/elements/square.typ)6
-rw-r--r--tests/typ/layout/spacing.typ17
-rw-r--r--tests/typ/layout/stack-1.typ12
-rw-r--r--tests/typ/layout/stack-2.typ6
-rw-r--r--tests/typ/layout/transform.typ6
-rw-r--r--tests/typ/markup/heading.typ9
-rw-r--r--tests/typ/style/set-block.typ10
-rw-r--r--tests/typ/style/set-site.typ30
-rw-r--r--tests/typ/style/set-toggle.typ10
-rw-r--r--tests/typ/text/baseline.typ2
-rw-r--r--tests/typ/text/basic.typ2
-rw-r--r--tests/typ/text/bidi.typ42
-rw-r--r--tests/typ/text/chinese.typ2
-rw-r--r--tests/typ/text/decorations.typ2
-rw-r--r--tests/typ/text/em.typ17
-rw-r--r--tests/typ/text/features.typ71
-rw-r--r--tests/typ/text/font.typ70
-rw-r--r--tests/typ/text/links.typ11
-rw-r--r--tests/typ/text/par.typ39
-rw-r--r--tests/typ/text/shaping.typ10
-rw-r--r--tests/typ/text/tracking.typ6
-rw-r--r--tests/typ/text/whitespace.typ45
-rw-r--r--tests/typeset.rs106
122 files changed, 3797 insertions, 2843 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 2778fd43..7667942e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,17 +19,19 @@ debug = 0
opt-level = 2
[dependencies]
-fxhash = "0.2.1"
+fxhash = "0.2"
image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
itertools = "0.10"
miniz_oxide = "0.4"
+once_cell = "1"
pdf-writer = "0.4"
rustybuzz = "0.4"
serde = { version = "1", features = ["derive", "rc"] }
svg2pdf = { version = "0.1", default-features = false, features = ["text", "png", "jpeg"] }
ttf-parser = "0.12"
+typst-macros = { path = "./macros" }
unicode-bidi = "0.3.5"
-unicode-segmentation = "1.8"
+unicode-segmentation = "1"
unicode-xid = "0.2"
usvg = { version = "0.19", default-features = false, features = ["text"] }
xi-unicode = "0.3"
diff --git a/benches/bench.typ b/benches/bench.typ
index f290844b..75c97d02 100644
--- a/benches/bench.typ
+++ b/benches/bench.typ
@@ -1,5 +1,5 @@
// Configuration with `page` and `font` functions.
-#page(width: 450pt, margins: 1cm)
+#set page(width: 450pt, margins: 1cm)
// There are variables and they can take normal values like strings, ...
#let city = "Berlin"
diff --git a/benches/oneshot.rs b/benches/oneshot.rs
index 6bfb923b..d3e2ff8e 100644
--- a/benches/oneshot.rs
+++ b/benches/oneshot.rs
@@ -2,8 +2,6 @@ use std::path::Path;
use iai::{black_box, main, Iai};
-use typst::eval::eval;
-use typst::layout::layout;
use typst::loading::MemLoader;
use typst::parse::{parse, Scanner, TokenMode, Tokens};
use typst::source::SourceId;
@@ -53,20 +51,14 @@ fn bench_parse(iai: &mut Iai) {
fn bench_eval(iai: &mut Iai) {
let (mut ctx, id) = context();
- let ast = ctx.sources.get(id).ast().unwrap();
- iai.run(|| eval(&mut ctx, id, &ast).unwrap());
-}
-
-fn bench_to_tree(iai: &mut Iai) {
- let (mut ctx, id) = context();
- let module = ctx.evaluate(id).unwrap();
- iai.run(|| module.template.to_document(ctx.style()));
+ iai.run(|| ctx.evaluate(id).unwrap());
}
fn bench_layout(iai: &mut Iai) {
let (mut ctx, id) = context();
- let tree = ctx.execute(id).unwrap();
- iai.run(|| layout(&mut ctx, &tree));
+ let module = ctx.evaluate(id).unwrap();
+ let tree = module.into_root();
+ iai.run(|| tree.layout(&mut ctx));
}
main!(
@@ -75,6 +67,5 @@ main!(
bench_tokenize,
bench_parse,
bench_eval,
- bench_to_tree,
bench_layout
);
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
new file mode 100644
index 00000000..6a7f5522
--- /dev/null
+++ b/macros/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "typst-macros"
+version = "0.1.0"
+authors = ["The Typst Project Developers"]
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1"
+quote = "1"
+syn = { version = "1", features = ["full"] }
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
new file mode 100644
index 00000000..3b1d0548
--- /dev/null
+++ b/macros/src/lib.rs
@@ -0,0 +1,171 @@
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse_quote;
+use syn::spanned::Spanned;
+use syn::{Error, Result};
+
+/// Generate node properties.
+#[proc_macro_attribute]
+pub fn properties(_: TokenStream, item: TokenStream) -> TokenStream {
+ let impl_block = syn::parse_macro_input!(item as syn::ItemImpl);
+ expand(impl_block).unwrap_or_else(|err| err.to_compile_error()).into()
+}
+
+/// Expand a property impl block for a node.
+fn expand(mut impl_block: syn::ItemImpl) -> Result<TokenStream2> {
+ // Split the node type into name and generic type arguments.
+ let (self_name, self_args) = parse_self(&*impl_block.self_ty)?;
+
+ // Rewrite the const items from values to keys.
+ let mut style_ids = vec![];
+ let mut modules = vec![];
+ for item in &mut impl_block.items {
+ if let syn::ImplItem::Const(item) = item {
+ let (style_id, module) = process_const(item, &self_name, &self_args)?;
+ style_ids.push(style_id);
+ modules.push(module);
+ }
+ }
+
+ // Here, we use the collected `style_ids` to provide a function that checks
+ // whether a property belongs to the node.
+ impl_block.items.insert(0, parse_quote! {
+ /// Check whether the property with the given type id belongs to `Self`.
+ pub fn has_property(id: StyleId) -> bool {
+ [#(#style_ids),*].contains(&id)
+ }
+ });
+
+ // Put everything into a module with a hopefully unique type to isolate
+ // it from the outside.
+ let module = quote::format_ident!("{}_types", self_name);
+ Ok(quote! {
+ #[allow(non_snake_case)]
+ mod #module {
+ use std::marker::PhantomData;
+ use once_cell::sync::Lazy;
+ use crate::eval::{Property, StyleId};
+ use super::*;
+
+ #impl_block
+ #(#modules)*
+ }
+ })
+}
+
+/// Parse the name and generic type arguments of the node type.
+fn parse_self(self_ty: &syn::Type) -> Result<(String, Vec<&syn::Type>)> {
+ // Extract the node type for which we want to generate properties.
+ let path = match self_ty {
+ syn::Type::Path(path) => path,
+ ty => return Err(Error::new(ty.span(), "must be a path type")),
+ };
+
+ // Split up the type into its name and its generic type arguments.
+ let last = path.path.segments.last().unwrap();
+ let self_name = last.ident.to_string();
+ let self_args = match &last.arguments {
+ syn::PathArguments::AngleBracketed(args) => args
+ .args
+ .iter()
+ .filter_map(|arg| match arg {
+ syn::GenericArgument::Type(ty) => Some(ty),
+ _ => None,
+ })
+ .collect(),
+ _ => vec![],
+ };
+
+ Ok((self_name, self_args))
+}
+
+/// Process a single const item.
+fn process_const(
+ item: &mut syn::ImplItemConst,
+ self_name: &str,
+ self_args: &[&syn::Type],
+) -> Result<(syn::Expr, syn::ItemMod)> {
+ // The type of the property's value is what the user of our macro wrote
+ // as type of the const ...
+ let value_ty = &item.ty;
+
+ // ... but the real type of the const becomes Key<#key_param>.
+ let key_param = if self_args.is_empty() {
+ quote! { #value_ty }
+ } else {
+ quote! { (#value_ty, #(#self_args),*) }
+ };
+
+ // The display name, e.g. `TextNode::STRONG`.
+ let name = format!("{}::{}", self_name, &item.ident);
+
+ // The default value of the property is what the user wrote as
+ // initialization value of the const.
+ let default = &item.expr;
+
+ // Look for a folding function like `#[fold(u64::add)]`.
+ let mut combinator = None;
+ for attr in &item.attrs {
+ if attr.path.is_ident("fold") {
+ let fold: syn::Expr = attr.parse_args()?;
+ combinator = Some(quote! {
+ fn combine(inner: Self::Value, outer: Self::Value) -> Self::Value {
+ let f: fn(Self::Value, Self::Value) -> Self::Value = #fold;
+ f(inner, outer)
+ }
+ });
+ }
+ }
+
+ // The implementation of the `Property` trait.
+ let property_impl = quote! {
+ impl<T: 'static> Property for Key<T> {
+ type Value = #value_ty;
+
+ const NAME: &'static str = #name;
+
+ fn default() -> Self::Value {
+ #default
+ }
+
+ fn default_ref() -> &'static Self::Value {
+ static LAZY: Lazy<#value_ty> = Lazy::new(|| #default);
+ &*LAZY
+ }
+
+ #combinator
+ }
+ };
+
+ // The module that will contain the `Key` type.
+ let module_name = &item.ident;
+
+ // Generate the style id and module code.
+ let style_id = parse_quote! { StyleId::of::<#module_name::Key<#key_param>>() };
+ let module = parse_quote! {
+ #[allow(non_snake_case)]
+ mod #module_name {
+ use super::*;
+
+ pub struct Key<T>(pub PhantomData<T>);
+ impl<T> Copy for Key<T> {}
+ impl<T> Clone for Key<T> {
+ fn clone(&self) -> Self {
+ *self
+ }
+ }
+
+ #property_impl
+ }
+ };
+
+ // Replace type and initializer expression with the `Key`.
+ item.attrs.retain(|attr| !attr.path.is_ident("fold"));
+ item.ty = parse_quote! { #module_name::Key<#key_param> };
+ item.expr = parse_quote! { #module_name::Key(PhantomData) };
+
+ Ok((style_id, module))
+}
diff --git a/src/eval/class.rs b/src/eval/class.rs
new file mode 100644
index 00000000..c4393b8a
--- /dev/null
+++ b/src/eval/class.rs
@@ -0,0 +1,139 @@
+use std::fmt::{self, Debug, Formatter, Write};
+use std::marker::PhantomData;
+use std::rc::Rc;
+
+use super::{Args, EvalContext, Node, Styles};
+use crate::diag::TypResult;
+use crate::util::EcoString;
+
+/// A class of [nodes](Node).
+///
+/// You can [construct] an instance of a class in Typst code by invoking the
+/// class as a callable. This always produces some node, but not necessarily one
+/// of fixed type. For example, the `text` constructor does not actually create
+/// a [`TextNode`]. Instead it applies styling to whatever node you pass in and
+/// returns it structurally unchanged.
+///
+/// The arguments you can pass to a class constructor fall into two categories:
+/// Data that is inherent to the instance (e.g. the text of a heading) and style
+/// properties (e.g. the fill color of a heading). As the latter are often
+/// shared by many instances throughout a document, they can also be
+/// conveniently configured through class's [`set`] rule. Then, they apply to
+/// all nodes that are instantiated into the template where the `set` was
+/// executed.
+///
+/// ```typst
+/// This is normal.
+/// [
+/// #set text(weight: "bold")
+/// #set heading(fill: blue)
+/// = A blue & bold heading
+/// ]
+/// Normal again.
+/// ```
+///
+/// [construct]: Self::construct
+/// [`TextNode`]: crate::library::TextNode
+/// [`set`]: Self::set
+#[derive(Clone)]
+pub struct Class(Rc<Inner<dyn Bounds>>);
+
+/// The unsized structure behind the [`Rc`].
+struct Inner<T: ?Sized> {
+ name: EcoString,
+ shim: T,
+}
+
+impl Class {
+ /// Create a new class.
+ pub fn new<T>(name: EcoString) -> Self
+ where
+ T: Construct + Set + 'static,
+ {
+ // By specializing the shim to `T`, its vtable will contain T's
+ // `Construct` and `Set` impls (through the `Bounds` trait), enabling us
+ // to use them in the class's methods.
+ Self(Rc::new(Inner { name, shim: Shim::<T>(PhantomData) }))
+ }
+
+ /// The name of the class.
+ pub fn name(&self) -> &EcoString {
+ &self.0.name
+ }
+
+ /// Construct an instance of the class.
+ ///
+ /// This parses both property and data arguments (in this order) and styles
+ /// the node constructed from the data with the style properties.
+ pub fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let mut styles = Styles::new();
+ self.set(args, &mut styles)?;
+ let node = self.0.shim.construct(ctx, args)?;
+ Ok(node.styled(styles))
+ }
+
+ /// Execute the class's set rule.
+ ///
+ /// This parses property arguments and writes the resulting styles into the
+ /// given style map. There are no further side effects.
+ pub fn set(&self, args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ self.0.shim.set(args, styles)
+ }
+}
+
+impl Debug for Class {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("<class ")?;
+ f.write_str(&self.0.name)?;
+ f.write_char('>')
+ }
+}
+
+impl PartialEq for Class {
+ fn eq(&self, other: &Self) -> bool {
+ // We cast to thin pointers for comparison because we don't want to
+ // compare vtables (there can be duplicate vtables across codegen units).
+ std::ptr::eq(
+ Rc::as_ptr(&self.0) as *const (),
+ Rc::as_ptr(&other.0) as *const (),
+ )
+ }
+}
+
+/// Construct an instance of a class.
+pub trait Construct {
+ /// Construct an instance of this class from the arguments.
+ ///
+ /// This is passed only the arguments that remain after execution of the
+ /// class's set rule.
+ fn construct(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node>;
+}
+
+/// Set style properties of a class.
+pub trait Set {
+ /// Parse the arguments and insert style properties of this class into the
+ /// given style map.
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()>;
+}
+
+/// Rewires the operations available on a class in an object-safe way. This is
+/// only implemented by the zero-sized `Shim` struct.
+trait Bounds {
+ fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node>;
+ fn set(&self, args: &mut Args, styles: &mut Styles) -> TypResult<()>;
+}
+
+struct Shim<T>(PhantomData<T>);
+
+impl<T> Bounds for Shim<T>
+where
+ T: Construct + Set,
+{
+ fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ T::construct(ctx, args)
+ }
+
+ fn set(&self, args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ T::set(args, styles)
+ }
+}
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index a0c31e98..17cc46ef 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -6,21 +6,24 @@ mod array;
mod dict;
#[macro_use]
mod value;
+#[macro_use]
+mod styles;
mod capture;
+mod class;
mod function;
+mod node;
mod ops;
mod scope;
-mod template;
-mod walk;
pub use array::*;
pub use capture::*;
+pub use class::*;
pub use dict::*;
pub use function::*;
+pub use node::*;
pub use scope::*;
-pub use template::*;
+pub use styles::*;
pub use value::*;
-pub use walk::*;
use std::cell::RefMut;
use std::collections::HashMap;
@@ -33,6 +36,8 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult};
use crate::geom::{Angle, Fractional, Length, Relative};
use crate::image::ImageStore;
+use crate::layout::RootNode;
+use crate::library::{self, TextNode};
use crate::loading::Loader;
use crate::source::{SourceId, SourceStore};
use crate::syntax::ast::*;
@@ -40,20 +45,30 @@ 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 template = markup.eval(&mut ctx)?;
- Ok(Module { scope: ctx.scopes.top, template })
-}
-
-/// An evaluated module, ready for importing or instantiation.
+/// 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,
- /// The template defined by this module.
- pub template: Template,
+ /// The module's layoutable contents.
+ 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.
@@ -70,8 +85,8 @@ pub struct EvalContext<'a> {
pub modules: HashMap<SourceId, Module>,
/// The active scopes.
pub scopes: Scopes<'a>,
- /// The currently built template.
- pub template: Template,
+ /// The active styles.
+ pub styles: Styles,
}
impl<'a> EvalContext<'a> {
@@ -84,7 +99,7 @@ impl<'a> EvalContext<'a> {
route: vec![source],
modules: HashMap::new(),
scopes: Scopes::new(Some(&ctx.std)),
- template: Template::new(),
+ styles: Styles::new(),
}
}
@@ -115,18 +130,20 @@ impl<'a> EvalContext<'a> {
// Prepare the new context.
let new_scopes = Scopes::new(self.scopes.base);
- let old_scopes = mem::replace(&mut self.scopes, new_scopes);
+ let prev_scopes = mem::replace(&mut self.scopes, new_scopes);
+ let prev_styles = mem::take(&mut self.styles);
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, old_scopes);
+ let new_scopes = mem::replace(&mut self.scopes, prev_scopes);
+ self.styles = prev_styles;
self.route.pop().unwrap();
// Save the evaluated module.
- let module = Module { scope: new_scopes.top, template };
+ let module = Module { scope: new_scopes.top, node };
self.modules.insert(id, module);
Ok(id)
@@ -145,29 +162,108 @@ impl<'a> EvalContext<'a> {
}
}
-/// Evaluate an expression.
-pub trait Eval {
- /// The output of evaluating the expression.
- type Output;
+impl Eval for Markup {
+ type Output = Node;
- /// Evaluate the expression to the output value.
- fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output>;
+ fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
+ let prev = mem::take(&mut ctx.styles);
+ let nodes = self.nodes();
+ let upper = nodes.size_hint().1.unwrap_or_default();
+ let mut seq = Vec::with_capacity(upper);
+ for piece in nodes {
+ seq.push((piece.eval(ctx)?, ctx.styles.clone()));
+ }
+ ctx.styles = prev;
+ Ok(Node::Sequence(seq))
+ }
}
-impl Eval for Markup {
- type Output = Template;
+impl Eval for MarkupNode {
+ type Output = Node;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
- Ok({
- let prev = mem::take(&mut ctx.template);
- ctx.template.save();
- self.walk(ctx)?;
- ctx.template.restore();
- mem::replace(&mut ctx.template, prev)
+ Ok(match self {
+ Self::Space => Node::Space,
+ Self::Linebreak => Node::Linebreak,
+ Self::Parbreak => Node::Parbreak,
+ Self::Strong => {
+ ctx.styles.toggle(TextNode::STRONG);
+ Node::new()
+ }
+ Self::Emph => {
+ ctx.styles.toggle(TextNode::EMPH);
+ Node::new()
+ }
+ Self::Text(text) => Node::Text(text.clone()),
+ Self::Raw(raw) => raw.eval(ctx)?,
+ Self::Math(math) => math.eval(ctx)?,
+ Self::Heading(heading) => heading.eval(ctx)?,
+ Self::List(list) => list.eval(ctx)?,
+ Self::Enum(enum_) => enum_.eval(ctx)?,
+ Self::Expr(expr) => expr.eval(ctx)?.show(),
+ })
+ }
+}
+
+impl Eval for RawNode {
+ type Output = Node;
+
+ fn eval(&self, _: &mut EvalContext) -> TypResult<Self::Output> {
+ let text = Node::Text(self.text.clone()).monospaced();
+ Ok(if self.block {
+ Node::Block(text.into_block())
+ } else {
+ text
})
}
}
+impl Eval for MathNode {
+ type Output = Node;
+
+ fn eval(&self, _: &mut EvalContext) -> TypResult<Self::Output> {
+ let text = Node::Text(self.formula.trim().into()).monospaced();
+ Ok(if self.display {
+ Node::Block(text.into_block())
+ } else {
+ text
+ })
+ }
+}
+
+impl Eval for HeadingNode {
+ type Output = Node;
+
+ fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
+ Ok(Node::block(library::HeadingNode {
+ child: self.body().eval(ctx)?.into_block(),
+ level: self.level(),
+ }))
+ }
+}
+
+impl Eval for ListNode {
+ type Output = Node;
+
+ fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
+ Ok(Node::block(library::ListNode {
+ child: self.body().eval(ctx)?.into_block(),
+ labelling: library::Unordered,
+ }))
+ }
+}
+
+impl Eval for EnumNode {
+ type Output = Node;
+
+ fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
+ Ok(Node::block(library::ListNode {
+ child: self.body().eval(ctx)?.into_block(),
+ labelling: library::Ordered(self.number()),
+ }))
+ }
+}
+
impl Eval for Expr {
type Output = Value;
@@ -177,7 +273,7 @@ impl Eval for Expr {
Self::Ident(v) => v.eval(ctx),
Self::Array(v) => v.eval(ctx).map(Value::Array),
Self::Dict(v) => v.eval(ctx).map(Value::Dict),
- Self::Template(v) => v.eval(ctx).map(Value::Template),
+ Self::Template(v) => v.eval(ctx).map(Value::Node),
Self::Group(v) => v.eval(ctx),
Self::Block(v) => v.eval(ctx),
Self::Call(v) => v.eval(ctx),
@@ -186,6 +282,7 @@ impl Eval for Expr {
Self::Unary(v) => v.eval(ctx),
Self::Binary(v) => v.eval(ctx),
Self::Let(v) => v.eval(ctx),
+ Self::Set(v) => v.eval(ctx),
Self::If(v) => v.eval(ctx),
Self::While(v) => v.eval(ctx),
Self::For(v) => v.eval(ctx),
@@ -244,7 +341,7 @@ impl Eval for DictExpr {
}
impl Eval for TemplateExpr {
- type Output = Template;
+ type Output = Node;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
self.body().eval(ctx)
@@ -372,9 +469,15 @@ impl Eval for CallExpr {
Ok(value)
}
+ Value::Class(class) => {
+ let node = class.construct(ctx, &mut args)?;
+ args.finish()?;
+ Ok(Value::Node(node))
+ }
+
v => bail!(
self.callee().span(),
- "expected function or collection, found {}",
+ "expected callable or collection, found {}",
v.type_name(),
),
}
@@ -541,6 +644,19 @@ impl Eval for LetExpr {
}
}
+impl Eval for SetExpr {
+ type Output = Value;
+
+ fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
+ let class = self.class();
+ let class = class.eval(ctx)?.cast::<Class>().at(class.span())?;
+ let mut args = self.args().eval(ctx)?;
+ class.set(&mut args, &mut ctx.styles)?;
+ args.finish()?;
+ Ok(Value::None)
+ }
+}
+
impl Eval for IfExpr {
type Output = Value;
@@ -665,7 +781,7 @@ impl Eval for IncludeExpr {
let resolved = path.eval(ctx)?.cast::<EcoString>().at(path.span())?;
let file = ctx.import(&resolved, path.span())?;
let module = &ctx.modules[&file];
- Ok(Value::Template(module.template.clone()))
+ Ok(Value::Node(module.node.clone()))
}
}
diff --git a/src/eval/node.rs b/src/eval/node.rs
new file mode 100644
index 00000000..34a4f275
--- /dev/null
+++ b/src/eval/node.rs
@@ -0,0 +1,453 @@
+use std::convert::TryFrom;
+use std::fmt::Debug;
+use std::hash::Hash;
+use std::iter::Sum;
+use std::mem;
+use std::ops::{Add, AddAssign};
+
+use super::Styles;
+use crate::diag::StrResult;
+use crate::geom::SpecAxis;
+use crate::layout::{Layout, PackedNode, RootNode};
+use crate::library::{
+ 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](PackedNode) or
+/// [root node](RootNode).
+///
+/// When you write `[Hi] + [you]` in Typst, this type's [`Add`] implementation
+/// is invoked. There, multiple nodes are combined into a single
+/// [`Sequence`](Self::Sequence) node.
+#[derive(Debug, PartialEq, Clone, Hash)]
+pub enum Node {
+ /// A word space.
+ Space,
+ /// A line break.
+ Linebreak,
+ /// A paragraph break.
+ Parbreak,
+ /// A page break.
+ Pagebreak,
+ /// Plain text.
+ Text(EcoString),
+ /// Spacing.
+ Spacing(SpecAxis, SpacingKind),
+ /// An inline node.
+ Inline(PackedNode),
+ /// A block node.
+ Block(PackedNode),
+ /// A page node.
+ Page(PageNode),
+ /// Multiple nodes with attached styles.
+ ///
+ /// For example, the Typst template `[Hi *you!*]` would result in the
+ /// sequence:
+ /// ```ignore
+ /// Sequence([
+ /// (Text("Hi"), {}),
+ /// (Space, {}),
+ /// (Text("you!"), { TextNode::STRONG: true }),
+ /// ])
+ /// ```
+ /// A sequence may contain nested sequences (meaning this variant
+ /// effectively allows nodes to form trees). All nested sequences can
+ /// equivalently be represented as a single flat sequence, but allowing
+ /// nesting doesn't hurt since we can just recurse into the nested sequences
+ /// during packing. Also, in theory, this allows better complexity when
+ /// adding (large) sequence nodes (just like for a text rope).
+ Sequence(Vec<(Self, Styles)>),
+}
+
+impl Node {
+ /// Create an empty node.
+ pub fn new() -> Self {
+ Self::Sequence(vec![])
+ }
+
+ /// Create an inline-level node.
+ pub fn inline<T>(node: T) -> Self
+ where
+ T: Layout + Debug + Hash + 'static,
+ {
+ Self::Inline(node.pack())
+ }
+
+ /// Create a block-level node.
+ pub fn block<T>(node: T) -> Self
+ where
+ T: Layout + Debug + Hash + 'static,
+ {
+ Self::Block(node.pack())
+ }
+
+ /// Style this node.
+ pub fn styled(self, styles: Styles) -> Self {
+ match self {
+ Self::Inline(inline) => Self::Inline(inline.styled(styles)),
+ Self::Block(block) => Self::Block(block.styled(styles)),
+ Self::Page(page) => Self::Page(page.styled(styles)),
+ other => Self::Sequence(vec![(other, styles)]),
+ }
+ }
+
+ /// Style this node in monospace.
+ pub fn monospaced(self) -> Self {
+ self.styled(Styles::one(TextNode::MONOSPACE, true))
+ }
+
+ /// Lift to a type-erased block-level node.
+ pub fn into_block(self) -> PackedNode {
+ if let Node::Block(packed) = self {
+ packed
+ } else {
+ let mut packer = Packer::new(false);
+ packer.walk(self, Styles::new());
+ packer.into_block()
+ }
+ }
+
+ /// 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_root()
+ }
+
+ /// 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(style): Make more efficient.
+ Ok(Self::Sequence(vec![(self.clone(), Styles::new()); count]))
+ }
+}
+
+impl Default for Node {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Add for Node {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ // TODO(style): Make more efficient.
+ Self::Sequence(vec![(self, Styles::new()), (rhs, Styles::new())])
+ }
+}
+
+impl AddAssign for Node {
+ fn add_assign(&mut self, rhs: Self) {
+ *self = mem::take(self) + rhs;
+ }
+}
+
+impl Sum for Node {
+ fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+ Self::Sequence(iter.map(|n| (n, Styles::new())).collect())
+ }
+}
+
+/// Packs a [`Node`] into a flow or root node.
+struct Packer {
+ /// Whether this packer produces a root node.
+ top: bool,
+ /// The accumulated page nodes.
+ pages: Vec<PageNode>,
+ /// The accumulated flow children.
+ flow: Builder<FlowChild>,
+ /// The accumulated paragraph children.
+ par: Builder<ParChild>,
+}
+
+impl Packer {
+ /// Start a new node-packing session.
+ fn new(top: bool) -> Self {
+ Self {
+ top,
+ pages: vec![],
+ flow: Builder::default(),
+ par: Builder::default(),
+ }
+ }
+
+ /// Finish up and return the resulting flow.
+ fn into_block(mut self) -> PackedNode {
+ self.parbreak(None);
+ FlowNode(self.flow.children).pack()
+ }
+
+ /// Finish up and return the resulting root node.
+ fn into_root(mut self) -> RootNode {
+ self.pagebreak();
+ RootNode(self.pages)
+ }
+
+ /// Consider a node with the given styles.
+ fn walk(&mut self, node: Node, styles: Styles) {
+ match node {
+ Node::Space => {
+ // A text space is "soft", meaning that it can be eaten up by
+ // adjacent line breaks or explicit spacings.
+ self.par.last.soft(ParChild::text(' ', styles));
+ }
+ Node::Linebreak => {
+ // A line break eats up surrounding text spaces.
+ self.par.last.hard();
+ self.push_inline(ParChild::text('\n', styles));
+ self.par.last.hard();
+ }
+ Node::Parbreak => {
+ // An explicit paragraph break is styled according to the active
+ // styles (`Some(_)`) whereas paragraph breaks forced by
+ // incompatibility take their styles from the preceding
+ // paragraph.
+ self.parbreak(Some(styles));
+ }
+ Node::Pagebreak => {
+ // We must set the flow styles after the page break such that an
+ // empty page created by two page breaks in a row has styles at
+ // all.
+ self.pagebreak();
+ self.flow.styles = styles;
+ }
+ Node::Text(text) => {
+ self.push_inline(ParChild::text(text, styles));
+ }
+ Node::Spacing(SpecAxis::Horizontal, kind) => {
+ // Just like a line break, explicit horizontal spacing eats up
+ // surrounding text spaces.
+ self.par.last.hard();
+ self.push_inline(ParChild::Spacing(SpacingNode { kind, styles }));
+ self.par.last.hard();
+ }
+ Node::Spacing(SpecAxis::Vertical, kind) => {
+ // Explicit vertical spacing ends the current paragraph and then
+ // discards the paragraph break.
+ self.parbreak(None);
+ self.make_flow_compatible(&styles);
+ self.flow
+ .children
+ .push(FlowChild::Spacing(SpacingNode { kind, styles }));
+ self.flow.last.hard();
+ }
+ Node::Inline(inline) => {
+ self.push_inline(ParChild::Node(inline.styled(styles)));
+ }
+ Node::Block(block) => {
+ self.push_block(block.styled(styles));
+ }
+ Node::Page(page) => {
+ if self.top {
+ self.pagebreak();
+ self.pages.push(page.styled(styles));
+ } else {
+ let flow = page.child.styled(page.styles);
+ self.push_block(flow.styled(styles));
+ }
+ }
+ Node::Sequence(list) => {
+ // For a list of nodes, we apply the list's styles to each node
+ // individually.
+ for (node, mut inner) in list {
+ inner.apply(&styles);
+ self.walk(node, inner);
+ }
+ }
+ }
+ }
+
+ /// Insert an inline-level element into the current paragraph.
+ fn push_inline(&mut self, child: ParChild) {
+ if let Some(child) = self.par.last.any() {
+ self.push_coalescing(child);
+ }
+
+ // The node must be both compatible with the current page and the
+ // current paragraph.
+ self.make_flow_compatible(child.styles());
+ self.make_par_compatible(child.styles());
+ self.push_coalescing(child);
+ self.par.last.any();
+ }
+
+ /// Push a paragraph child, coalescing text nodes with compatible styles.
+ fn push_coalescing(&mut self, child: ParChild) {
+ if let ParChild::Text(right) = &child {
+ if let Some(ParChild::Text(left)) = self.par.children.last_mut() {
+ if left.styles.compatible(&right.styles, TextNode::has_property) {
+ left.text.push_str(&right.text);
+ return;
+ }
+ }
+ }
+
+ self.par.children.push(child);
+ }
+
+ /// Insert a block-level element into the current flow.
+ fn push_block(&mut self, node: PackedNode) {
+ let placed = node.is::<PlacedNode>();
+
+ self.parbreak(None);
+ self.make_flow_compatible(&node.styles);
+ self.flow.children.extend(self.flow.last.any());
+ self.flow.children.push(FlowChild::Node(node));
+ self.parbreak(None);
+
+ // Prevent paragraph spacing between the placed node and the paragraph
+ // below it.
+ if placed {
+ self.flow.last.hard();
+ }
+ }
+
+ /// Advance to the next paragraph.
+ fn parbreak(&mut self, break_styles: Option<Styles>) {
+ // Erase any styles that will be inherited anyway.
+ let Builder { mut children, styles, .. } = mem::take(&mut self.par);
+ for child in &mut children {
+ child.styles_mut().erase(&styles);
+ }
+
+ // For explicit paragraph breaks, `break_styles` is already `Some(_)`.
+ // For page breaks due to incompatibility, we fall back to the styles
+ // of the preceding paragraph.
+ let break_styles = break_styles.unwrap_or_else(|| styles.clone());
+
+ // We don't want empty paragraphs.
+ if !children.is_empty() {
+ // The paragraph's children are all compatible with the page, so the
+ // paragraph is too, meaning we don't need to check or intersect
+ // anything here.
+ let par = ParNode(children).pack().styled(styles);
+ self.flow.children.extend(self.flow.last.any());
+ self.flow.children.push(FlowChild::Node(par));
+ }
+
+ // Insert paragraph spacing.
+ self.flow.last.soft(FlowChild::Break(break_styles));
+ }
+
+ /// Advance to the next page.
+ fn pagebreak(&mut self) {
+ if self.top {
+ self.parbreak(None);
+
+ // Take the flow and erase any styles that will be inherited anyway.
+ let Builder { mut children, styles, .. } = mem::take(&mut self.flow);
+ for child in &mut children {
+ child.styles_mut().erase(&styles);
+ }
+
+ let flow = FlowNode(children).pack();
+ let page = PageNode { child: flow, styles };
+ self.pages.push(page);
+ }
+ }
+
+ /// Break to a new paragraph if the `styles` contain paragraph styles that
+ /// are incompatible with the current paragraph.
+ fn make_par_compatible(&mut self, styles: &Styles) {
+ if self.par.children.is_empty() {
+ self.par.styles = styles.clone();
+ return;
+ }
+
+ if !self.par.styles.compatible(&styles, ParNode::has_property) {
+ self.parbreak(None);
+ self.par.styles = styles.clone();
+ return;
+ }
+
+ self.par.styles.intersect(&styles);
+ }
+
+ /// Break to a new page if the `styles` contain page styles that are
+ /// incompatible with the current flow.
+ fn make_flow_compatible(&mut self, styles: &Styles) {
+ if self.flow.children.is_empty() && self.par.children.is_empty() {
+ self.flow.styles = styles.clone();
+ return;
+ }
+
+ if self.top && !self.flow.styles.compatible(&styles, PageNode::has_property) {
+ self.pagebreak();
+ self.flow.styles = styles.clone();
+ return;
+ }
+
+ self.flow.styles.intersect(styles);
+ }
+}
+
+/// Container for building a flow or paragraph.
+struct Builder<T> {
+ /// The intersection of the style properties of all `children`.
+ styles: Styles,
+ /// The accumulated flow or paragraph children.
+ children: Vec<T>,
+ /// The kind of thing that was last added.
+ last: Last<T>,
+}
+
+impl<T> Default for Builder<T> {
+ fn default() -> Self {
+ Self {
+ styles: Styles::new(),
+ children: vec![],
+ last: Last::None,
+ }
+ }
+}
+
+/// The kind of node that was last added to a flow or paragraph. A small finite
+/// state machine used to coalesce spaces.
+///
+/// Soft nodes can only exist when surrounded by `Any` nodes. Not at the
+/// start, end or next to hard nodes. This way, spaces at start and end of
+/// paragraphs and next to `#h(..)` goes away.
+enum Last<N> {
+ /// Start state, nothing there.
+ None,
+ /// Text or a block node or something.
+ Any,
+ /// Hard nodes: Linebreaks and explicit spacing.
+ Hard,
+ /// Soft nodes: Word spaces and paragraph breaks. These are saved here
+ /// temporarily and then applied once an `Any` node appears.
+ Soft(N),
+}
+
+impl<N> Last<N> {
+ /// Transition into the `Any` state and return a soft node to really add
+ /// now if currently in `Soft` state.
+ fn any(&mut self) -> Option<N> {
+ match mem::replace(self, Self::Any) {
+ Self::Soft(soft) => Some(soft),
+ _ => None,
+ }
+ }
+
+ /// Transition into the `Soft` state, but only if in `Any`. Otherwise, the
+ /// soft node is discarded.
+ fn soft(&mut self, soft: N) {
+ if let Self::Any = self {
+ *self = Self::Soft(soft);
+ }
+ }
+
+ /// Transition into the `Hard` state, discarding a possibly existing soft
+ /// node and preventing further soft nodes from being added.
+ fn hard(&mut self) {
+ *self = Self::Hard;
+ }
+}
diff --git a/src/eval/ops.rs b/src/eval/ops.rs
index ede1230f..23530c10 100644
--- a/src/eval/ops.rs
+++ b/src/eval/ops.rs
@@ -22,9 +22,9 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(Str(a), Str(b)) => Str(a + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
- (Template(a), Template(b)) => Template(a + b),
- (Template(a), Str(b)) => Template(a + b),
- (Str(a), Template(b)) => Template(a + b),
+ (Node(a), Node(b)) => Node(a + b),
+ (Node(a), Str(b)) => Node(a + super::Node::Text(b)),
+ (Str(a), Node(b)) => Node(super::Node::Text(a) + b),
(a, b) => mismatch!("cannot join {} with {}", a, b),
})
}
@@ -84,9 +84,9 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Str(a), Str(b)) => Str(a + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
- (Template(a), Template(b)) => Template(a + b),
- (Template(a), Str(b)) => Template(a + b),
- (Str(a), Template(b)) => Template(a + b),
+ (Node(a), Node(b)) => Node(a + b),
+ (Node(a), Str(b)) => Node(a + super::Node::Text(b)),
+ (Str(a), Node(b)) => Node(super::Node::Text(a) + b),
(a, b) => {
if let (Dyn(a), Dyn(b)) = (&a, &b) {
@@ -179,8 +179,8 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> {
(Int(a), Str(b)) => Str(repeat_str(b, a)?),
(Array(a), Int(b)) => Array(a.repeat(b)?),
(Int(a), Array(b)) => Array(b.repeat(a)?),
- (Template(a), Int(b)) => Template(a.repeat(b)?),
- (Int(a), Template(b)) => Template(b.repeat(a)?),
+ (Node(a), Int(b)) => Node(a.repeat(b)?),
+ (Int(a), Node(b)) => Node(b.repeat(a)?),
(a, b) => mismatch!("cannot multiply {} with {}", a, b),
})
@@ -297,7 +297,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Str(a), Str(b)) => a == b,
(Array(a), Array(b)) => a == b,
(Dict(a), Dict(b)) => a == b,
- (Template(a), Template(b)) => a == b,
+ (Node(a), Node(b)) => a == b,
(Func(a), Func(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b,
diff --git a/src/eval/scope.rs b/src/eval/scope.rs
index 2290affd..5178c819 100644
--- a/src/eval/scope.rs
+++ b/src/eval/scope.rs
@@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Formatter};
use std::iter;
use std::rc::Rc;
-use super::{Args, EvalContext, Function, Value};
+use super::{Args, Class, Construct, EvalContext, Function, Set, Value};
use crate::diag::TypResult;
use crate::util::EcoString;
@@ -88,15 +88,6 @@ impl Scope {
self.values.insert(var.into(), Rc::new(cell));
}
- /// Define a constant function.
- pub fn def_func<F>(&mut self, name: impl Into<EcoString>, f: F)
- where
- F: Fn(&mut EvalContext, &mut Args) -> TypResult<Value> + 'static,
- {
- let name = name.into();
- self.def_const(name.clone(), Function::new(Some(name), f));
- }
-
/// Define a mutable variable with a value.
pub fn def_mut(&mut self, var: impl Into<EcoString>, value: impl Into<Value>) {
self.values.insert(var.into(), Rc::new(RefCell::new(value.into())));
@@ -107,6 +98,24 @@ impl Scope {
self.values.insert(var.into(), slot);
}
+ /// Define a constant function.
+ pub fn def_func<F>(&mut self, name: &str, f: F)
+ where
+ F: Fn(&mut EvalContext, &mut Args) -> TypResult<Value> + 'static,
+ {
+ let name = EcoString::from(name);
+ self.def_const(name.clone(), Function::new(Some(name), f));
+ }
+
+ /// Define a constant class.
+ pub fn def_class<T>(&mut self, name: &str)
+ where
+ T: Construct + Set + 'static,
+ {
+ let name = EcoString::from(name);
+ self.def_const(name.clone(), Class::new::<T>(name));
+ }
+
/// Look up the value of a variable.
pub fn get(&self, var: &str) -> Option<&Slot> {
self.values.get(var)
@@ -120,6 +129,7 @@ impl Scope {
impl Debug for Scope {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Scope ")?;
f.debug_map()
.entries(self.values.iter().map(|(k, v)| (k, v.borrow())))
.finish()
diff --git a/src/eval/styles.rs b/src/eval/styles.rs
new file mode 100644
index 00000000..1c4b17ae
--- /dev/null
+++ b/src/eval/styles.rs
@@ -0,0 +1,292 @@
+use std::any::{Any, TypeId};
+use std::fmt::{self, Debug, Formatter};
+use std::hash::{Hash, Hasher};
+use std::rc::Rc;
+
+// TODO(style): Possible optimizations:
+// - Ref-count map for cheaper cloning and smaller footprint
+// - Store map in `Option` to make empty maps non-allocating
+// - Store small properties inline
+
+/// A map of style properties.
+#[derive(Default, Clone, Hash)]
+pub struct Styles {
+ map: Vec<(StyleId, Entry)>,
+}
+
+impl Styles {
+ /// Create a new, empty style map.
+ pub fn new() -> Self {
+ 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();
+ styles.set(key, value);
+ styles
+ }
+
+ /// Set the value for a style property.
+ pub fn set<P: Property>(&mut self, key: P, value: P::Value) {
+ let id = StyleId::of::<P>();
+ for pair in &mut self.map {
+ if pair.0 == id {
+ let prev = pair.1.downcast::<P::Value>().unwrap();
+ let folded = P::combine(value, prev.clone());
+ pair.1 = Entry::new(key, folded);
+ return;
+ }
+ }
+
+ 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>();
+ for (i, pair) in self.map.iter_mut().enumerate() {
+ if pair.0 == id {
+ self.map.swap_remove(i);
+ return;
+ }
+ }
+
+ self.map.push((id, Entry::new(key, true)));
+ }
+
+ /// Get the value of a copyable style property.
+ ///
+ /// Returns the property's default value if the map does not contain an
+ /// entry for it.
+ pub fn get<P: Property>(&self, key: P) -> P::Value
+ where
+ P::Value: Copy,
+ {
+ self.get_direct(key)
+ .map(|&v| P::combine(v, P::default()))
+ .unwrap_or_else(P::default)
+ }
+
+ /// Get a reference to a style property.
+ ///
+ /// Returns a reference to the property's default value if the map does not
+ /// contain an entry for it.
+ pub fn get_ref<P: Property>(&self, key: P) -> &P::Value {
+ self.get_direct(key).unwrap_or_else(|| P::default_ref())
+ }
+
+ /// Get a reference to a style directly in this map (no default 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`.
+ pub fn apply(&mut self, outer: &Self) {
+ 'outer: for pair in &outer.map {
+ for (id, entry) in &mut self.map {
+ if pair.0 == *id {
+ entry.apply(&pair.1);
+ continue 'outer;
+ }
+ }
+
+ self.map.push(pair.clone());
+ }
+ }
+
+ /// 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`.
+ pub fn intersect(&mut self, other: &Self) {
+ self.map.retain(|a| other.map.iter().any(|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(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))
+ }
+}
+
+impl Debug for Styles {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ for pair in &self.map {
+ writeln!(f, "{:#?}", pair.1)?;
+ }
+ Ok(())
+ } else {
+ f.write_str("Styles ")?;
+ f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish()
+ }
+ }
+}
+
+impl PartialEq for Styles {
+ fn eq(&self, other: &Self) -> bool {
+ self.compatible(other, |_| true)
+ }
+}
+
+/// An entry for a single style property.
+#[derive(Clone)]
+pub(crate) struct Entry(Rc<dyn Bounds>);
+
+impl Entry {
+ fn new<P: Property>(key: P, value: P::Value) -> Self {
+ Self(Rc::new((key, value)))
+ }
+
+ fn downcast<T: 'static>(&self) -> Option<&T> {
+ self.0.as_any().downcast_ref()
+ }
+
+ fn apply(&mut self, outer: &Self) {
+ *self = self.0.combine(outer);
+ }
+}
+
+impl Debug for Entry {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.0.dyn_fmt(f)
+ }
+}
+
+impl PartialEq for Entry {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.dyn_eq(other)
+ }
+}
+
+impl Hash for Entry {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ state.write_u64(self.0.hash64());
+ }
+}
+
+trait Bounds: 'static {
+ fn as_any(&self) -> &dyn Any;
+ 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;
+}
+
+// `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 dyn_fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ write!(f, "#[{} = {:?}]", P::NAME, self.1)
+ } else {
+ write!(f, "{}: {:?}", P::NAME, self.1)
+ }
+ }
+
+ fn dyn_eq(&self, other: &Entry) -> bool {
+ if let Some(other) = other.downcast::<P::Value>() {
+ &self.1 == other
+ } else {
+ false
+ }
+ }
+
+ fn hash64(&self) -> u64 {
+ // No need to hash the TypeId since there's only one
+ // valid value type per property.
+ fxhash::hash64(&self.1)
+ }
+
+ fn combine(&self, outer: &Entry) -> Entry {
+ let outer = outer.downcast::<P::Value>().unwrap();
+ let combined = P::combine(self.1.clone(), outer.clone());
+ Entry::new(self.0, combined)
+ }
+}
+
+/// Style property keys.
+///
+/// This trait is not intended to be implemented manually, but rather through
+/// the `#[properties]` proc-macro.
+pub trait Property: Copy + '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;
+
+ /// The default value of the property.
+ fn default() -> Self::Value;
+
+ /// 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
+ /// recreated all the time.
+ fn default_ref() -> &'static 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
+ }
+}
+
+/// A unique identifier for a style property.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct StyleId(TypeId);
+
+impl StyleId {
+ /// The style id of the property.
+ pub fn of<P: Property>() -> Self {
+ Self(TypeId::of::<P>())
+ }
+}
diff --git a/src/eval/template.rs b/src/eval/template.rs
deleted file mode 100644
index 9c57bbf3..00000000
--- a/src/eval/template.rs
+++ /dev/null
@@ -1,547 +0,0 @@
-use std::convert::TryFrom;
-use std::fmt::{self, Debug, Formatter};
-use std::hash::Hash;
-use std::mem;
-use std::ops::{Add, AddAssign};
-use std::rc::Rc;
-
-use crate::diag::StrResult;
-use crate::geom::{Align, Dir, Length, Linear, Paint, Sides, Size, SpecAxis};
-use crate::layout::{Layout, PackedNode};
-use crate::library::{
- Decoration, DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode,
- PlacedNode, Spacing,
-};
-use crate::style::Style;
-use crate::util::EcoString;
-
-/// A template value: `[*Hi* there]`.
-#[derive(Default, Clone)]
-pub struct Template(Rc<Vec<TemplateNode>>);
-
-/// One node in a template.
-#[derive(Clone)]
-enum TemplateNode {
- /// A word space.
- Space,
- /// A line break.
- Linebreak,
- /// A paragraph break.
- Parbreak,
- /// A page break.
- Pagebreak(bool),
- /// Plain text.
- Text(EcoString),
- /// Spacing.
- Spacing(SpecAxis, Spacing),
- /// A decorated template.
- Decorated(Decoration, Template),
- /// An inline node builder.
- Inline(Rc<dyn Fn(&Style) -> PackedNode>),
- /// A block node builder.
- Block(Rc<dyn Fn(&Style) -> PackedNode>),
- /// Save the current style.
- Save,
- /// Restore the last saved style.
- Restore,
- /// A function that can modify the current style.
- Modify(Rc<dyn Fn(&mut Style)>),
-}
-
-impl Template {
- /// Create a new, empty template.
- pub fn new() -> Self {
- Self(Rc::new(vec![]))
- }
-
- /// Create a template from a builder for an inline-level node.
- pub fn from_inline<F, T>(f: F) -> Self
- where
- F: Fn(&Style) -> T + 'static,
- T: Layout + Debug + Hash + 'static,
- {
- let node = TemplateNode::Inline(Rc::new(move |s| f(s).pack()));
- Self(Rc::new(vec![node]))
- }
-
- /// Create a template from a builder for a block-level node.
- pub fn from_block<F, T>(f: F) -> Self
- where
- F: Fn(&Style) -> T + 'static,
- T: Layout + Debug + Hash + 'static,
- {
- let node = TemplateNode::Block(Rc::new(move |s| f(s).pack()));
- Self(Rc::new(vec![node]))
- }
-
- /// Add a word space to the template.
- pub fn space(&mut self) {
- self.make_mut().push(TemplateNode::Space);
- }
-
- /// Add a line break to the template.
- pub fn linebreak(&mut self) {
- self.make_mut().push(TemplateNode::Linebreak);
- }
-
- /// Add a paragraph break to the template.
- pub fn parbreak(&mut self) {
- self.make_mut().push(TemplateNode::Parbreak);
- }
-
- /// Add a page break to the template.
- pub fn pagebreak(&mut self, keep: bool) {
- self.make_mut().push(TemplateNode::Pagebreak(keep));
- }
-
- /// Add text to the template.
- pub fn text(&mut self, text: impl Into<EcoString>) {
- self.make_mut().push(TemplateNode::Text(text.into()));
- }
-
- /// Add text, but in monospace.
- pub fn monospace(&mut self, text: impl Into<EcoString>) {
- self.save();
- self.modify(|style| style.text_mut().monospace = true);
- self.text(text);
- self.restore();
- }
-
- /// Add spacing along an axis.
- pub fn spacing(&mut self, axis: SpecAxis, spacing: Spacing) {
- self.make_mut().push(TemplateNode::Spacing(axis, spacing));
- }
-
- /// Register a restorable snapshot.
- pub fn save(&mut self) {
- self.make_mut().push(TemplateNode::Save);
- }
-
- /// Ensure that later nodes are untouched by style modifications made since
- /// the last snapshot.
- pub fn restore(&mut self) {
- self.make_mut().push(TemplateNode::Restore);
- }
-
- /// Modify the style.
- pub fn modify<F>(&mut self, f: F)
- where
- F: Fn(&mut Style) + 'static,
- {
- self.make_mut().push(TemplateNode::Modify(Rc::new(f)));
- }
-
- /// Return a new template which is modified from start to end.
- pub fn modified<F>(self, f: F) -> Self
- where
- F: Fn(&mut Style) + 'static,
- {
- let mut wrapper = Self::new();
- wrapper.save();
- wrapper.modify(f);
- wrapper += self;
- wrapper.restore();
- wrapper
- }
-
- /// Add a decoration to all contained nodes.
- pub fn decorate(self, deco: Decoration) -> Self {
- Self(Rc::new(vec![TemplateNode::Decorated(deco, self)]))
- }
-
- /// Pack the template into a layout node.
- pub fn pack(&self, style: &Style) -> PackedNode {
- if let [TemplateNode::Block(f)] = self.0.as_slice() {
- f(style)
- } else {
- let mut builder = Builder::new(style, false);
- builder.template(self);
- builder.build_flow().pack()
- }
- }
-
- /// Build the layout tree resulting from instantiating the template with the
- /// given style.
- pub fn to_document(&self, style: &Style) -> DocumentNode {
- let mut builder = Builder::new(style, true);
- builder.template(self);
- builder.build_document()
- }
-
- /// Repeat this template `n` times.
- pub fn repeat(&self, n: i64) -> StrResult<Self> {
- let count = usize::try_from(n)
- .ok()
- .and_then(|n| self.0.len().checked_mul(n))
- .ok_or_else(|| format!("cannot repeat this template {} times", n))?;
-
- Ok(Self(Rc::new(
- self.0.iter().cloned().cycle().take(count).collect(),
- )))
- }
-
- /// Return a mutable reference to the inner vector.
- fn make_mut(&mut self) -> &mut Vec<TemplateNode> {
- Rc::make_mut(&mut self.0)
- }
-}
-
-impl Debug for Template {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad("<template>")
- }
-}
-
-impl PartialEq for Template {
- fn eq(&self, other: &Self) -> bool {
- Rc::ptr_eq(&self.0, &other.0)
- }
-}
-
-impl Add for Template {
- type Output = Self;
-
- fn add(mut self, rhs: Self) -> Self::Output {
- self += rhs;
- self
- }
-}
-
-impl AddAssign for Template {
- fn add_assign(&mut self, rhs: Template) {
- let sink = Rc::make_mut(&mut self.0);
- match Rc::try_unwrap(rhs.0) {
- Ok(source) => sink.extend(source),
- Err(rc) => sink.extend(rc.iter().cloned()),
- }
- }
-}
-
-impl Add<EcoString> for Template {
- type Output = Self;
-
- fn add(mut self, rhs: EcoString) -> Self::Output {
- Rc::make_mut(&mut self.0).push(TemplateNode::Text(rhs));
- self
- }
-}
-
-impl Add<Template> for EcoString {
- type Output = Template;
-
- fn add(self, mut rhs: Template) -> Self::Output {
- Rc::make_mut(&mut rhs.0).insert(0, TemplateNode::Text(self));
- rhs
- }
-}
-
-/// Transforms from template to layout representation.
-struct Builder {
- /// The current style.
- style: Style,
- /// Snapshots of the style.
- snapshots: Vec<Style>,
- /// The finished page nodes.
- finished: Vec<PageNode>,
- /// When we are building the top-level layout trees, this contains metrics
- /// of the page. While building a flow, this is `None`.
- page: Option<PageBuilder>,
- /// The currently built flow of paragraphs.
- flow: FlowBuilder,
-}
-
-impl Builder {
- /// Create a new builder with a base style.
- fn new(style: &Style, pages: bool) -> Self {
- Self {
- style: style.clone(),
- snapshots: vec![],
- finished: vec![],
- page: pages.then(|| PageBuilder::new(style, true)),
- flow: FlowBuilder::new(style),
- }
- }
-
- /// Build a template.
- fn template(&mut self, template: &Template) {
- for node in template.0.iter() {
- self.node(node);
- }
- }
-
- /// Build a template node.
- fn node(&mut self, node: &TemplateNode) {
- match node {
- TemplateNode::Save => self.snapshots.push(self.style.clone()),
- TemplateNode::Restore => {
- let style = self.snapshots.pop().unwrap();
- let newpage = style.page != self.style.page;
- self.style = style;
- if newpage {
- self.pagebreak(true, false);
- }
- }
- TemplateNode::Space => self.space(),
- TemplateNode::Linebreak => self.linebreak(),
- TemplateNode::Parbreak => self.parbreak(),
- TemplateNode::Pagebreak(keep) => self.pagebreak(*keep, true),
- TemplateNode::Text(text) => self.text(text),
- TemplateNode::Spacing(axis, amount) => self.spacing(*axis, *amount),
- TemplateNode::Decorated(deco, template) => {
- self.flow.par.push(ParChild::Decorate(deco.clone()));
- self.template(template);
- self.flow.par.push(ParChild::Undecorate);
- }
- TemplateNode::Inline(f) => self.inline(f(&self.style)),
- TemplateNode::Block(f) => self.block(f(&self.style)),
- TemplateNode::Modify(f) => f(&mut self.style),
- }
- }
-
- /// Push a word space into the active paragraph.
- fn space(&mut self) {
- self.flow.par.push_soft(self.make_text_node(' '));
- }
-
- /// Apply a forced line break.
- fn linebreak(&mut self) {
- self.flow.par.push_hard(self.make_text_node('\n'));
- }
-
- /// Apply a forced paragraph break.
- fn parbreak(&mut self) {
- let amount = self.style.par_spacing();
- self.flow.finish_par(&self.style);
- self.flow
- .push_soft(FlowChild::Spacing(Spacing::Linear(amount.into())));
- }
-
- /// Apply a forced page break.
- fn pagebreak(&mut self, keep: bool, hard: bool) {
- if let Some(builder) = &mut self.page {
- let page = mem::replace(builder, PageBuilder::new(&self.style, hard));
- let flow = mem::replace(&mut self.flow, FlowBuilder::new(&self.style));
- self.finished.extend(page.build(flow.build(), keep));
- }
- }
-
- /// Push text into the active paragraph.
- fn text(&mut self, text: impl Into<EcoString>) {
- self.flow.par.push(self.make_text_node(text));
- }
-
- /// Push an inline node into the active paragraph.
- fn inline(&mut self, node: PackedNode) {
- self.flow.par.push(ParChild::Node(node.into()));
- }
-
- /// Push a block node into the active flow, finishing the active paragraph.
- fn block(&mut self, node: PackedNode) {
- let mut is_placed = false;
- if let Some(placed) = node.downcast::<PlacedNode>() {
- is_placed = true;
-
- // This prevents paragraph spacing after the placed node if it
- // is completely out-of-flow.
- if placed.out_of_flow() {
- self.flow.last = Last::None;
- }
- }
-
- self.parbreak();
- self.flow.push(FlowChild::Node(node));
- self.parbreak();
-
- // This prevents paragraph spacing between the placed node and
- // the paragraph below it.
- if is_placed {
- self.flow.last = Last::None;
- }
- }
-
- /// Push spacing into the active paragraph or flow depending on the `axis`.
- fn spacing(&mut self, axis: SpecAxis, spacing: Spacing) {
- match axis {
- SpecAxis::Vertical => {
- self.flow.finish_par(&self.style);
- self.flow.push_hard(FlowChild::Spacing(spacing));
- }
- SpecAxis::Horizontal => {
- self.flow.par.push_hard(ParChild::Spacing(spacing));
- }
- }
- }
-
- /// Finish building and return the created flow.
- fn build_flow(self) -> FlowNode {
- assert!(self.page.is_none());
- self.flow.build()
- }
-
- /// Finish building and return the created layout tree.
- fn build_document(mut self) -> DocumentNode {
- assert!(self.page.is_some());
- self.pagebreak(true, false);
- DocumentNode { pages: self.finished }
- }
-
- /// Construct a text node with the given text and settings from the current
- /// style.
- fn make_text_node(&self, text: impl Into<EcoString>) -> ParChild {
- ParChild::Text(text.into(), Rc::clone(&self.style.text))
- }
-}
-
-struct PageBuilder {
- size: Size,
- padding: Sides<Linear>,
- fill: Option<Paint>,
- hard: bool,
-}
-
-impl PageBuilder {
- fn new(style: &Style, hard: bool) -> Self {
- Self {
- size: style.page.size,
- padding: style.page.margins(),
- fill: style.page.fill,
- hard,
- }
- }
-
- fn build(self, child: FlowNode, keep: bool) -> Option<PageNode> {
- let Self { size, padding, fill, hard } = self;
- (!child.children.is_empty() || (keep && hard)).then(|| PageNode {
- child: child.pack().padded(padding),
- size,
- fill,
- })
- }
-}
-
-struct FlowBuilder {
- children: Vec<FlowChild>,
- last: Last<FlowChild>,
- par: ParBuilder,
-}
-
-impl FlowBuilder {
- fn new(style: &Style) -> Self {
- Self {
- children: vec![],
- last: Last::None,
- par: ParBuilder::new(style),
- }
- }
-
- fn push(&mut self, child: FlowChild) {
- self.children.extend(self.last.any());
- self.children.push(child);
- }
-
- fn push_soft(&mut self, child: FlowChild) {
- self.last.soft(child);
- }
-
- fn push_hard(&mut self, child: FlowChild) {
- self.last.hard();
- self.children.push(child);
- }
-
- fn finish_par(&mut self, style: &Style) {
- let par = mem::replace(&mut self.par, ParBuilder::new(style));
- if let Some(par) = par.build() {
- self.push(par);
- }
- }
-
- fn build(self) -> FlowNode {
- let Self { mut children, par, mut last } = self;
- if let Some(par) = par.build() {
- children.extend(last.any());
- children.push(par);
- }
- FlowNode { children }
- }
-}
-
-struct ParBuilder {
- dir: Dir,
- align: Align,
- leading: Length,
- children: Vec<ParChild>,
- last: Last<ParChild>,
-}
-
-impl ParBuilder {
- fn new(style: &Style) -> Self {
- Self {
- dir: style.par.dir,
- align: style.par.align,
- leading: style.leading(),
- children: vec![],
- last: Last::None,
- }
- }
-
- fn push(&mut self, child: ParChild) {
- if let Some(soft) = self.last.any() {
- self.push_inner(soft);
- }
- self.push_inner(child);
- }
-
- fn push_soft(&mut self, child: ParChild) {
- self.last.soft(child);
- }
-
- fn push_hard(&mut self, child: ParChild) {
- self.last.hard();
- self.push_inner(child);
- }
-
- fn push_inner(&mut self, child: ParChild) {
- if let ParChild::Text(text2, style2) = &child {
- if let Some(ParChild::Text(text1, style1)) = self.children.last_mut() {
- if Rc::ptr_eq(style1, style2) {
- text1.push_str(text2);
- return;
- }
- }
- }
-
- self.children.push(child);
- }
-
- fn build(self) -> Option<FlowChild> {
- let Self { dir, align, leading, children, .. } = self;
- (!children.is_empty())
- .then(|| FlowChild::Node(ParNode { dir, align, leading, children }.pack()))
- }
-}
-
-/// Finite state machine for spacing coalescing.
-enum Last<N> {
- None,
- Any,
- Soft(N),
-}
-
-impl<N> Last<N> {
- fn any(&mut self) -> Option<N> {
- match mem::replace(self, Self::Any) {
- Self::Soft(soft) => Some(soft),
- _ => None,
- }
- }
-
- fn soft(&mut self, soft: N) {
- if let Self::Any = self {
- *self = Self::Soft(soft);
- }
- }
-
- fn hard(&mut self) {
- *self = Self::None;
- }
-}
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 16e8b810..0995ab75 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -1,11 +1,13 @@
use std::any::Any;
use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
+use std::hash::Hash;
use std::rc::Rc;
-use super::{ops, Array, Dict, Function, Template};
+use super::{ops, Array, Class, Dict, Function, Node};
use crate::diag::StrResult;
use crate::geom::{Angle, Color, Fractional, Length, Linear, Relative, RgbaColor};
+use crate::layout::Layout;
use crate::syntax::Spanned;
use crate::util::EcoString;
@@ -24,7 +26,7 @@ pub enum Value {
Float(f64),
/// A length: `12pt`, `3cm`.
Length(Length),
- /// An angle: `1.5rad`, `90deg`.
+ /// An angle: `1.5rad`, `90deg`.
Angle(Angle),
/// A relative value: `50%`.
Relative(Relative),
@@ -40,15 +42,33 @@ pub enum Value {
Array(Array),
/// A dictionary value: `(color: #f79143, pattern: dashed)`.
Dict(Dict),
- /// A template value: `[*Hi* there]`.
- Template(Template),
+ /// A node value: `[*Hi* there]`.
+ Node(Node),
/// An executable function.
Func(Function),
+ /// A class of nodes.
+ Class(Class),
/// A dynamic value.
Dyn(Dynamic),
}
impl Value {
+ /// Create an inline-level node value.
+ pub fn inline<T>(node: T) -> Self
+ where
+ T: Layout + Debug + Hash + 'static,
+ {
+ Self::Node(Node::inline(node))
+ }
+
+ /// Create a block-level node value.
+ pub fn block<T>(node: T) -> Self
+ where
+ T: Layout + Debug + Hash + 'static,
+ {
+ Self::Node(Node::block(node))
+ }
+
/// The name of the stored value's type.
pub fn type_name(&self) -> &'static str {
match self {
@@ -66,8 +86,9 @@ impl Value {
Self::Str(_) => EcoString::TYPE_NAME,
Self::Array(_) => Array::TYPE_NAME,
Self::Dict(_) => Dict::TYPE_NAME,
- Self::Template(_) => Template::TYPE_NAME,
+ Self::Node(_) => Node::TYPE_NAME,
Self::Func(_) => Function::TYPE_NAME,
+ Self::Class(_) => Class::TYPE_NAME,
Self::Dyn(v) => v.type_name(),
}
}
@@ -80,14 +101,28 @@ impl Value {
T::cast(self)
}
+ /// Join the value with another value.
+ pub fn join(self, rhs: Self) -> StrResult<Self> {
+ ops::join(self, rhs)
+ }
+
/// Return the debug representation of the value.
pub fn repr(&self) -> EcoString {
format_eco!("{:?}", self)
}
- /// Join the value with another value.
- pub fn join(self, rhs: Self) -> StrResult<Self> {
- ops::join(self, rhs)
+ /// Return the display representation of the value.
+ pub fn show(self) -> Node {
+ match self {
+ Value::None => Node::new(),
+ Value::Int(v) => Node::Text(format_eco!("{}", v)),
+ Value::Float(v) => Node::Text(format_eco!("{}", v)),
+ Value::Str(v) => Node::Text(v),
+ Value::Node(v) => v,
+ // For values which can't be shown "naturally", we print the
+ // representation in monospace.
+ v => Node::Text(v.repr()).monospaced(),
+ }
}
}
@@ -114,8 +149,9 @@ impl Debug for Value {
Self::Str(v) => Debug::fmt(v, f),
Self::Array(v) => Debug::fmt(v, f),
Self::Dict(v) => Debug::fmt(v, f),
- Self::Template(v) => Debug::fmt(v, f),
+ Self::Node(_) => f.pad("<template>"),
Self::Func(v) => Debug::fmt(v, f),
+ Self::Class(v) => Debug::fmt(v, f),
Self::Dyn(v) => Debug::fmt(v, f),
}
}
@@ -354,14 +390,15 @@ primitive! { f64: "float", Float, Int(v) => v as f64 }
primitive! { Length: "length", Length }
primitive! { Angle: "angle", Angle }
primitive! { Relative: "relative", Relative }
-primitive! { Linear: "linear", Linear, Length(v) => v.into(), Relative(v) => v.into() }
-primitive! { Fractional: "fractional", Fractional }
+primitive! { Linear: "relative length", Linear, Length(v) => v.into(), Relative(v) => v.into() }
+primitive! { Fractional: "fractional length", Fractional }
primitive! { Color: "color", Color }
primitive! { EcoString: "string", Str }
primitive! { Array: "array", Array }
primitive! { Dict: "dictionary", Dict }
-primitive! { Template: "template", Template }
+primitive! { Node: "template", Node }
primitive! { Function: "function", Func }
+primitive! { Class: "class", Class }
impl Cast<Value> for Value {
fn is(_: &Value) -> bool {
diff --git a/src/eval/walk.rs b/src/eval/walk.rs
deleted file mode 100644
index 0898f20b..00000000
--- a/src/eval/walk.rs
+++ /dev/null
@@ -1,141 +0,0 @@
-use std::rc::Rc;
-
-use super::{Eval, EvalContext, Template, Value};
-use crate::diag::TypResult;
-use crate::geom::Spec;
-use crate::layout::Layout;
-use crate::library::{GridNode, ParChild, ParNode, TrackSizing};
-use crate::syntax::ast::*;
-use crate::util::{BoolExt, EcoString};
-
-/// Walk markup, filling the currently built template.
-pub trait Walk {
- /// Walk the node.
- fn walk(&self, ctx: &mut EvalContext) -> TypResult<()>;
-}
-
-impl Walk for Markup {
- fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
- for node in self.nodes() {
- node.walk(ctx)?;
- }
- Ok(())
- }
-}
-
-impl Walk for MarkupNode {
- fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
- match self {
- Self::Space => ctx.template.space(),
- Self::Linebreak => ctx.template.linebreak(),
- Self::Parbreak => ctx.template.parbreak(),
- Self::Strong => ctx.template.modify(|s| s.text_mut().strong.flip()),
- Self::Emph => ctx.template.modify(|s| s.text_mut().emph.flip()),
- Self::Text(text) => ctx.template.text(text),
- Self::Raw(raw) => raw.walk(ctx)?,
- Self::Math(math) => math.walk(ctx)?,
- Self::Heading(heading) => heading.walk(ctx)?,
- Self::List(list) => list.walk(ctx)?,
- Self::Enum(enum_) => enum_.walk(ctx)?,
- Self::Expr(expr) => match expr.eval(ctx)? {
- Value::None => {}
- Value::Int(v) => ctx.template.text(format_eco!("{}", v)),
- Value::Float(v) => ctx.template.text(format_eco!("{}", v)),
- Value::Str(v) => ctx.template.text(v),
- Value::Template(v) => ctx.template += v,
- // For values which can't be shown "naturally", we print the
- // representation in monospace.
- other => ctx.template.monospace(other.repr()),
- },
- }
- Ok(())
- }
-}
-
-impl Walk for RawNode {
- fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
- if self.block {
- ctx.template.parbreak();
- }
-
- ctx.template.monospace(&self.text);
-
- if self.block {
- ctx.template.parbreak();
- }
-
- Ok(())
- }
-}
-
-impl Walk for MathNode {
- fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
- if self.display {
- ctx.template.parbreak();
- }
-
- ctx.template.monospace(self.formula.trim());
-
- if self.display {
- ctx.template.parbreak();
- }
-
- Ok(())
- }
-}
-
-impl Walk for HeadingNode {
- fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
- let level = self.level();
- let body = self.body().eval(ctx)?;
-
- ctx.template.parbreak();
- ctx.template.save();
- ctx.template.modify(move |style| {
- let text = style.text_mut();
- let upscale = (1.6 - 0.1 * level as f64).max(0.75);
- text.size *= upscale;
- text.strong = true;
- });
- ctx.template += body;
- ctx.template.restore();
- ctx.template.parbreak();
-
- Ok(())
- }
-}
-
-impl Walk for ListNode {
- fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
- let body = self.body().eval(ctx)?;
- walk_item(ctx, EcoString::from('•'), body);
- Ok(())
- }
-}
-
-impl Walk for EnumNode {
- fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
- let body = self.body().eval(ctx)?;
- let label = format_eco!("{}.", self.number().unwrap_or(1));
- walk_item(ctx, label, body);
- Ok(())
- }
-}
-
-fn walk_item(ctx: &mut EvalContext, label: EcoString, body: Template) {
- ctx.template += Template::from_block(move |style| {
- let label = Layout::pack(ParNode {
- dir: style.par.dir,
- align: style.par.align,
- leading: style.leading(),
- children: vec![ParChild::Text(label.clone(), Rc::clone(&style.text))],
- });
-
- let spacing = style.text.size / 2.0;
- GridNode {
- tracks: Spec::new(vec![TrackSizing::Auto; 2], vec![]),
- gutter: Spec::new(vec![TrackSizing::Linear(spacing.into())], vec![]),
- children: vec![label, body.pack(style)],
- }
- });
-}
diff --git a/src/font.rs b/src/font.rs
index 5afd1aca..c2e1beac 100644
--- a/src/font.rs
+++ b/src/font.rs
@@ -283,11 +283,11 @@ impl Face {
/// Look up a vertical metric at the given font size.
pub fn vertical_metric(&self, metric: VerticalFontMetric, size: Length) -> Length {
match metric {
- VerticalFontMetric::Ascender => self.ascender.to_length(size),
- VerticalFontMetric::CapHeight => self.cap_height.to_length(size),
- VerticalFontMetric::XHeight => self.x_height.to_length(size),
+ VerticalFontMetric::Ascender => self.ascender.resolve(size),
+ VerticalFontMetric::CapHeight => self.cap_height.resolve(size),
+ VerticalFontMetric::XHeight => self.x_height.resolve(size),
VerticalFontMetric::Baseline => Length::zero(),
- VerticalFontMetric::Descender => self.descender.to_length(size),
+ VerticalFontMetric::Descender => self.descender.resolve(size),
VerticalFontMetric::Linear(v) => v.resolve(size),
}
}
diff --git a/src/frame.rs b/src/frame.rs
index b0442a06..46f23446 100644
--- a/src/frame.rs
+++ b/src/frame.rs
@@ -107,6 +107,11 @@ impl Frame {
wrapper.push(Point::zero(), Element::Group(group));
*self = wrapper;
}
+
+ /// Link the whole frame to a resource.
+ pub fn link(&mut self, url: impl Into<String>) {
+ self.push(Point::zero(), Element::Link(url.into(), self.size));
+ }
}
impl Debug for Frame {
@@ -171,14 +176,19 @@ pub struct Text {
pub face_id: FaceId,
/// The font size.
pub size: Length,
- /// The width of the text run.
- pub width: Length,
/// Glyph color.
pub fill: Paint,
/// The glyphs.
pub glyphs: Vec<Glyph>,
}
+impl Text {
+ /// The width of the text run.
+ pub fn width(&self) -> Length {
+ self.glyphs.iter().map(|g| g.x_advance.resolve(self.size)).sum()
+ }
+}
+
/// A glyph in a run of shaped text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Glyph {
diff --git a/src/geom/em.rs b/src/geom/em.rs
index 05d1d7d5..1868222f 100644
--- a/src/geom/em.rs
+++ b/src/geom/em.rs
@@ -29,7 +29,7 @@ impl Em {
}
/// Convert to a length at the given font size.
- pub fn to_length(self, font_size: Length) -> Length {
+ pub fn resolve(self, font_size: Length) -> Length {
self.get() * font_size
}
diff --git a/src/geom/linear.rs b/src/geom/linear.rs
index 77923c43..78602d8b 100644
--- a/src/geom/linear.rs
+++ b/src/geom/linear.rs
@@ -36,6 +36,16 @@ impl Linear {
self.rel.resolve(length) + self.abs
}
+ /// Compose with another linear.
+ ///
+ /// The resulting linear is (self ∘ inner)(x) = self(inner(x)).
+ pub fn compose(self, inner: Self) -> Self {
+ Self {
+ rel: self.rel * inner.rel,
+ abs: self.rel.resolve(inner.abs) + self.abs,
+ }
+ }
+
/// Whether both parts are zero.
pub fn is_zero(self) -> bool {
self.rel.is_zero() && self.abs.is_zero()
@@ -155,10 +165,7 @@ impl Mul<Linear> for f64 {
type Output = Linear;
fn mul(self, other: Linear) -> Linear {
- Linear {
- rel: self * other.rel,
- abs: self * other.abs,
- }
+ other * self
}
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 9c57152a..114e7491 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -15,17 +15,57 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
+use crate::eval::Styles;
use crate::font::FontStore;
use crate::frame::Frame;
-use crate::geom::{Align, Linear, Point, Sides, Spec, Transform};
+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.
@@ -37,6 +77,9 @@ pub struct LayoutContext<'a> {
/// 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,
@@ -50,51 +93,50 @@ impl<'a> LayoutContext<'a> {
images: &mut ctx.images,
#[cfg(feature = "layout-cache")]
layouts: &mut ctx.layouts,
+ styles: ctx.styles.clone(),
#[cfg(feature = "layout-cache")]
level: 0,
}
}
}
-/// 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: {
- let mut state = fxhash::FxHasher64::default();
- self.type_id().hash(&mut state);
- self.hash(&mut state);
- state.finish()
- },
- node: Rc::new(self),
- }
+ ) -> 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 style properties and a precomputed hash.
#[derive(Clone)]
pub struct PackedNode {
+ /// The type-erased node.
node: Rc<dyn Bounds>,
+ /// A precomputed hash for the node.
#[cfg(feature = "layout-cache")]
hash: u64,
+ /// The node's styles.
+ pub styles: Styles,
}
impl PackedNode {
+ /// Check whether the contained node is a specific layout node.
+ pub fn is<T: 'static>(&self) -> bool {
+ self.node.as_any().is::<T>()
+ }
+
/// Try to downcast to a specific layout node.
pub fn downcast<T>(&self) -> Option<&T>
where
@@ -103,6 +145,16 @@ impl PackedNode {
self.node.as_any().downcast_ref()
}
+ /// Style the node with styles from a style map.
+ pub fn styled(mut self, styles: Styles) -> Self {
+ if self.styles.is_empty() {
+ self.styles = styles;
+ } else {
+ self.styles.apply(&styles);
+ }
+ self
+ }
+
/// Force a size for this node.
pub fn sized(self, sizing: Spec<Option<Linear>>) -> Self {
if sizing.any(Option::is_some) {
@@ -156,12 +208,20 @@ impl Layout for PackedNode {
regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> {
#[cfg(not(feature = "layout-cache"))]
- return self.node.layout(ctx, regions);
+ return self.layout_impl(ctx, regions);
+
+ #[cfg(feature = "layout-cache")]
+ let hash = {
+ let mut state = fxhash::FxHasher64::default();
+ self.hash(&mut state);
+ ctx.styles.hash(&mut state);
+ state.finish()
+ };
#[cfg(feature = "layout-cache")]
- ctx.layouts.get(self.hash, regions).unwrap_or_else(|| {
+ ctx.layouts.get(hash, regions).unwrap_or_else(|| {
ctx.level += 1;
- let frames = self.node.layout(ctx, regions);
+ let frames = self.layout_impl(ctx, regions);
ctx.level -= 1;
let entry = FramesEntry::new(frames.clone(), ctx.level);
@@ -177,7 +237,7 @@ impl Layout for PackedNode {
panic!("constraints did not match regions they were created for");
}
- ctx.layouts.insert(self.hash, entry);
+ ctx.layouts.insert(hash, entry);
frames
})
}
@@ -190,30 +250,74 @@ impl Layout for PackedNode {
}
}
-impl Hash for PackedNode {
- fn hash<H: Hasher>(&self, _state: &mut H) {
- #[cfg(feature = "layout-cache")]
- _state.write_u64(self.hash);
- #[cfg(not(feature = "layout-cache"))]
- unimplemented!()
+impl PackedNode {
+ /// Layout the node without checking the cache.
+ fn layout_impl(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ let new = self.styles.chain(&ctx.styles);
+ let prev = std::mem::replace(&mut ctx.styles, new);
+ let frames = self.node.layout(ctx, regions);
+ ctx.styles = prev;
+ frames
+ }
+}
+
+impl Default for PackedNode {
+ fn default() -> Self {
+ EmptyNode.pack()
}
}
impl Debug for PackedNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ self.styles.fmt(f)?;
+ }
self.node.fmt(f)
}
}
+impl PartialEq for PackedNode {
+ fn eq(&self, other: &Self) -> bool {
+ Rc::as_ptr(&self.node) as *const () == Rc::as_ptr(&other.node) as *const ()
+ }
+}
+
+impl Hash for PackedNode {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // Hash the node.
+ #[cfg(feature = "layout-cache")]
+ state.write_u64(self.hash);
+ #[cfg(not(feature = "layout-cache"))]
+ state.write_u64(self.hash64());
+
+ // Hash the styles.
+ self.styles.hash(state);
+ }
+}
+
trait Bounds: Layout + Debug + 'static {
fn as_any(&self) -> &dyn Any;
+ fn hash64(&self) -> u64;
}
impl<T> Bounds for T
where
- T: Layout + Debug + 'static,
+ T: Layout + Hash + Debug + 'static,
{
fn as_any(&self) -> &dyn Any {
self
}
+
+ fn hash64(&self) -> u64 {
+ // Also hash the TypeId since nodes with different types but
+ // equal data should be different.
+ let mut state = fxhash::FxHasher64::default();
+ self.type_id().hash(&mut state);
+ self.hash(&mut state);
+ state.finish()
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index b05a57b3..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]
@@ -44,22 +46,19 @@ pub mod library;
pub mod loading;
pub mod parse;
pub mod source;
-pub mod style;
pub mod syntax;
use std::rc::Rc;
use crate::diag::TypResult;
-use crate::eval::{Module, Scope};
+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};
-use crate::style::Style;
/// The core context which holds the loader, configuration and cached artifacts.
pub struct Context {
@@ -76,8 +75,8 @@ pub struct Context {
pub layouts: LayoutCache,
/// The standard library scope.
std: Scope,
- /// The default style.
- style: Style,
+ /// The default styles.
+ styles: Styles,
}
impl Context {
@@ -96,21 +95,21 @@ impl Context {
&self.std
}
- /// A read-only reference to the style.
- pub fn style(&self) -> &Style {
- &self.style
+ /// A read-only reference to the styles.
+ pub fn styles(&self) -> &Styles {
+ &self.styles
}
/// 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.template.to_document(&self.style))
+ 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.
@@ -119,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)
}
@@ -136,7 +136,7 @@ impl Context {
/// This struct is created by [`Context::builder`].
pub struct ContextBuilder {
std: Option<Scope>,
- style: Option<Style>,
+ styles: Option<Styles>,
#[cfg(feature = "layout-cache")]
policy: EvictionPolicy,
#[cfg(feature = "layout-cache")]
@@ -151,9 +151,9 @@ impl ContextBuilder {
self
}
- /// The initial properties for page size, font selection and so on.
- pub fn style(mut self, style: Style) -> Self {
- self.style = Some(style);
+ /// The default properties for page size, font selection and so on.
+ pub fn styles(mut self, styles: Styles) -> Self {
+ self.styles = Some(styles);
self
}
@@ -185,7 +185,7 @@ impl ContextBuilder {
#[cfg(feature = "layout-cache")]
layouts: LayoutCache::new(self.policy, self.max_size),
std: self.std.unwrap_or_else(library::new),
- style: self.style.unwrap_or_default(),
+ styles: self.styles.unwrap_or_default(),
}
}
}
@@ -194,7 +194,7 @@ impl Default for ContextBuilder {
fn default() -> Self {
Self {
std: None,
- style: None,
+ styles: None,
#[cfg(feature = "layout-cache")]
policy: EvictionPolicy::default(),
#[cfg(feature = "layout-cache")]
diff --git a/src/library/align.rs b/src/library/align.rs
index 18920369..c16277f6 100644
--- a/src/library/align.rs
+++ b/src/library/align.rs
@@ -1,29 +1,19 @@
use super::prelude::*;
+use super::ParNode;
/// `align`: Configure the alignment along the layouting axes.
pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- castable! {
- Spec<Option<Align>>,
- Expected: "1d or 2d alignment",
- @align: Align => {
- let mut aligns = Spec::default();
- aligns.set(align.axis(), Some(*align));
- aligns
- },
- @aligns: Spec<Align> => aligns.map(Some),
+ let aligns: Spec<_> = args.expect("alignment")?;
+ let body: Node = args.expect("body")?;
+ let mut styles = Styles::new();
+ if let Some(align) = aligns.x {
+ styles.set(ParNode::ALIGN, align);
}
- let aligns = args.expect::<Spec<_>>("alignment")?;
- let body = args.expect::<Template>("body")?;
- Ok(Value::Template(Template::from_block(move |style| {
- let mut style = style.clone();
- if let Some(x) = aligns.x {
- style.par_mut().align = x;
- }
-
- body.pack(&style).aligned(aligns)
- })))
+ Ok(Value::block(
+ body.into_block().styled(styles).aligned(aligns),
+ ))
}
/// A node that aligns its child.
@@ -67,3 +57,19 @@ impl Layout for AlignNode {
frames
}
}
+
+dynamic! {
+ Align: "alignment",
+}
+
+castable! {
+ Spec<Option<Align>>,
+ Expected: "1d or 2d alignment",
+ @align: Align => {
+ let mut aligns = Spec::default();
+ aligns.set(align.axis(), Some(*align));
+ aligns
+ },
+ @aligns: Spec<Align> => aligns.map(Some),
+
+}
diff --git a/src/library/deco.rs b/src/library/deco.rs
deleted file mode 100644
index cb065689..00000000
--- a/src/library/deco.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-use super::prelude::*;
-use crate::util::EcoString;
-
-/// `strike`: Typeset striken-through text.
-pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Strikethrough)
-}
-
-/// `underline`: Typeset underlined text.
-pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Underline)
-}
-
-/// `overline`: Typeset text with an overline.
-pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Overline)
-}
-
-fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
- let stroke = args.named("stroke")?.or_else(|| args.find());
- let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
- let offset = args.named("offset")?;
- let extent = args.named("extent")?.unwrap_or_default();
- let body: Template = args.expect("body")?;
- Ok(Value::Template(body.decorate(Decoration::Line(
- LineDecoration { kind, stroke, thickness, offset, extent },
- ))))
-}
-
-/// `link`: Typeset text as a link.
-pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let url = args.expect::<EcoString>("url")?;
- let body = args.find().unwrap_or_else(|| {
- let mut template = Template::new();
- template.text(url.trim_start_matches("mailto:").trim_start_matches("tel:"));
- template
- });
-
- Ok(Value::Template(body.decorate(Decoration::Link(url))))
-}
-
-/// A decoration for a frame.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub enum Decoration {
- /// A link to an external resource.
- Link(EcoString),
- /// An underline/strikethrough/overline decoration.
- Line(LineDecoration),
-}
-
-impl Decoration {
- /// Apply a decoration to a child's frame.
- pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
- match self {
- Decoration::Link(href) => {
- let link = Element::Link(href.to_string(), frame.size);
- frame.push(Point::zero(), link);
- }
- Decoration::Line(line) => {
- line.apply(ctx, frame);
- }
- }
- }
-}
-
-/// Defines a line that is positioned over, under or on top of text.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct LineDecoration {
- /// The kind of line.
- pub kind: LineKind,
- /// Stroke color of the line, defaults to the text color if `None`.
- pub stroke: Option<Paint>,
- /// Thickness of the line's strokes (dependent on scaled font size), read
- /// from the font tables if `None`.
- pub thickness: Option<Linear>,
- /// Position of the line relative to the baseline (dependent on scaled font
- /// size), read from the font tables if `None`.
- pub offset: Option<Linear>,
- /// Amount that the line will be longer or shorter than its associated text
- /// (dependent on scaled font size).
- pub extent: Linear,
-}
-
-/// The kind of line decoration.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum LineKind {
- /// A line under text.
- Underline,
- /// A line through text.
- Strikethrough,
- /// A line over text.
- Overline,
-}
-
-impl LineDecoration {
- /// Apply a line decoration to a all text elements in a frame.
- pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
- for i in 0 .. frame.elements.len() {
- let (pos, child) = &frame.elements[i];
- if let Element::Text(text) = child {
- let face = ctx.fonts.get(text.face_id);
- let metrics = match self.kind {
- LineKind::Underline => face.underline,
- LineKind::Strikethrough => face.strikethrough,
- LineKind::Overline => face.overline,
- };
-
- let thickness = self
- .thickness
- .map(|s| s.resolve(text.size))
- .unwrap_or(metrics.thickness.to_length(text.size));
-
- let stroke = Stroke {
- paint: self.stroke.unwrap_or(text.fill),
- thickness,
- };
-
- let offset = self
- .offset
- .map(|s| s.resolve(text.size))
- .unwrap_or(-metrics.position.to_length(text.size));
-
- let extent = self.extent.resolve(text.size);
-
- let subpos = Point::new(pos.x - extent, pos.y + offset);
- let target = Point::new(text.width + 2.0 * extent, Length::zero());
- let shape = Shape::stroked(Geometry::Line(target), stroke);
- frame.push(subpos, Element::Shape(shape));
- }
- }
- }
-}
diff --git a/src/library/document.rs b/src/library/document.rs
deleted file mode 100644
index fe01d2df..00000000
--- a/src/library/document.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-use super::prelude::*;
-use super::PageNode;
-
-/// The root layout node, a document consisting of top-level page runs.
-#[derive(Debug, Hash)]
-pub struct DocumentNode {
- /// The page runs.
- pub pages: 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.pages.iter().flat_map(|node| node.layout(ctx)).collect()
- }
-}
diff --git a/src/library/flow.rs b/src/library/flow.rs
index 98b518b7..cfa761b6 100644
--- a/src/library/flow.rs
+++ b/src/library/flow.rs
@@ -1,50 +1,14 @@
use std::fmt::{self, Debug, Formatter};
use super::prelude::*;
-use super::{AlignNode, ParNode, PlacedNode, Spacing};
-
-/// `flow`: A vertical flow of paragraphs and other layout nodes.
-pub fn flow(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- enum Child {
- Spacing(Spacing),
- Any(Template),
- }
-
- castable! {
- Child,
- Expected: "linear, fractional or template",
- Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
- Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Template(v) => Self::Any(v),
- }
-
- let children: Vec<Child> = args.all().collect();
-
- Ok(Value::Template(Template::from_block(move |style| {
- let children = children
- .iter()
- .map(|child| match child {
- Child::Spacing(spacing) => FlowChild::Spacing(*spacing),
- Child::Any(node) => FlowChild::Node(node.pack(style)),
- })
- .collect();
-
- FlowNode { children }
- })))
-}
+use super::{AlignNode, ParNode, PlacedNode, SpacingKind, SpacingNode, TextNode};
/// A vertical flow of content consisting of paragraphs and other layout nodes.
///
/// This node is reponsible for layouting both the top-level content flow and
/// the contents of boxes.
-#[derive(Debug, Hash)]
-pub struct FlowNode {
- /// The children that compose the flow. There are different kinds of
- /// children for different purposes.
- pub children: Vec<FlowChild>,
-}
+#[derive(Hash)]
+pub struct FlowNode(pub Vec<FlowChild>);
impl Layout for FlowNode {
fn layout(
@@ -56,19 +20,54 @@ impl Layout for FlowNode {
}
}
+impl Debug for FlowNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Flow ")?;
+ f.debug_list().entries(&self.0).finish()
+ }
+}
+
/// A child of a flow node.
#[derive(Hash)]
pub enum FlowChild {
+ /// A paragraph/block break.
+ Break(Styles),
/// Vertical spacing between other children.
- Spacing(Spacing),
+ Spacing(SpacingNode),
/// An arbitrary node.
Node(PackedNode),
}
+impl FlowChild {
+ /// A reference to the child's styles.
+ pub fn styles(&self) -> &Styles {
+ match self {
+ Self::Break(styles) => styles,
+ Self::Spacing(node) => &node.styles,
+ Self::Node(node) => &node.styles,
+ }
+ }
+
+ /// A mutable reference to the child's styles.
+ pub fn styles_mut(&mut self) -> &mut Styles {
+ match self {
+ Self::Break(styles) => styles,
+ Self::Spacing(node) => &mut node.styles,
+ Self::Node(node) => &mut node.styles,
+ }
+ }
+}
+
impl Debug for FlowChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
- Self::Spacing(spacing) => spacing.fmt(f),
+ Self::Break(styles) => {
+ if f.alternate() {
+ styles.fmt(f)?;
+ }
+ write!(f, "Break")
+ }
+ Self::Spacing(node) => node.fmt(f),
Self::Node(node) => node.fmt(f),
}
}
@@ -118,7 +117,7 @@ impl<'a> FlowLayouter<'a> {
regions.expand.y = false;
Self {
- children: &flow.children,
+ children: &flow.0,
expand,
full,
regions,
@@ -132,15 +131,21 @@ impl<'a> FlowLayouter<'a> {
/// Layout all children.
fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
for child in self.children {
- match *child {
- FlowChild::Spacing(Spacing::Linear(v)) => {
- self.layout_absolute(v);
- }
- FlowChild::Spacing(Spacing::Fractional(v)) => {
- self.items.push(FlowItem::Fractional(v));
- self.fr += v;
+ match child {
+ FlowChild::Break(styles) => {
+ let chain = styles.chain(&ctx.styles);
+ let em = chain.get(TextNode::SIZE).abs;
+ let amount = chain.get(ParNode::SPACING).resolve(em);
+ self.layout_absolute(amount.into());
}
- FlowChild::Node(ref node) => {
+ FlowChild::Spacing(node) => match node.kind {
+ SpacingKind::Linear(v) => self.layout_absolute(v),
+ SpacingKind::Fractional(v) => {
+ self.items.push(FlowItem::Fractional(v));
+ self.fr += v;
+ }
+ },
+ FlowChild::Node(node) => {
if self.regions.is_full() {
self.finish_region();
}
@@ -166,18 +171,25 @@ impl<'a> FlowLayouter<'a> {
/// Layout a node.
fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode) {
+ // Placed nodes that are out of flow produce placed items which aren't
+ // aligned later.
if let Some(placed) = node.downcast::<PlacedNode>() {
- let frame = node.layout(ctx, &self.regions).remove(0);
if placed.out_of_flow() {
+ let frame = node.layout(ctx, &self.regions).remove(0);
self.items.push(FlowItem::Placed(frame.item));
return;
}
}
+ // How to align the node.
let aligns = Spec::new(
// For non-expanding paragraphs it is crucial that we align the
// whole paragraph according to its internal alignment.
- node.downcast::<ParNode>().map_or(Align::Left, |par| par.align),
+ if node.is::<ParNode>() {
+ node.styles.chain(&ctx.styles).get(ParNode::ALIGN)
+ } else {
+ Align::Left
+ },
// Vertical align node alignment is respected by the flow node.
node.downcast::<AlignNode>()
.and_then(|aligned| aligned.aligns.y)
@@ -235,7 +247,7 @@ impl<'a> FlowLayouter<'a> {
output.push_frame(pos, frame);
}
FlowItem::Placed(frame) => {
- output.push_frame(Point::with_y(offset), frame);
+ output.push_frame(Point::zero(), frame);
}
}
}
diff --git a/src/library/grid.rs b/src/library/grid.rs
index 7a9d88c3..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())
@@ -39,15 +39,11 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
row_gutter.unwrap_or(base_gutter),
);
- let children: Vec<Template> = args.all().collect();
-
- Ok(Value::Template(Template::from_block(move |style| {
- GridNode {
- tracks: tracks.clone(),
- gutter: gutter.clone(),
- children: children.iter().map(|child| child.pack(style)).collect(),
- }
- })))
+ Ok(Value::block(GridNode {
+ tracks,
+ gutter,
+ children: args.all().map(Node::into_block).collect(),
+ }))
}
/// A node that arranges its children in a grid.
diff --git a/src/library/heading.rs b/src/library/heading.rs
new file mode 100644
index 00000000..96ff2688
--- /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(args: &mut Args, styles: &mut Styles) -> 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/image.rs b/src/library/image.rs
index 08ed5069..efb246a1 100644
--- a/src/library/image.rs
+++ b/src/library/image.rs
@@ -1,6 +1,7 @@
use std::io;
use super::prelude::*;
+use super::LinkNode;
use crate::diag::Error;
use crate::image::ImageId;
@@ -20,9 +21,9 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})
})?;
- Ok(Value::Template(Template::from_inline(move |_| {
- ImageNode { id, fit }.pack().sized(Spec::new(width, height))
- })))
+ Ok(Value::inline(
+ ImageNode { id, fit }.pack().sized(Spec::new(width, height)),
+ ))
}
/// An image node.
@@ -85,6 +86,11 @@ impl Layout for ImageNode {
frame.clip();
}
+ // Apply link if it exists.
+ if let Some(url) = ctx.styles.get_ref(LinkNode::URL) {
+ frame.link(url);
+ }
+
vec![frame.constrain(Constraints::tight(regions))]
}
}
diff --git a/src/library/link.rs b/src/library/link.rs
new file mode 100644
index 00000000..40604a62
--- /dev/null
+++ b/src/library/link.rs
@@ -0,0 +1,28 @@
+use super::prelude::*;
+use crate::util::EcoString;
+
+/// `link`: Link text or other elements.
+pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let url: String = args.expect::<EcoString>("url")?.into();
+ let body = args.find().unwrap_or_else(|| {
+ let mut text = url.as_str();
+ for prefix in ["mailto:", "tel:"] {
+ text = text.trim_start_matches(prefix);
+ }
+ Node::Text(text.into())
+ });
+
+ Ok(Value::Node(
+ body.styled(Styles::one(LinkNode::URL, Some(url))),
+ ))
+}
+
+/// Host for link styles.
+#[derive(Debug, Hash)]
+pub struct LinkNode;
+
+#[properties]
+impl LinkNode {
+ /// An URL to link to.
+ pub const URL: Option<String> = None;
+}
diff --git a/src/library/list.rs b/src/library/list.rs
new file mode 100644
index 00000000..25eb3600
--- /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(args: &mut Args, styles: &mut Styles) -> 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 d60a13ea..b2dd0dbe 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -4,11 +4,12 @@
//! definitions.
mod align;
-mod deco;
-mod document;
mod flow;
mod grid;
+mod heading;
mod image;
+mod link;
+mod list;
mod pad;
mod page;
mod par;
@@ -23,10 +24,15 @@ mod utility;
/// Helpful imports for creating library functionality.
mod prelude {
+ pub use std::fmt::{self, Debug, Formatter};
pub use std::rc::Rc;
+ pub use typst_macros::properties;
+
pub use crate::diag::{At, TypResult};
- pub use crate::eval::{Args, EvalContext, Smart, Template, Value};
+ pub use crate::eval::{
+ Args, Construct, EvalContext, Node, Property, Set, Smart, Styles, Value,
+ };
pub use crate::frame::*;
pub use crate::geom::*;
pub use crate::layout::*;
@@ -36,10 +42,11 @@ mod prelude {
pub use self::image::*;
pub use align::*;
-pub use deco::*;
-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::*;
@@ -54,28 +61,38 @@ pub use utility::*;
use crate::eval::{Scope, Value};
use crate::geom::*;
-use crate::style::FontFamily;
/// Construct a scope containing all standard library definitions.
pub fn new() -> Scope {
let mut std = Scope::new();
- // Text.
- std.def_func("font", font);
- std.def_func("par", par);
+ // Classes.
+ 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.
- std.def_func("page", page);
+ // 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("flow", flow);
std.def_func("stack", stack);
std.def_func("grid", grid);
std.def_func("pad", pad);
@@ -84,15 +101,13 @@ pub fn new() -> Scope {
std.def_func("move", move_);
std.def_func("scale", scale);
std.def_func("rotate", rotate);
-
- // Elements.
std.def_func("image", image);
std.def_func("rect", rect);
std.def_func("square", square);
std.def_func("ellipse", ellipse);
std.def_func("circle", circle);
- // Utility.
+ // Utility functions.
std.def_func("assert", assert);
std.def_func("type", type_);
std.def_func("repr", repr);
@@ -110,14 +125,15 @@ pub fn new() -> Scope {
std.def_func("len", len);
std.def_func("sorted", sorted);
- // Colors.
+ // 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));
std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
- // Arbitrary constants.
+ // Other constants.
std.def_const("ltr", Dir::LTR);
std.def_const("rtl", Dir::RTL);
std.def_const("ttb", Dir::TTB);
@@ -139,17 +155,20 @@ dynamic! {
Dir: "direction",
}
-dynamic! {
- Align: "alignment",
+castable! {
+ Paint,
+ Expected: "color",
+ Value::Color(color) => Paint::Solid(color),
}
-dynamic! {
- FontFamily: "font family",
- Value::Str(string) => Self::Named(string.to_lowercase()),
+castable! {
+ usize,
+ Expected: "non-negative integer",
+ Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?,
}
castable! {
- Paint,
- Expected: "color",
- Value::Color(color) => Paint::Solid(color),
+ String,
+ Expected: "string",
+ Value::Str(string) => string.into(),
}
diff --git a/src/library/pad.rs b/src/library/pad.rs
index 681da73e..75fea2e5 100644
--- a/src/library/pad.rs
+++ b/src/library/pad.rs
@@ -7,7 +7,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let top = args.named("top")?;
let right = args.named("right")?;
let bottom = args.named("bottom")?;
- let body: Template = args.expect("body")?;
+ let body: Node = args.expect("body")?;
let padding = Sides::new(
left.or(all).unwrap_or_default(),
top.or(all).unwrap_or_default(),
@@ -15,9 +15,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
bottom.or(all).unwrap_or_default(),
);
- Ok(Value::Template(Template::from_block(move |style| {
- body.pack(style).padded(padding)
- })))
+ Ok(Value::block(body.into_block().padded(padding)))
}
/// A node that adds padding to its child.
diff --git a/src/library/page.rs b/src/library/page.rs
index 0d29ddb6..0e690770 100644
--- a/src/library/page.rs
+++ b/src/library/page.rs
@@ -1,115 +1,405 @@
-use super::prelude::*;
-use crate::style::{Paper, PaperClass};
-
-/// `page`: Configure pages.
-pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- castable! {
- Paper,
- Expected: "string",
- Value::Str(string) => Paper::from_name(&string).ok_or("unknown paper")?,
- }
-
- let paper = args.named::<Paper>("paper")?.or_else(|| args.find());
- let width = args.named::<Smart<_>>("width")?;
- let height = args.named::<Smart<_>>("height")?;
- let flip = args.named("flip")?;
- let margins = args.named("margins")?;
- let left = args.named("left")?;
- let top = args.named("top")?;
- let right = args.named("right")?;
- let bottom = args.named("bottom")?;
- let fill = args.named("fill")?;
-
- ctx.template.modify(move |style| {
- let page = style.page_mut();
-
- if let Some(paper) = paper {
- page.class = paper.class();
- page.size = paper.size();
- }
+#![allow(unused)]
- if let Some(width) = width {
- page.class = PaperClass::Custom;
- page.size.x = width.unwrap_or(Length::inf());
- }
+use std::fmt::{self, Display, Formatter};
+use std::str::FromStr;
- if let Some(height) = height {
- page.class = PaperClass::Custom;
- page.size.y = height.unwrap_or(Length::inf());
- }
+use super::prelude::*;
+use super::PadNode;
- if flip.unwrap_or(false) {
- std::mem::swap(&mut page.size.x, &mut page.size.y);
- }
+/// `pagebreak`: Start a new page.
+pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
+ Ok(Value::Node(Node::Pagebreak))
+}
- if let Some(margins) = margins {
- page.margins = Sides::splat(margins);
- }
+/// Layouts its child onto one or multiple pages.
+#[derive(Clone, PartialEq, Hash)]
+pub struct PageNode {
+ /// The node producing the content.
+ pub child: PackedNode,
+ /// The page's styles.
+ pub styles: Styles,
+}
- if let Some(left) = left {
- page.margins.left = left;
- }
+#[properties]
+impl PageNode {
+ /// The unflipped width of the page.
+ pub const WIDTH: Smart<Length> = Smart::Custom(Paper::default().width());
+ /// The unflipped height of the page.
+ pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::default().height());
+ /// The class of paper. Defines the default margins.
+ pub const CLASS: PaperClass = Paper::default().class();
+ /// Whether the page is flipped into landscape orientation.
+ pub const FLIPPED: bool = false;
+ /// The left margin.
+ pub const LEFT: Smart<Linear> = Smart::Auto;
+ /// The right margin.
+ pub const RIGHT: Smart<Linear> = Smart::Auto;
+ /// The top margin.
+ pub const TOP: Smart<Linear> = Smart::Auto;
+ /// The bottom margin.
+ pub const BOTTOM: Smart<Linear> = Smart::Auto;
+ /// The page's background color.
+ pub const FILL: Option<Paint> = None;
+}
- if let Some(top) = top {
- page.margins.top = top;
- }
+impl Construct for PageNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::Page(Self {
+ child: args.expect::<Node>("body")?.into_block(),
+ styles: Styles::new(),
+ }))
+ }
+}
- if let Some(right) = right {
- page.margins.right = right;
+impl Set for PageNode {
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
+ styles.set(Self::CLASS, paper.class());
+ styles.set(Self::WIDTH, Smart::Custom(paper.width()));
+ styles.set(Self::HEIGHT, Smart::Custom(paper.height()));
}
- if let Some(bottom) = bottom {
- page.margins.bottom = bottom;
+ if let Some(width) = args.named("width")? {
+ styles.set(Self::CLASS, PaperClass::Custom);
+ styles.set(Self::WIDTH, width);
}
- if let Some(fill) = fill {
- page.fill = fill;
+ if let Some(height) = args.named("height")? {
+ styles.set(Self::CLASS, PaperClass::Custom);
+ styles.set(Self::HEIGHT, height);
}
- });
-
- ctx.template.pagebreak(false);
-
- Ok(Value::None)
-}
-/// `pagebreak`: Start a new page.
-pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.pagebreak(true);
- Ok(Value::Template(template))
-}
+ let margins = args.named("margins")?;
+ 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")?);
-/// Layouts its children onto one or multiple pages.
-#[derive(Debug, Hash)]
-pub struct PageNode {
- /// The size of the page.
- pub size: Size,
- /// The background fill.
- pub fill: Option<Paint>,
- /// The node that produces the actual pages.
- pub child: PackedNode,
+ Ok(())
+ }
}
impl PageNode {
+ /// Style the node with styles from a style map.
+ pub fn styled(mut self, styles: Styles) -> Self {
+ self.styles.apply(&styles);
+ self
+ }
+
/// Layout the page run into a sequence of frames, one per page.
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
+ let prev = ctx.styles.clone();
+ ctx.styles = self.styles.chain(&ctx.styles);
+
// When one of the lengths is infinite the page fits its content along
// that axis.
- let expand = self.size.map(Length::is_finite);
- let regions = Regions::repeat(self.size, self.size, expand);
+ let width = ctx.styles.get(Self::WIDTH).unwrap_or(Length::inf());
+ let height = ctx.styles.get(Self::HEIGHT).unwrap_or(Length::inf());
+ let mut size = Size::new(width, height);
+ if ctx.styles.get(Self::FLIPPED) {
+ std::mem::swap(&mut size.x, &mut size.y);
+ }
+
+ // Determine the margins.
+ let class = ctx.styles.get(Self::CLASS);
+ let default = class.default_margins();
+ let padding = Sides {
+ left: ctx.styles.get(Self::LEFT).unwrap_or(default.left),
+ right: ctx.styles.get(Self::RIGHT).unwrap_or(default.right),
+ top: ctx.styles.get(Self::TOP).unwrap_or(default.top),
+ bottom: ctx.styles.get(Self::BOTTOM).unwrap_or(default.bottom),
+ };
+
+ // Pad the child.
+ let padded = PadNode { child: self.child.clone(), padding }.pack();
// Layout the child.
+ let expand = size.map(Length::is_finite);
+ let regions = Regions::repeat(size, size, expand);
let mut frames: Vec<_> =
- self.child.layout(ctx, &regions).into_iter().map(|c| c.item).collect();
+ padded.layout(ctx, &regions).into_iter().map(|c| c.item).collect();
// Add background fill if requested.
- if let Some(fill) = self.fill {
+ if let Some(fill) = ctx.styles.get(Self::FILL) {
for frame in &mut frames {
let shape = Shape::filled(Geometry::Rect(frame.size), fill);
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
}
}
+ ctx.styles = prev;
frames
}
}
+
+impl Debug for PageNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ self.styles.fmt(f)?;
+ }
+ f.write_str("Page(")?;
+ self.child.fmt(f)?;
+ f.write_str(")")
+ }
+}
+
+/// Specification of a paper.
+#[derive(Debug, Copy, Clone)]
+pub struct Paper {
+ /// The broad class this paper belongs to.
+ class: PaperClass,
+ /// The width of the paper in millimeters.
+ width: f64,
+ /// The height of the paper in millimeters.
+ height: f64,
+}
+
+impl Paper {
+ /// The class of the paper.
+ pub fn class(self) -> PaperClass {
+ self.class
+ }
+
+ /// The width of the paper.
+ pub fn width(self) -> Length {
+ Length::mm(self.width)
+ }
+
+ /// The height of the paper.
+ pub fn height(self) -> Length {
+ Length::mm(self.height)
+ }
+}
+
+impl Default for Paper {
+ fn default() -> Self {
+ Paper::A4
+ }
+}
+
+castable! {
+ Paper,
+ Expected: "string",
+ Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?,
+}
+
+/// Defines default margins for a class of related papers.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum PaperClass {
+ Custom,
+ Base,
+ US,
+ Newspaper,
+ Book,
+}
+
+impl PaperClass {
+ /// The default margins for this page class.
+ fn default_margins(self) -> Sides<Linear> {
+ let f = |r| Relative::new(r).into();
+ let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b));
+ match self {
+ Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842),
+ Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842),
+ Self::US => s(0.1760, 0.1092, 0.1760, 0.0910),
+ Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294),
+ Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965),
+ }
+ }
+}
+
+/// Defines paper constants and a paper parsing implementation.
+macro_rules! papers {
+ ($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => {
+ /// Predefined papers.
+ ///
+ /// Each paper is parsable from its name in kebab-case.
+ impl Paper {
+ $(pub const $var: Self = Self {
+ class: PaperClass::$class,
+ width: $width,
+ height: $height,
+ };)*
+ }
+
+ impl FromStr for Paper {
+ type Err = ParsePaperError;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ match name.to_lowercase().as_str() {
+ $($($pats)* => Ok(Self::$var),)*
+ _ => Err(ParsePaperError),
+ }
+ }
+ }
+
+ /// The error when parsing a [`Paper`] from a string fails.
+ #[derive(Debug, Copy, Clone, Eq, PartialEq)]
+ pub struct ParsePaperError;
+
+ impl Display for ParsePaperError {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad("invalid paper name")
+ }
+ }
+
+ impl std::error::Error for ParsePaperError {}
+ };
+}
+
+// All paper sizes in mm.
+//
+// Resources:
+// - https://papersizes.io/
+// - https://en.wikipedia.org/wiki/Paper_size
+// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
+// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
+papers! {
+ // ---------------------------------------------------------------------- //
+ // ISO 216 A Series
+ (A0: Base, 841.0, 1189.0, "a0")
+ (A1: Base, 594.0, 841.0, "a1")
+ (A2: Base, 420.0, 594.0, "a2")
+ (A3: Base, 297.0, 420.0, "a3")
+ (A4: Base, 210.0, 297.0, "a4")
+ (A5: Base, 148.0, 210.0, "a5")
+ (A6: Book, 105.0, 148.0, "a6")
+ (A7: Base, 74.0, 105.0, "a7")
+ (A8: Base, 52.0, 74.0, "a8")
+ (A9: Base, 37.0, 52.0, "a9")
+ (A10: Base, 26.0, 37.0, "a10")
+ (A11: Base, 18.0, 26.0, "a11")
+
+ // ISO 216 B Series
+ (ISO_B1: Base, 707.0, 1000.0, "iso-b1")
+ (ISO_B2: Base, 500.0, 707.0, "iso-b2")
+ (ISO_B3: Base, 353.0, 500.0, "iso-b3")
+ (ISO_B4: Base, 250.0, 353.0, "iso-b4")
+ (ISO_B5: Book, 176.0, 250.0, "iso-b5")
+ (ISO_B6: Book, 125.0, 176.0, "iso-b6")
+ (ISO_B7: Base, 88.0, 125.0, "iso-b7")
+ (ISO_B8: Base, 62.0, 88.0, "iso-b8")
+
+ // ISO 216 C Series
+ (ISO_C3: Base, 324.0, 458.0, "iso-c3")
+ (ISO_C4: Base, 229.0, 324.0, "iso-c4")
+ (ISO_C5: Base, 162.0, 229.0, "iso-c5")
+ (ISO_C6: Base, 114.0, 162.0, "iso-c6")
+ (ISO_C7: Base, 81.0, 114.0, "iso-c7")
+ (ISO_C8: Base, 57.0, 81.0, "iso-c8")
+
+ // DIN D Series (extension to ISO)
+ (DIN_D3: Base, 272.0, 385.0, "din-d3")
+ (DIN_D4: Base, 192.0, 272.0, "din-d4")
+ (DIN_D5: Base, 136.0, 192.0, "din-d5")
+ (DIN_D6: Base, 96.0, 136.0, "din-d6")
+ (DIN_D7: Base, 68.0, 96.0, "din-d7")
+ (DIN_D8: Base, 48.0, 68.0, "din-d8")
+
+ // SIS (used in academia)
+ (SIS_G5: Base, 169.0, 239.0, "sis-g5")
+ (SIS_E5: Base, 115.0, 220.0, "sis-e5")
+
+ // ANSI Extensions
+ (ANSI_A: Base, 216.0, 279.0, "ansi-a")
+ (ANSI_B: Base, 279.0, 432.0, "ansi-b")
+ (ANSI_C: Base, 432.0, 559.0, "ansi-c")
+ (ANSI_D: Base, 559.0, 864.0, "ansi-d")
+ (ANSI_E: Base, 864.0, 1118.0, "ansi-e")
+
+ // ANSI Architectural Paper
+ (ARCH_A: Base, 229.0, 305.0, "arch-a")
+ (ARCH_B: Base, 305.0, 457.0, "arch-b")
+ (ARCH_C: Base, 457.0, 610.0, "arch-c")
+ (ARCH_D: Base, 610.0, 914.0, "arch-d")
+ (ARCH_E1: Base, 762.0, 1067.0, "arch-e1")
+ (ARCH_E: Base, 914.0, 1219.0, "arch-e")
+
+ // JIS B Series
+ (JIS_B0: Base, 1030.0, 1456.0, "jis-b0")
+ (JIS_B1: Base, 728.0, 1030.0, "jis-b1")
+ (JIS_B2: Base, 515.0, 728.0, "jis-b2")
+ (JIS_B3: Base, 364.0, 515.0, "jis-b3")
+ (JIS_B4: Base, 257.0, 364.0, "jis-b4")
+ (JIS_B5: Base, 182.0, 257.0, "jis-b5")
+ (JIS_B6: Base, 128.0, 182.0, "jis-b6")
+ (JIS_B7: Base, 91.0, 128.0, "jis-b7")
+ (JIS_B8: Base, 64.0, 91.0, "jis-b8")
+ (JIS_B9: Base, 45.0, 64.0, "jis-b9")
+ (JIS_B10: Base, 32.0, 45.0, "jis-b10")
+ (JIS_B11: Base, 22.0, 32.0, "jis-b11")
+
+ // SAC D Series
+ (SAC_D0: Base, 764.0, 1064.0, "sac-d0")
+ (SAC_D1: Base, 532.0, 760.0, "sac-d1")
+ (SAC_D2: Base, 380.0, 528.0, "sac-d2")
+ (SAC_D3: Base, 264.0, 376.0, "sac-d3")
+ (SAC_D4: Base, 188.0, 260.0, "sac-d4")
+ (SAC_D5: Base, 130.0, 184.0, "sac-d5")
+ (SAC_D6: Base, 92.0, 126.0, "sac-d6")
+
+ // ISO 7810 ID
+ (ISO_ID_1: Base, 85.6, 53.98, "iso-id-1")
+ (ISO_ID_2: Base, 74.0, 105.0, "iso-id-2")
+ (ISO_ID_3: Base, 88.0, 125.0, "iso-id-3")
+
+ // ---------------------------------------------------------------------- //
+ // Asia
+ (ASIA_F4: Base, 210.0, 330.0, "asia-f4")
+
+ // Japan
+ (JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4")
+ (JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5")
+ (JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6")
+ (JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4")
+ (JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5")
+ (JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card")
+
+ // China
+ (CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card")
+
+ // Europe
+ (EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card")
+
+ // French Traditional (AFNOR)
+ (FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière")
+ (FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture")
+ (FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition")
+ (FR_RAISIN: Base, 500.0, 650.0, "fr-raisin")
+ (FR_CARRE: Base, 450.0, 560.0, "fr-carré")
+ (FR_JESUS: Base, 560.0, 760.0, "fr-jésus")
+
+ // United Kingdom Imperial
+ (UK_BRIEF: Base, 406.4, 342.9, "uk-brief")
+ (UK_DRAFT: Base, 254.0, 406.4, "uk-draft")
+ (UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap")
+ (UK_QUARTO: Base, 203.2, 254.0, "uk-quarto")
+ (UK_CROWN: Base, 508.0, 381.0, "uk-crown")
+ (UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a")
+ (UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b")
+
+ // Unites States
+ (US_LETTER: US, 215.9, 279.4, "us-letter")
+ (US_LEGAL: US, 215.9, 355.6, "us-legal")
+ (US_TABLOID: US, 279.4, 431.8, "us-tabloid")
+ (US_EXECUTIVE: US, 184.15, 266.7, "us-executive")
+ (US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio")
+ (US_STATEMENT: US, 139.7, 215.9, "us-statement")
+ (US_LEDGER: US, 431.8, 279.4, "us-ledger")
+ (US_OFICIO: US, 215.9, 340.36, "us-oficio")
+ (US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter")
+ (US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal")
+ (US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card")
+ (US_DIGEST: Book, 139.7, 215.9, "us-digest")
+ (US_TRADE: Book, 152.4, 228.6, "us-trade")
+
+ // ---------------------------------------------------------------------- //
+ // Other
+ (NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact")
+ (NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner")
+ (NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet")
+ (PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9")
+ (PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3")
+}
diff --git a/src/library/par.rs b/src/library/par.rs
index 6abfa7af..26280d8e 100644
--- a/src/library/par.rs
+++ b/src/library/par.rs
@@ -6,75 +6,81 @@ use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
use super::prelude::*;
-use super::{shape, Decoration, ShapedText, Spacing};
-use crate::style::TextStyle;
+use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode};
use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
-/// `par`: Configure paragraphs.
-pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let spacing = args.named("spacing")?;
- let leading = args.named("leading")?;
-
- let mut dir =
- args.named("lang")?
- .map(|iso: EcoString| match iso.to_lowercase().as_str() {
- "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
- "en" | "fr" | "de" => Dir::LTR,
- _ => Dir::LTR,
- });
-
- if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
- if v.axis() != SpecAxis::Horizontal {
- bail!(span, "must be horizontal");
- }
- dir = Some(v);
- }
+/// `parbreak`: Start a new paragraph.
+pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
+ Ok(Value::Node(Node::Parbreak))
+}
- let mut align = None;
- if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? {
- if v.axis() != SpecAxis::Horizontal {
- bail!(span, "must be horizontal");
- }
- align = Some(v);
- }
+/// `linebreak`: Start a new line.
+pub fn linebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
+ Ok(Value::Node(Node::Linebreak))
+}
- ctx.template.modify(move |style| {
- let par = style.par_mut();
+/// A node that arranges its children into a paragraph.
+#[derive(Hash)]
+pub struct ParNode(pub Vec<ParChild>);
- if let Some(dir) = dir {
- par.dir = dir;
- par.align = if dir == Dir::LTR { Align::Left } else { Align::Right };
- }
+#[properties]
+impl ParNode {
+ /// The direction for text and inline objects.
+ pub const DIR: Dir = Dir::LTR;
+ /// How to align text and inline objects in their line.
+ pub const ALIGN: Align = Align::Left;
+ /// The spacing between lines (dependent on scaled font size).
+ pub const LEADING: Linear = Relative::new(0.65).into();
+ /// The spacing between paragraphs (dependent on scaled font size).
+ pub const SPACING: Linear = Relative::new(1.2).into();
+}
- if let Some(align) = align {
- par.align = align;
- }
+impl Construct for ParNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ // Lift to a block so that it doesn't merge with adjacent stuff.
+ Ok(Node::Block(args.expect::<Node>("body")?.into_block()))
+ }
+}
- if let Some(leading) = leading {
- par.leading = leading;
+impl Set for ParNode {
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ let spacing = args.named("spacing")?;
+ let leading = args.named("leading")?;
+
+ let mut dir =
+ args.named("lang")?
+ .map(|iso: EcoString| match iso.to_lowercase().as_str() {
+ "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
+ "en" | "fr" | "de" => Dir::LTR,
+ _ => Dir::LTR,
+ });
+
+ if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
+ if v.axis() != SpecAxis::Horizontal {
+ bail!(span, "must be horizontal");
+ }
+ dir = Some(v);
}
- if let Some(spacing) = spacing {
- par.spacing = spacing;
+ let mut align = None;
+ if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? {
+ if v.axis() != SpecAxis::Horizontal {
+ bail!(span, "must be horizontal");
+ }
+ align = Some(v);
}
- });
- ctx.template.parbreak();
+ if let (Some(dir), None) = (dir, align) {
+ align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
+ }
- Ok(Value::None)
-}
+ styles.set_opt(Self::DIR, dir);
+ styles.set_opt(Self::ALIGN, align);
+ styles.set_opt(Self::LEADING, leading);
+ styles.set_opt(Self::SPACING, spacing);
-/// A node that arranges its children into a paragraph.
-#[derive(Debug, Hash)]
-pub struct ParNode {
- /// The text direction (either LTR or RTL).
- pub dir: Dir,
- /// How to align text in its line.
- pub align: Align,
- /// The spacing to insert between each line.
- pub leading: Length,
- /// The children to be arranged in a paragraph.
- pub children: Vec<ParChild>,
+ Ok(())
+ }
}
impl Layout for ParNode {
@@ -87,7 +93,7 @@ impl Layout for ParNode {
let text = self.collect_text();
// Find out the BiDi embedding levels.
- let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
+ 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.
@@ -123,38 +129,63 @@ impl ParNode {
/// The string representation of each child.
fn strings(&self) -> impl Iterator<Item = &str> {
- self.children.iter().map(|child| match child {
+ self.0.iter().map(|child| match child {
ParChild::Spacing(_) => " ",
- ParChild::Text(ref piece, ..) => piece,
- ParChild::Node(..) => "\u{FFFC}",
- ParChild::Decorate(_) | ParChild::Undecorate => "",
+ ParChild::Text(ref node) => &node.text,
+ ParChild::Node(_) => "\u{FFFC}",
})
}
}
+impl Debug for ParNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Par ")?;
+ f.debug_list().entries(&self.0).finish()
+ }
+}
+
/// A child of a paragraph node.
#[derive(Hash)]
pub enum ParChild {
/// Spacing between other nodes.
- Spacing(Spacing),
+ Spacing(SpacingNode),
/// A run of text and how to align it in its line.
- Text(EcoString, Rc<TextStyle>),
+ Text(TextNode),
/// Any child node and how to align it in its line.
Node(PackedNode),
- /// A decoration that applies until a matching `Undecorate`.
- Decorate(Decoration),
- /// The end of a decoration.
- Undecorate,
+}
+
+impl ParChild {
+ /// Create a text child.
+ pub fn text(text: impl Into<EcoString>, styles: Styles) -> Self {
+ Self::Text(TextNode { text: text.into(), styles })
+ }
+
+ /// A reference to the child's styles.
+ pub fn styles(&self) -> &Styles {
+ match self {
+ Self::Spacing(node) => &node.styles,
+ Self::Text(node) => &node.styles,
+ Self::Node(node) => &node.styles,
+ }
+ }
+
+ /// A mutable reference to the child's styles.
+ pub fn styles_mut(&mut self) -> &mut Styles {
+ match self {
+ Self::Spacing(node) => &mut node.styles,
+ Self::Text(node) => &mut node.styles,
+ Self::Node(node) => &mut node.styles,
+ }
+ }
}
impl Debug for ParChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
- Self::Spacing(v) => write!(f, "Spacing({:?})", v),
- Self::Text(text, _) => write!(f, "Text({:?})", text),
+ Self::Spacing(node) => node.fmt(f),
+ Self::Text(node) => node.fmt(f),
Self::Node(node) => node.fmt(f),
- Self::Decorate(deco) => write!(f, "Decorate({:?})", deco),
- Self::Undecorate => write!(f, "Undecorate"),
}
}
}
@@ -172,8 +203,6 @@ struct ParLayouter<'a> {
items: Vec<ParItem<'a>>,
/// The ranges of the items in `bidi.text`.
ranges: Vec<Range>,
- /// The decorations and the ranges they span.
- decos: Vec<(Range, &'a Decoration)>,
}
/// Range of a substring of text.
@@ -201,22 +230,22 @@ impl<'a> ParLayouter<'a> {
) -> Self {
let mut items = vec![];
let mut ranges = vec![];
- let mut starts = vec![];
- let mut decos = vec![];
// Layout the children and collect them into items.
- for (range, child) in par.ranges().zip(&par.children) {
- match *child {
- ParChild::Spacing(Spacing::Linear(v)) => {
- let resolved = v.resolve(regions.current.x);
- items.push(ParItem::Absolute(resolved));
- ranges.push(range);
- }
- ParChild::Spacing(Spacing::Fractional(v)) => {
- items.push(ParItem::Fractional(v));
- ranges.push(range);
- }
- ParChild::Text(_, ref style) => {
+ for (range, child) in par.ranges().zip(&par.0) {
+ match child {
+ ParChild::Spacing(node) => match node.kind {
+ SpacingKind::Linear(v) => {
+ let resolved = v.resolve(regions.current.x);
+ items.push(ParItem::Absolute(resolved));
+ ranges.push(range);
+ }
+ SpacingKind::Fractional(v) => {
+ items.push(ParItem::Fractional(v));
+ ranges.push(range);
+ }
+ },
+ ParChild::Text(node) => {
// TODO: Also split by language and script.
let mut cursor = range.start;
for (level, group) in bidi.levels[range].group_by_key(|&lvl| lvl) {
@@ -224,41 +253,27 @@ impl<'a> ParLayouter<'a> {
cursor += group.len();
let subrange = start .. cursor;
let text = &bidi.text[subrange.clone()];
- let shaped = shape(ctx, text, style, level.dir());
+ let styles = node.styles.chain(&ctx.styles);
+ let shaped = shape(&mut ctx.fonts, text, styles, level.dir());
items.push(ParItem::Text(shaped));
ranges.push(subrange);
}
}
- ParChild::Node(ref node) => {
+ ParChild::Node(node) => {
let size = Size::new(regions.current.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false));
let frame = node.layout(ctx, &pod).remove(0);
items.push(ParItem::Frame(Rc::take(frame.item)));
ranges.push(range);
}
- ParChild::Decorate(ref deco) => {
- starts.push((range.start, deco));
- }
- ParChild::Undecorate => {
- if let Some((start, deco)) = starts.pop() {
- decos.push((start .. range.end, deco));
- }
- }
}
}
- for (start, deco) in starts {
- decos.push((start .. bidi.text.len(), deco));
- }
+ let em = ctx.styles.get(TextNode::SIZE).abs;
+ let align = ctx.styles.get(ParNode::ALIGN);
+ let leading = ctx.styles.get(ParNode::LEADING).resolve(em);
- Self {
- align: par.align,
- leading: par.leading,
- bidi,
- items,
- ranges,
- decos,
- }
+ Self { align, leading, bidi, items, ranges }
}
/// Find first-fit line breaks and build the paragraph.
@@ -430,7 +445,7 @@ impl<'a> LineLayout<'a> {
// empty string.
if !range.is_empty() || rest.is_empty() {
// Reshape that part.
- let reshaped = shaped.reshape(ctx, range);
+ let reshaped = shaped.reshape(&mut ctx.fonts, range);
last = Some(ParItem::Text(reshaped));
}
@@ -451,7 +466,7 @@ impl<'a> LineLayout<'a> {
// Reshape if necessary.
if range.len() < shaped.text.len() {
if !range.is_empty() {
- let reshaped = shaped.reshape(ctx, range);
+ let reshaped = shaped.reshape(&mut ctx.fonts, range);
first = Some(ParItem::Text(reshaped));
}
@@ -504,28 +519,19 @@ impl<'a> LineLayout<'a> {
let mut output = Frame::new(size);
output.baseline = Some(self.baseline);
- for (range, item) in self.reordered() {
- let mut position = |mut frame: Frame| {
- // Decorate.
- for (deco_range, deco) in &self.par.decos {
- if deco_range.contains(&range.start) {
- deco.apply(ctx, &mut frame);
- }
- }
-
+ for item in self.reordered() {
+ let mut position = |frame: Frame| {
let x = offset + self.par.align.resolve(remaining);
let y = self.baseline - frame.baseline();
offset += frame.size.x;
-
- // Add to the line's frame.
output.merge_frame(Point::new(x, y), frame);
};
- match *item {
- ParItem::Absolute(v) => offset += v,
+ match item {
+ ParItem::Absolute(v) => offset += *v,
ParItem::Fractional(v) => offset += v.resolve(self.fr, remaining),
- ParItem::Text(ref shaped) => position(shaped.build()),
- ParItem::Frame(ref frame) => position(frame.clone()),
+ ParItem::Text(shaped) => position(shaped.build(&ctx.fonts)),
+ ParItem::Frame(frame) => position(frame.clone()),
}
}
@@ -533,7 +539,7 @@ impl<'a> LineLayout<'a> {
}
/// Iterate through the line's items in visual order.
- fn reordered(&self) -> impl Iterator<Item = (Range, &ParItem<'a>)> {
+ fn reordered(&self) -> impl Iterator<Item = &ParItem<'a>> {
// The bidi crate doesn't like empty lines.
let (levels, runs) = if !self.line.is_empty() {
// Find the paragraph that contains the line.
@@ -548,7 +554,7 @@ impl<'a> LineLayout<'a> {
// Compute the reordered ranges in visual order (left to right).
self.par.bidi.visual_runs(para, self.line.clone())
} else {
- <_>::default()
+ (vec![], vec![])
};
runs.into_iter()
@@ -565,7 +571,7 @@ impl<'a> LineLayout<'a> {
Either::Right(range.rev())
}
})
- .map(move |idx| (self.ranges[idx].clone(), self.get(idx).unwrap()))
+ .map(move |idx| self.get(idx).unwrap())
}
/// Find the index of the item whose range contains the `text_offset`.
diff --git a/src/library/placed.rs b/src/library/placed.rs
index 722e0035..589a299b 100644
--- a/src/library/placed.rs
+++ b/src/library/placed.rs
@@ -6,27 +6,22 @@ pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let aligns = args.find().unwrap_or(Spec::new(Some(Align::Left), None));
let tx = args.named("dx")?.unwrap_or_default();
let ty = args.named("dy")?.unwrap_or_default();
- let body: Template = args.expect("body")?;
- Ok(Value::Template(Template::from_block(move |style| {
- PlacedNode {
- child: body.pack(style).moved(Point::new(tx, ty)).aligned(aligns),
- }
- })))
+ let body: Node = args.expect("body")?;
+ Ok(Value::block(PlacedNode(
+ body.into_block().moved(Point::new(tx, ty)).aligned(aligns),
+ )))
}
/// A node that places its child absolutely.
#[derive(Debug, Hash)]
-pub struct PlacedNode {
- /// The node to be placed.
- pub child: PackedNode,
-}
+pub struct PlacedNode(pub PackedNode);
impl PlacedNode {
/// Whether this node wants to be placed relative to its its parent's base
/// origin. instead of relative to the parent's current flow/cursor
/// position.
pub fn out_of_flow(&self) -> bool {
- self.child
+ self.0
.downcast::<AlignNode>()
.map_or(false, |node| node.aligns.y.is_some())
}
@@ -48,7 +43,7 @@ impl Layout for PlacedNode {
Regions::one(regions.base, regions.base, expand)
};
- let mut frames = self.child.layout(ctx, &pod);
+ let mut frames = self.0.layout(ctx, &pod);
let Constrained { item: frame, cts } = &mut frames[0];
// If expansion is off, zero all sizes so that we don't take up any
@@ -56,13 +51,6 @@ impl Layout for PlacedNode {
let target = regions.expand.select(regions.current, Size::zero());
Rc::make_mut(frame).resize(target, Align::LEFT_TOP);
- // Place relative to parent's base origin by offsetting our elements by
- // the negative cursor position.
- if out_of_flow {
- let offset = (regions.current - regions.base).to_point();
- Rc::make_mut(frame).translate(offset);
- }
-
// Set base constraint because our pod size is base and exact
// constraints if we needed to expand or offset.
*cts = Constraints::new(regions.expand);
diff --git a/src/library/shape.rs b/src/library/shape.rs
index 61c0d6e3..a9c9b333 100644
--- a/src/library/shape.rs
+++ b/src/library/shape.rs
@@ -1,6 +1,7 @@
use std::f64::consts::SQRT_2;
use super::prelude::*;
+use super::LinkNode;
/// `rect`: A rectangle with optional content.
pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
@@ -76,20 +77,15 @@ fn shape_impl(
}
// The shape's contents.
- let body = args.find::<Template>();
-
- Ok(Value::Template(Template::from_inline(move |style| {
- ShapeNode {
- kind,
- fill,
- stroke,
- child: body
- .as_ref()
- .map(|body| body.pack(style).padded(Sides::splat(padding))),
- }
- .pack()
- .sized(Spec::new(width, height))
- })))
+ let child = args
+ .find()
+ .map(|body: Node| body.into_block().padded(Sides::splat(padding)));
+
+ Ok(Value::inline(
+ ShapeNode { kind, fill, stroke, child }
+ .pack()
+ .sized(Spec::new(width, height)),
+ ))
}
/// Places its child into a sizable and fillable shape.
@@ -154,9 +150,10 @@ impl Layout for ShapeNode {
frames = vec![Frame::new(size).constrain(Constraints::tight(regions))];
}
+ let frame = Rc::make_mut(&mut frames[0].item);
+
// Add fill and/or stroke.
if self.fill.is_some() || self.stroke.is_some() {
- let frame = Rc::make_mut(&mut frames[0].item);
let geometry = match self.kind {
ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size),
ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size),
@@ -171,6 +168,11 @@ impl Layout for ShapeNode {
frame.prepend(Point::zero(), Element::Shape(shape));
}
+ // Apply link if it exists.
+ if let Some(url) = ctx.styles.get_ref(LinkNode::URL) {
+ frame.link(url);
+ }
+
frames
}
}
diff --git a/src/library/sized.rs b/src/library/sized.rs
index dfdc721d..6d677ca8 100644
--- a/src/library/sized.rs
+++ b/src/library/sized.rs
@@ -4,18 +4,16 @@ use super::prelude::*;
pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let width = args.named("width")?;
let height = args.named("height")?;
- let body: Template = args.find().unwrap_or_default();
- Ok(Value::Template(Template::from_inline(move |style| {
- body.pack(style).sized(Spec::new(width, height))
- })))
+ let body: Node = args.find().unwrap_or_default();
+ Ok(Value::inline(
+ body.into_block().sized(Spec::new(width, height)),
+ ))
}
/// `block`: Place content into the flow.
pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let body: Template = args.find().unwrap_or_default();
- Ok(Value::Template(Template::from_block(move |style| {
- body.pack(style)
- })))
+ let body: Node = args.find().unwrap_or_default();
+ Ok(Value::block(body.into_block()))
}
/// A node that sizes its child.
diff --git a/src/library/spacing.rs b/src/library/spacing.rs
index 59911dc7..b5ecce69 100644
--- a/src/library/spacing.rs
+++ b/src/library/spacing.rs
@@ -2,21 +2,41 @@ use super::prelude::*;
/// `h`: Horizontal spacing.
pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.spacing(SpecAxis::Horizontal, args.expect("spacing")?);
- Ok(Value::Template(template))
+ Ok(Value::Node(Node::Spacing(
+ SpecAxis::Horizontal,
+ args.expect("spacing")?,
+ )))
}
/// `v`: Vertical spacing.
pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.spacing(SpecAxis::Vertical, args.expect("spacing")?);
- Ok(Value::Template(template))
+ Ok(Value::Node(Node::Spacing(
+ SpecAxis::Vertical,
+ args.expect("spacing")?,
+ )))
+}
+
+/// Explicit spacing in a flow or paragraph.
+#[derive(Hash)]
+pub struct SpacingNode {
+ /// The kind of spacing.
+ pub kind: SpacingKind,
+ /// The spacing's styles.
+ pub styles: Styles,
+}
+
+impl Debug for SpacingNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ self.styles.fmt(f)?;
+ }
+ write!(f, "{:?}", self.kind)
+ }
}
/// Kinds of spacing.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Spacing {
+pub enum SpacingKind {
/// A length stated in absolute values and/or relative to the parent's size.
Linear(Linear),
/// A length that is the fraction of the remaining free space in the parent.
@@ -24,7 +44,7 @@ pub enum Spacing {
}
castable! {
- Spacing,
+ SpacingKind,
Expected: "linear or fractional",
Value::Length(v) => Self::Linear(v.into()),
Value::Relative(v) => Self::Linear(v.into()),
diff --git a/src/library/stack.rs b/src/library/stack.rs
index 2b1371ab..8a1f0fd5 100644
--- a/src/library/stack.rs
+++ b/src/library/stack.rs
@@ -1,54 +1,31 @@
use std::fmt::{self, Debug, Formatter};
use super::prelude::*;
-use super::{AlignNode, Spacing};
+use super::{AlignNode, SpacingKind, SpacingNode};
/// `stack`: Stack children along an axis.
pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- enum Child {
- Spacing(Spacing),
- Any(Template),
- }
-
- castable! {
- Child,
- Expected: "linear, fractional or template",
- Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
- Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Template(v) => Self::Any(v),
- }
-
let dir = args.named("dir")?.unwrap_or(Dir::TTB);
let spacing = args.named("spacing")?;
- let list: Vec<Child> = args.all().collect();
- Ok(Value::Template(Template::from_block(move |style| {
- let mut children = vec![];
- let mut delayed = None;
+ let mut children = vec![];
+ let mut deferred = None;
- // Build the list of stack children.
- for child in &list {
- match child {
- Child::Spacing(v) => {
- children.push(StackChild::Spacing(*v));
- delayed = None;
- }
- Child::Any(child) => {
- if let Some(v) = delayed {
- children.push(StackChild::Spacing(v));
- }
-
- let node = child.pack(style);
- children.push(StackChild::Node(node));
- delayed = spacing;
+ // Build the list of stack children.
+ for child in args.all() {
+ match child {
+ StackChild::Spacing(_) => deferred = None,
+ StackChild::Node(_) => {
+ if let Some(v) = deferred {
+ children.push(StackChild::spacing(v));
}
+ deferred = spacing;
}
}
+ children.push(child);
+ }
- StackNode { dir, children }
- })))
+ Ok(Value::block(StackNode { dir, children }))
}
/// A node that stacks its children.
@@ -74,20 +51,37 @@ impl Layout for StackNode {
#[derive(Hash)]
pub enum StackChild {
/// Spacing between other nodes.
- Spacing(Spacing),
+ Spacing(SpacingNode),
/// An arbitrary node.
Node(PackedNode),
}
+impl StackChild {
+ /// Create a spacing node from a spacing kind.
+ pub fn spacing(kind: SpacingKind) -> Self {
+ Self::Spacing(SpacingNode { kind, styles: Styles::new() })
+ }
+}
+
impl Debug for StackChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
- Self::Spacing(spacing) => spacing.fmt(f),
+ Self::Spacing(node) => node.fmt(f),
Self::Node(node) => node.fmt(f),
}
}
}
+castable! {
+ StackChild,
+ Expected: "linear, fractional or template",
+ Value::Length(v) => Self::spacing(SpacingKind::Linear(v.into())),
+ Value::Relative(v) => Self::spacing(SpacingKind::Linear(v.into())),
+ Value::Linear(v) => Self::spacing(SpacingKind::Linear(v)),
+ Value::Fractional(v) => Self::spacing(SpacingKind::Fractional(v)),
+ Value::Node(v) => Self::Node(v.into_block()),
+}
+
/// Performs stack layout.
struct StackLayouter<'a> {
/// The stack node to layout.
@@ -148,15 +142,15 @@ impl<'a> StackLayouter<'a> {
/// Layout all children.
fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
for child in &self.stack.children {
- match *child {
- StackChild::Spacing(Spacing::Linear(v)) => {
- self.layout_absolute(v);
- }
- StackChild::Spacing(Spacing::Fractional(v)) => {
- self.items.push(StackItem::Fractional(v));
- self.fr += v;
- }
- StackChild::Node(ref node) => {
+ match child {
+ StackChild::Spacing(node) => match node.kind {
+ SpacingKind::Linear(v) => self.layout_absolute(v),
+ SpacingKind::Fractional(v) => {
+ self.items.push(StackItem::Fractional(v));
+ self.fr += v;
+ }
+ },
+ StackChild::Node(node) => {
if self.regions.is_full() {
self.finish_region();
}
diff --git a/src/library/text.rs b/src/library/text.rs
index 0790196d..99c68f79 100644
--- a/src/library/text.rs
+++ b/src/library/text.rs
@@ -1,426 +1,488 @@
use std::borrow::Cow;
use std::convert::TryInto;
-use std::ops::Range;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::{BitXor, Range};
use rustybuzz::{Feature, UnicodeBuffer};
use ttf_parser::Tag;
use super::prelude::*;
+use super::LinkNode;
use crate::font::{
Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight,
VerticalFontMetric,
};
use crate::geom::{Dir, Em, Length, Point, Size};
-use crate::style::{
- FontFamily, FontFeatures, NumberPosition, NumberType, NumberWidth, Style,
- StylisticSet, TextStyle,
-};
use crate::util::{EcoString, SliceExt};
-/// `font`: Configure the font.
-pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- castable! {
- Vec<FontFamily>,
- Expected: "string, generic family or array thereof",
- Value::Str(string) => vec![FontFamily::Named(string.to_lowercase())],
- Value::Array(values) => {
- values.into_iter().filter_map(|v| v.cast().ok()).collect()
- },
- @family: FontFamily => vec![family.clone()],
- }
-
- castable! {
- Vec<EcoString>,
- Expected: "string or array of strings",
- Value::Str(string) => vec![string.to_lowercase()],
- Value::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .map(|string: EcoString| string.to_lowercase())
- .collect(),
- }
-
- castable! {
- FontStyle,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "normal" => Self::Normal,
- "italic" => Self::Italic,
- "oblique" => Self::Oblique,
- _ => Err(r#"expected "normal", "italic" or "oblique""#)?,
- },
- }
-
- castable! {
- FontWeight,
- Expected: "integer or string",
- Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number),
- Value::Str(string) => match string.as_str() {
- "thin" => Self::THIN,
- "extralight" => Self::EXTRALIGHT,
- "light" => Self::LIGHT,
- "regular" => Self::REGULAR,
- "medium" => Self::MEDIUM,
- "semibold" => Self::SEMIBOLD,
- "bold" => Self::BOLD,
- "extrabold" => Self::EXTRABOLD,
- "black" => Self::BLACK,
- _ => Err("unknown font weight")?,
- },
- }
-
- castable! {
- FontStretch,
- Expected: "relative",
- Value::Relative(v) => Self::from_ratio(v.get() as f32),
- }
-
- castable! {
- VerticalFontMetric,
- Expected: "linear or string",
- Value::Length(v) => Self::Linear(v.into()),
- Value::Relative(v) => Self::Linear(v.into()),
- Value::Linear(v) => Self::Linear(v),
- Value::Str(string) => match string.as_str() {
- "ascender" => Self::Ascender,
- "cap-height" => Self::CapHeight,
- "x-height" => Self::XHeight,
- "baseline" => Self::Baseline,
- "descender" => Self::Descender,
- _ => Err("unknown font metric")?,
- },
- }
-
- castable! {
- StylisticSet,
- Expected: "integer",
- Value::Int(v) => match v {
- 1 ..= 20 => Self::new(v as u8),
- _ => Err("must be between 1 and 20")?,
- },
- }
-
- castable! {
- NumberType,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "lining" => Self::Lining,
- "old-style" => Self::OldStyle,
- _ => Err(r#"expected "lining" or "old-style""#)?,
- },
- }
-
- castable! {
- NumberWidth,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "proportional" => Self::Proportional,
- "tabular" => Self::Tabular,
- _ => Err(r#"expected "proportional" or "tabular""#)?,
- },
- }
-
- castable! {
- NumberPosition,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "normal" => Self::Normal,
- "subscript" => Self::Subscript,
- "superscript" => Self::Superscript,
- _ => Err(r#"expected "normal", "subscript" or "superscript""#)?,
- },
- }
-
- castable! {
- Vec<(Tag, u32)>,
- Expected: "array of strings or dictionary mapping tags to integers",
- Value::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1))
- .collect(),
- Value::Dict(values) => values
- .into_iter()
- .filter_map(|(k, v)| {
- let tag = Tag::from_bytes_lossy(k.as_bytes());
- let num = v.cast::<i64>().ok()?.try_into().ok()?;
- Some((tag, num))
- })
- .collect(),
+/// `strike`: Typeset striken-through text.
+pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Strikethrough)
+}
+
+/// `underline`: Typeset underlined text.
+pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Underline)
+}
+
+/// `overline`: Typeset text with an overline.
+pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Overline)
+}
+
+fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
+ let stroke = args.named("stroke")?.or_else(|| args.find());
+ let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
+ let offset = args.named("offset")?;
+ let extent = args.named("extent")?.unwrap_or_default();
+ let body: Node = args.expect("body")?;
+ let deco = LineDecoration { kind, stroke, thickness, offset, extent };
+ Ok(Value::Node(
+ body.styled(Styles::one(TextNode::LINES, vec![deco])),
+ ))
+}
+
+/// A single run of text with the same style.
+#[derive(Hash)]
+pub struct TextNode {
+ /// The run's text.
+ pub text: EcoString,
+ /// The run's styles.
+ pub styles: Styles,
+}
+
+#[properties]
+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<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")];
+ /// The sans-serif font family/families.
+ pub const SANS_SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")];
+ /// The monospace font family/families.
+ 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;
+
+ /// How the font is styled.
+ pub const STYLE: FontStyle = FontStyle::Normal;
+ /// The boldness / thickness of the font's glyphs.
+ pub const WEIGHT: FontWeight = FontWeight::REGULAR;
+ /// The width of the glyphs.
+ pub const STRETCH: FontStretch = FontStretch::NORMAL;
+ /// Whether the font weight should be increased by 300.
+ #[fold(bool::bitxor)]
+ pub const STRONG: bool = false;
+ /// Whether the the font style should be inverted.
+ #[fold(bool::bitxor)]
+ pub const EMPH: bool = false;
+ /// Whether a monospace font should be preferred.
+ pub const MONOSPACE: bool = false;
+ /// The glyph fill color.
+ pub const FILL: Paint = RgbaColor::BLACK.into();
+ /// Decorative lines.
+ #[fold(|a, b| a.into_iter().chain(b).collect())]
+ pub const LINES: Vec<LineDecoration> = vec![];
+
+ /// The size of the glyphs.
+ #[fold(Linear::compose)]
+ pub const SIZE: Linear = Length::pt(11.0).into();
+ /// The amount of space that should be added between characters.
+ pub const TRACKING: Em = Em::zero();
+ /// The top end of the text bounding box.
+ pub const TOP_EDGE: VerticalFontMetric = VerticalFontMetric::CapHeight;
+ /// The bottom end of the text bounding box.
+ pub const BOTTOM_EDGE: VerticalFontMetric = VerticalFontMetric::Baseline;
+
+ /// Whether to apply kerning ("kern").
+ pub const KERNING: bool = true;
+ /// Whether small capital glyphs should be used. ("smcp")
+ pub const SMALLCAPS: bool = false;
+ /// Whether to apply stylistic alternates. ("salt")
+ pub const ALTERNATES: bool = false;
+ /// Which stylistic set to apply. ("ss01" - "ss20")
+ pub const STYLISTIC_SET: Option<StylisticSet> = None;
+ /// Whether standard ligatures are active. ("liga", "clig")
+ pub const LIGATURES: bool = true;
+ /// Whether ligatures that should be used sparingly are active. ("dlig")
+ pub const DISCRETIONARY_LIGATURES: bool = false;
+ /// Whether historical ligatures are active. ("hlig")
+ pub const HISTORICAL_LIGATURES: bool = false;
+ /// Which kind of numbers / figures to select.
+ pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto;
+ /// The width of numbers / figures.
+ pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto;
+ /// How to position numbers.
+ pub const NUMBER_POSITION: NumberPosition = NumberPosition::Normal;
+ /// Whether to have a slash through the zero glyph. ("zero")
+ pub const SLASHED_ZERO: bool = false;
+ /// Whether to convert fractions. ("frac")
+ pub const FRACTIONS: bool = false;
+ /// Raw OpenType features to apply.
+ pub const FEATURES: Vec<(Tag, u32)> = vec![];
+}
+
+impl Construct for TextNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ // We don't need to do anything more here because the whole point of the
+ // text constructor is to apply the styles and that happens
+ // automatically during class construction.
+ args.expect::<Node>("body")
}
+}
- let list = args.named("family")?.or_else(|| {
- let families: Vec<_> = args.all().collect();
- (!families.is_empty()).then(|| families)
- });
+impl Set for TextNode {
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ let list = args.named("family")?.or_else(|| {
+ let families: Vec<_> = args.all().collect();
+ (!families.is_empty()).then(|| families)
+ });
+
+ 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")?);
- let serif = args.named("serif")?;
- let sans_serif = args.named("sans-serif")?;
- let monospace = args.named("monospace")?;
- let fallback = args.named("fallback")?;
- let style = args.named("style")?;
- let weight = args.named("weight")?;
- let stretch = args.named("stretch")?;
- let size = args.named::<Linear>("size")?.or_else(|| args.find());
- let tracking = args.named("tracking")?.map(Em::new);
- let top_edge = args.named("top-edge")?;
- let bottom_edge = args.named("bottom-edge")?;
- let fill = args.named("fill")?.or_else(|| args.find());
- let kerning = args.named("kerning")?;
- let smallcaps = args.named("smallcaps")?;
- let alternates = args.named("alternates")?;
- let stylistic_set = args.named("stylistic-set")?;
- let ligatures = args.named("ligatures")?;
- let discretionary_ligatures = args.named("discretionary-ligatures")?;
- let historical_ligatures = args.named("historical-ligatures")?;
- let number_type = args.named("number-type")?;
- let number_width = args.named("number-width")?;
- let number_position = args.named("number-position")?;
- let slashed_zero = args.named("slashed-zero")?;
- let fractions = args.named("fractions")?;
- let features = args.named("features")?;
- let body = args.find::<Template>();
-
- macro_rules! set {
- ($target:expr => $source:expr) => {
- if let Some(v) = $source {
- $target = v;
- }
- };
+ Ok(())
}
+}
- let f = move |style_: &mut Style| {
- let text = style_.text_mut();
- set!(text.families_mut().list => list.clone());
- set!(text.families_mut().serif => serif.clone());
- set!(text.families_mut().sans_serif => sans_serif.clone());
- set!(text.families_mut().monospace => monospace.clone());
- set!(text.fallback => fallback);
- set!(text.variant.style => style);
- set!(text.variant.weight => weight);
- set!(text.variant.stretch => stretch);
- set!(text.size => size.map(|v| v.resolve(text.size)));
- set!(text.tracking => tracking);
- set!(text.top_edge => top_edge);
- set!(text.bottom_edge => bottom_edge);
- set!(text.fill => fill);
- set!(text.features_mut().kerning => kerning);
- set!(text.features_mut().smallcaps => smallcaps);
- set!(text.features_mut().alternates => alternates);
- set!(text.features_mut().stylistic_set => stylistic_set);
- set!(text.features_mut().ligatures.standard => ligatures);
- set!(text.features_mut().ligatures.discretionary => discretionary_ligatures);
- set!(text.features_mut().ligatures.historical => historical_ligatures);
- set!(text.features_mut().numbers.type_ => number_type);
- set!(text.features_mut().numbers.width => number_width);
- set!(text.features_mut().numbers.position => number_position);
- set!(text.features_mut().numbers.slashed_zero => slashed_zero);
- set!(text.features_mut().numbers.fractions => fractions);
- set!(text.features_mut().raw => features.clone());
- };
+impl Debug for TextNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ self.styles.fmt(f)?;
+ }
+ write!(f, "Text({:?})", self.text)
+ }
+}
- Ok(if let Some(body) = body {
- Value::Template(body.modified(f))
- } else {
- ctx.template.modify(f);
- Value::None
- })
+/// A generic or named font family.
+#[derive(Clone, Eq, PartialEq, Hash)]
+pub enum FontFamily {
+ /// A family that has "serifs", small strokes attached to letters.
+ Serif,
+ /// A family in which glyphs do not have "serifs", small attached strokes.
+ SansSerif,
+ /// A family in which (almost) all glyphs are of equal width.
+ Monospace,
+ /// A specific font family like "Arial".
+ Named(NamedFamily),
}
-/// Shape text into [`ShapedText`].
-pub fn shape<'a>(
- ctx: &mut LayoutContext,
- text: &'a str,
- style: &'a TextStyle,
- dir: Dir,
-) -> ShapedText<'a> {
- let mut glyphs = vec![];
- if !text.is_empty() {
- shape_segment(
- ctx.fonts,
- &mut glyphs,
- 0,
- text,
- style.variant(),
- style.families(),
- None,
- dir,
- &tags(&style.features),
- );
+impl FontFamily {
+ /// Create a named font family variant, directly from a string.
+ pub fn named(string: &str) -> Self {
+ Self::Named(NamedFamily::new(string))
}
+}
- track(&mut glyphs, style.tracking);
+impl Debug for FontFamily {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Serif => f.pad("serif"),
+ Self::SansSerif => f.pad("sans-serif"),
+ Self::Monospace => f.pad("monospace"),
+ Self::Named(s) => s.fmt(f),
+ }
+ }
+}
- let (size, baseline) = measure(ctx, &glyphs, style);
- ShapedText {
- text,
- dir,
- style,
- size,
- baseline,
- glyphs: Cow::Owned(glyphs),
+/// 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
}
}
-/// The result of shaping text.
-///
-/// This type contains owned or borrowed shaped text runs, which can be
-/// measured, used to reshape substrings more quickly and converted into a
-/// frame.
-#[derive(Debug, Clone)]
-pub struct ShapedText<'a> {
- /// The text that was shaped.
- pub text: &'a str,
- /// The text direction.
- pub dir: Dir,
- /// The properties used for font selection.
- pub style: &'a TextStyle,
- /// The font size.
- pub size: Size,
- /// The baseline from the top of the frame.
- pub baseline: Length,
- /// The shaped glyphs.
- pub glyphs: Cow<'a, [ShapedGlyph]>,
+impl Debug for NamedFamily {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.0.fmt(f)
+ }
}
-/// A single glyph resulting from shaping.
-#[derive(Debug, Copy, Clone)]
-pub struct ShapedGlyph {
- /// The font face the glyph is contained in.
- pub face_id: FaceId,
- /// The glyph's index in the face.
- pub glyph_id: u16,
- /// The advance width of the glyph.
- pub x_advance: Em,
- /// The horizontal offset of the glyph.
- pub x_offset: Em,
- /// The start index of the glyph in the source text.
- pub text_index: usize,
- /// Whether splitting the shaping result before this glyph would yield the
- /// same results as shaping the parts to both sides of `text_index`
- /// separately.
- pub safe_to_break: bool,
+dynamic! {
+ FontFamily: "font family",
+ Value::Str(string) => Self::named(&string),
}
-impl<'a> ShapedText<'a> {
- /// Build the shaped text's frame.
- pub fn build(&self) -> Frame {
- let mut offset = Length::zero();
- let mut frame = Frame::new(self.size);
- frame.baseline = Some(self.baseline);
+castable! {
+ Vec<FontFamily>,
+ Expected: "string, generic family or array thereof",
+ Value::Str(string) => vec![FontFamily::named(&string)],
+ Value::Array(values) => {
+ values.into_iter().filter_map(|v| v.cast().ok()).collect()
+ },
+ @family: FontFamily => vec![family.clone()],
+}
- for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
- let pos = Point::new(offset, self.baseline);
+castable! {
+ Vec<NamedFamily>,
+ Expected: "string or array of strings",
+ Value::Str(string) => vec![NamedFamily::new(&string)],
+ Value::Array(values) => values
+ .into_iter()
+ .filter_map(|v| v.cast().ok())
+ .map(|string: EcoString| NamedFamily::new(&string))
+ .collect(),
+}
- let mut text = Text {
- face_id,
- size: self.style.size,
- width: Length::zero(),
- fill: self.style.fill,
- glyphs: vec![],
- };
+castable! {
+ FontStyle,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "normal" => Self::Normal,
+ "italic" => Self::Italic,
+ "oblique" => Self::Oblique,
+ _ => Err(r#"expected "normal", "italic" or "oblique""#)?,
+ },
+}
- for glyph in group {
- text.glyphs.push(Glyph {
- id: glyph.glyph_id,
- x_advance: glyph.x_advance,
- x_offset: glyph.x_offset,
- });
- text.width += glyph.x_advance.to_length(text.size);
- }
+castable! {
+ FontWeight,
+ Expected: "integer or string",
+ 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,
+ "light" => Self::LIGHT,
+ "regular" => Self::REGULAR,
+ "medium" => Self::MEDIUM,
+ "semibold" => Self::SEMIBOLD,
+ "bold" => Self::BOLD,
+ "extrabold" => Self::EXTRABOLD,
+ "black" => Self::BLACK,
+ _ => Err("unknown font weight")?,
+ },
+}
- offset += text.width;
- frame.push(pos, Element::Text(text));
- }
+castable! {
+ FontStretch,
+ Expected: "relative",
+ Value::Relative(v) => Self::from_ratio(v.get() as f32),
+}
- frame
+castable! {
+ VerticalFontMetric,
+ Expected: "linear or string",
+ Value::Length(v) => Self::Linear(v.into()),
+ Value::Relative(v) => Self::Linear(v.into()),
+ Value::Linear(v) => Self::Linear(v),
+ Value::Str(string) => match string.as_str() {
+ "ascender" => Self::Ascender,
+ "cap-height" => Self::CapHeight,
+ "x-height" => Self::XHeight,
+ "baseline" => Self::Baseline,
+ "descender" => Self::Descender,
+ _ => Err("unknown font metric")?,
+ },
+}
+
+/// A stylistic set in a font face.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct StylisticSet(u8);
+
+impl StylisticSet {
+ /// Creates a new set, clamping to 1-20.
+ pub fn new(index: u8) -> Self {
+ Self(index.clamp(1, 20))
}
- /// Reshape a range of the shaped text, reusing information from this
- /// shaping process if possible.
- pub fn reshape(
- &'a self,
- ctx: &mut LayoutContext,
- text_range: Range<usize>,
- ) -> ShapedText<'a> {
- if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
- let (size, baseline) = measure(ctx, glyphs, self.style);
- Self {
- text: &self.text[text_range],
- dir: self.dir,
- style: self.style,
- size,
- baseline,
- glyphs: Cow::Borrowed(glyphs),
- }
- } else {
- shape(ctx, &self.text[text_range], self.style, self.dir)
- }
+ /// Get the value, guaranteed to be 1-20.
+ pub fn get(self) -> u8 {
+ self.0
}
+}
- /// Find the subslice of glyphs that represent the given text range if both
- /// sides are safe to break.
- fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
- let Range { mut start, mut end } = text_range;
- if !self.dir.is_positive() {
- std::mem::swap(&mut start, &mut end);
- }
+castable! {
+ StylisticSet,
+ Expected: "integer",
+ Value::Int(v) => match v {
+ 1 ..= 20 => Self::new(v as u8),
+ _ => Err("must be between 1 and 20")?,
+ },
+}
- let left = self.find_safe_to_break(start, Side::Left)?;
- let right = self.find_safe_to_break(end, Side::Right)?;
- Some(&self.glyphs[left .. right])
- }
+/// Which kind of numbers / figures to select.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum NumberType {
+ /// Numbers that fit well with capital text. ("lnum")
+ Lining,
+ /// Numbers that fit well into flow of upper- and lowercase text. ("onum")
+ OldStyle,
+}
- /// Find the glyph offset matching the text index that is most towards the
- /// given side and safe-to-break.
- fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
- let ltr = self.dir.is_positive();
+castable! {
+ NumberType,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "lining" => Self::Lining,
+ "old-style" => Self::OldStyle,
+ _ => Err(r#"expected "lining" or "old-style""#)?,
+ },
+}
- // Handle edge cases.
- let len = self.glyphs.len();
- if text_index == 0 {
- return Some(if ltr { 0 } else { len });
- } else if text_index == self.text.len() {
- return Some(if ltr { len } else { 0 });
- }
+/// The width of numbers / figures.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum NumberWidth {
+ /// Number widths are glyph specific. ("pnum")
+ Proportional,
+ /// All numbers are of equal width / monospaced. ("tnum")
+ Tabular,
+}
- // Find any glyph with the text index.
- let mut idx = self
- .glyphs
- .binary_search_by(|g| {
- let ordering = g.text_index.cmp(&text_index);
- if ltr { ordering } else { ordering.reverse() }
- })
- .ok()?;
+castable! {
+ NumberWidth,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "proportional" => Self::Proportional,
+ "tabular" => Self::Tabular,
+ _ => Err(r#"expected "proportional" or "tabular""#)?,
+ },
+}
- let next = match towards {
- Side::Left => usize::checked_sub,
- Side::Right => usize::checked_add,
- };
+/// How to position numbers.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum NumberPosition {
+ /// Numbers are positioned on the same baseline as text.
+ Normal,
+ /// Numbers are smaller and placed at the bottom. ("subs")
+ Subscript,
+ /// Numbers are smaller and placed at the top. ("sups")
+ Superscript,
+}
- // Search for the outermost glyph with the text index.
- while let Some(next) = next(idx, 1) {
- if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
- break;
- }
- idx = next;
- }
+castable! {
+ NumberPosition,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "normal" => Self::Normal,
+ "subscript" => Self::Subscript,
+ "superscript" => Self::Superscript,
+ _ => Err(r#"expected "normal", "subscript" or "superscript""#)?,
+ },
+}
- // RTL needs offset one because the left side of the range should be
- // exclusive and the right side inclusive, contrary to the normal
- // behaviour of ranges.
- if !ltr {
- idx += 1;
- }
+castable! {
+ Vec<(Tag, u32)>,
+ Expected: "array of strings or dictionary mapping tags to integers",
+ Value::Array(values) => values
+ .into_iter()
+ .filter_map(|v| v.cast().ok())
+ .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1))
+ .collect(),
+ Value::Dict(values) => values
+ .into_iter()
+ .filter_map(|(k, v)| {
+ let tag = Tag::from_bytes_lossy(k.as_bytes());
+ let num = v.cast::<i64>().ok()?.try_into().ok()?;
+ Some((tag, num))
+ })
+ .collect(),
+}
- self.glyphs[idx].safe_to_break.then(|| idx)
- }
+/// Defines a line that is positioned over, under or on top of text.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct LineDecoration {
+ /// The kind of line.
+ pub kind: LineKind,
+ /// Stroke color of the line, defaults to the text color if `None`.
+ pub stroke: Option<Paint>,
+ /// Thickness of the line's strokes (dependent on scaled font size), read
+ /// from the font tables if `None`.
+ pub thickness: Option<Linear>,
+ /// Position of the line relative to the baseline (dependent on scaled font
+ /// size), read from the font tables if `None`.
+ pub offset: Option<Linear>,
+ /// Amount that the line will be longer or shorter than its associated text
+ /// (dependent on scaled font size).
+ pub extent: Linear,
}
-/// A visual side.
-enum Side {
- Left,
- Right,
+/// The kind of line decoration.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum LineKind {
+ /// A line under text.
+ Underline,
+ /// A line through text.
+ Strikethrough,
+ /// A line over text.
+ Overline,
+}
+
+/// Shape text into [`ShapedText`].
+pub fn shape<'a>(
+ fonts: &mut FontStore,
+ text: &'a str,
+ styles: Styles,
+ dir: Dir,
+) -> ShapedText<'a> {
+ let mut glyphs = vec![];
+ if !text.is_empty() {
+ shape_segment(
+ fonts,
+ &mut glyphs,
+ 0,
+ text,
+ variant(&styles),
+ families(&styles),
+ None,
+ dir,
+ &tags(&styles),
+ );
+ }
+
+ track(&mut glyphs, styles.get(TextNode::TRACKING));
+ let (size, baseline) = measure(fonts, &glyphs, &styles);
+
+ ShapedText {
+ text,
+ dir,
+ styles,
+ size,
+ baseline,
+ glyphs: Cow::Owned(glyphs),
+ }
}
/// Shape text with font fallback using the `families` iterator.
@@ -568,36 +630,40 @@ fn track(glyphs: &mut [ShapedGlyph], tracking: Em) {
/// Measure the size and baseline of a run of shaped glyphs with the given
/// properties.
fn measure(
- ctx: &mut LayoutContext,
+ fonts: &mut FontStore,
glyphs: &[ShapedGlyph],
- style: &TextStyle,
+ styles: &Styles,
) -> (Size, Length) {
let mut width = Length::zero();
let mut top = Length::zero();
let mut bottom = Length::zero();
+ let size = styles.get(TextNode::SIZE).abs;
+ let top_edge = styles.get(TextNode::TOP_EDGE);
+ let bottom_edge = styles.get(TextNode::BOTTOM_EDGE);
+
// Expand top and bottom by reading the face's vertical metrics.
let mut expand = |face: &Face| {
- top.set_max(face.vertical_metric(style.top_edge, style.size));
- bottom.set_max(-face.vertical_metric(style.bottom_edge, style.size));
+ top.set_max(face.vertical_metric(top_edge, size));
+ bottom.set_max(-face.vertical_metric(bottom_edge, size));
};
if glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
- for family in style.families() {
- if let Some(face_id) = ctx.fonts.select(family, style.variant) {
- expand(ctx.fonts.get(face_id));
+ for family in families(styles) {
+ if let Some(face_id) = fonts.select(family, variant(styles)) {
+ expand(fonts.get(face_id));
break;
}
}
} else {
for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
- let face = ctx.fonts.get(face_id);
+ let face = fonts.get(face_id);
expand(face);
for glyph in group {
- width += glyph.x_advance.to_length(style.size);
+ width += glyph.x_advance.resolve(size);
}
}
}
@@ -605,75 +671,320 @@ fn measure(
(Size::new(width, top + bottom), top)
}
+/// Resolve the font variant with `STRONG` and `EMPH` factored in.
+fn variant(styles: &Styles) -> FontVariant {
+ let mut variant = FontVariant::new(
+ styles.get(TextNode::STYLE),
+ styles.get(TextNode::WEIGHT),
+ styles.get(TextNode::STRETCH),
+ );
+
+ if styles.get(TextNode::STRONG) {
+ variant.weight = variant.weight.thicken(300);
+ }
+
+ if styles.get(TextNode::EMPH) {
+ variant.style = match variant.style {
+ FontStyle::Normal => FontStyle::Italic,
+ FontStyle::Italic => FontStyle::Normal,
+ FontStyle::Oblique => FontStyle::Normal,
+ }
+ }
+
+ variant
+}
+
+/// Resolve a prioritized iterator over the font families.
+fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone {
+ let head = if styles.get(TextNode::MONOSPACE) {
+ styles.get_ref(TextNode::MONOSPACE_LIST).as_slice()
+ } else {
+ &[]
+ };
+
+ let core = styles.get_ref(TextNode::FAMILY_LIST).iter().flat_map(move |family| {
+ match family {
+ FontFamily::Named(name) => std::slice::from_ref(name),
+ FontFamily::Serif => styles.get_ref(TextNode::SERIF_LIST),
+ FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF_LIST),
+ FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE_LIST),
+ }
+ });
+
+ let tail: &[&str] = if styles.get(TextNode::FALLBACK) {
+ &["ibm plex sans", "latin modern math", "twitter color emoji"]
+ } else {
+ &[]
+ };
+
+ head.iter()
+ .chain(core)
+ .map(|named| named.as_str())
+ .chain(tail.iter().copied())
+}
+
/// Collect the tags of the OpenType features to apply.
-fn tags(features: &FontFeatures) -> Vec<Feature> {
+fn tags(styles: &Styles) -> Vec<Feature> {
let mut tags = vec![];
let mut feat = |tag, value| {
tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
};
// Features that are on by default in Harfbuzz are only added if disabled.
- if !features.kerning {
+ if !styles.get(TextNode::KERNING) {
feat(b"kern", 0);
}
// Features that are off by default in Harfbuzz are only added if enabled.
- if features.smallcaps {
+ if styles.get(TextNode::SMALLCAPS) {
feat(b"smcp", 1);
}
- if features.alternates {
+ if styles.get(TextNode::ALTERNATES) {
feat(b"salt", 1);
}
let storage;
- if let Some(set) = features.stylistic_set {
+ if let Some(set) = styles.get(TextNode::STYLISTIC_SET) {
storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
feat(&storage, 1);
}
- if !features.ligatures.standard {
+ if !styles.get(TextNode::LIGATURES) {
feat(b"liga", 0);
feat(b"clig", 0);
}
- if features.ligatures.discretionary {
+ if styles.get(TextNode::DISCRETIONARY_LIGATURES) {
feat(b"dlig", 1);
}
- if features.ligatures.historical {
+ if styles.get(TextNode::HISTORICAL_LIGATURES) {
feat(b"hilg", 1);
}
- match features.numbers.type_ {
+ match styles.get(TextNode::NUMBER_TYPE) {
Smart::Auto => {}
Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
}
- match features.numbers.width {
+ match styles.get(TextNode::NUMBER_WIDTH) {
Smart::Auto => {}
Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
}
- match features.numbers.position {
+ match styles.get(TextNode::NUMBER_POSITION) {
NumberPosition::Normal => {}
NumberPosition::Subscript => feat(b"subs", 1),
NumberPosition::Superscript => feat(b"sups", 1),
}
- if features.numbers.slashed_zero {
+ if styles.get(TextNode::SLASHED_ZERO) {
feat(b"zero", 1);
}
- if features.numbers.fractions {
+ if styles.get(TextNode::FRACTIONS) {
feat(b"frac", 1);
}
- for &(tag, value) in features.raw.iter() {
+ for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() {
tags.push(Feature::new(tag, value, ..))
}
tags
}
+
+/// The result of shaping text.
+///
+/// This type contains owned or borrowed shaped text runs, which can be
+/// measured, used to reshape substrings more quickly and converted into a
+/// frame.
+#[derive(Debug, Clone)]
+pub struct ShapedText<'a> {
+ /// The text that was shaped.
+ pub text: &'a str,
+ /// The text direction.
+ pub dir: Dir,
+ /// The text's style properties.
+ // TODO(style): Go back to reference.
+ pub styles: Styles,
+ /// The font size.
+ pub size: Size,
+ /// The baseline from the top of the frame.
+ pub baseline: Length,
+ /// The shaped glyphs.
+ pub glyphs: Cow<'a, [ShapedGlyph]>,
+}
+
+/// A single glyph resulting from shaping.
+#[derive(Debug, Copy, Clone)]
+pub struct ShapedGlyph {
+ /// The font face the glyph is contained in.
+ pub face_id: FaceId,
+ /// The glyph's index in the face.
+ pub glyph_id: u16,
+ /// The advance width of the glyph.
+ pub x_advance: Em,
+ /// The horizontal offset of the glyph.
+ pub x_offset: Em,
+ /// The start index of the glyph in the source text.
+ pub text_index: usize,
+ /// Whether splitting the shaping result before this glyph would yield the
+ /// same results as shaping the parts to both sides of `text_index`
+ /// separately.
+ pub safe_to_break: bool,
+}
+
+impl<'a> ShapedText<'a> {
+ /// Build the shaped text's frame.
+ pub fn build(&self, fonts: &FontStore) -> Frame {
+ let mut offset = Length::zero();
+ let mut frame = Frame::new(self.size);
+ frame.baseline = Some(self.baseline);
+
+ for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
+ let pos = Point::new(offset, self.baseline);
+
+ let size = self.styles.get(TextNode::SIZE).abs;
+ let fill = self.styles.get(TextNode::FILL);
+ let glyphs = group
+ .iter()
+ .map(|glyph| Glyph {
+ id: glyph.glyph_id,
+ x_advance: glyph.x_advance,
+ x_offset: glyph.x_offset,
+ })
+ .collect();
+
+ let text = Text { face_id, size, fill, glyphs };
+ let width = text.width();
+ frame.push(pos, Element::Text(text));
+
+ // Apply line decorations.
+ for line in self.styles.get_ref(TextNode::LINES) {
+ let face = fonts.get(face_id);
+ let metrics = match line.kind {
+ LineKind::Underline => face.underline,
+ LineKind::Strikethrough => face.strikethrough,
+ LineKind::Overline => face.overline,
+ };
+
+ let extent = line.extent.resolve(size);
+ let offset = line
+ .offset
+ .map(|s| s.resolve(size))
+ .unwrap_or(-metrics.position.resolve(size));
+
+ let stroke = Stroke {
+ paint: line.stroke.unwrap_or(fill),
+ thickness: line
+ .thickness
+ .map(|s| s.resolve(size))
+ .unwrap_or(metrics.thickness.resolve(size)),
+ };
+
+ let subpos = Point::new(pos.x - extent, pos.y + offset);
+ let target = Point::new(width + 2.0 * extent, Length::zero());
+ let shape = Shape::stroked(Geometry::Line(target), stroke);
+ frame.push(subpos, Element::Shape(shape));
+ }
+
+ offset += width;
+ }
+
+ // Apply link if it exists.
+ if let Some(url) = self.styles.get_ref(LinkNode::URL) {
+ frame.link(url);
+ }
+
+ frame
+ }
+
+ /// Reshape a range of the shaped text, reusing information from this
+ /// shaping process if possible.
+ pub fn reshape(
+ &'a self,
+ fonts: &mut FontStore,
+ text_range: Range<usize>,
+ ) -> ShapedText<'a> {
+ if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
+ let (size, baseline) = measure(fonts, glyphs, &self.styles);
+ Self {
+ text: &self.text[text_range],
+ dir: self.dir,
+ styles: self.styles.clone(),
+ size,
+ baseline,
+ glyphs: Cow::Borrowed(glyphs),
+ }
+ } else {
+ shape(fonts, &self.text[text_range], self.styles.clone(), self.dir)
+ }
+ }
+
+ /// Find the subslice of glyphs that represent the given text range if both
+ /// sides are safe to break.
+ fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
+ let Range { mut start, mut end } = text_range;
+ if !self.dir.is_positive() {
+ std::mem::swap(&mut start, &mut end);
+ }
+
+ let left = self.find_safe_to_break(start, Side::Left)?;
+ let right = self.find_safe_to_break(end, Side::Right)?;
+ Some(&self.glyphs[left .. right])
+ }
+
+ /// Find the glyph offset matching the text index that is most towards the
+ /// given side and safe-to-break.
+ fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
+ let ltr = self.dir.is_positive();
+
+ // Handle edge cases.
+ let len = self.glyphs.len();
+ if text_index == 0 {
+ return Some(if ltr { 0 } else { len });
+ } else if text_index == self.text.len() {
+ return Some(if ltr { len } else { 0 });
+ }
+
+ // Find any glyph with the text index.
+ let mut idx = self
+ .glyphs
+ .binary_search_by(|g| {
+ let ordering = g.text_index.cmp(&text_index);
+ if ltr { ordering } else { ordering.reverse() }
+ })
+ .ok()?;
+
+ let next = match towards {
+ Side::Left => usize::checked_sub,
+ Side::Right => usize::checked_add,
+ };
+
+ // Search for the outermost glyph with the text index.
+ while let Some(next) = next(idx, 1) {
+ if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
+ break;
+ }
+ idx = next;
+ }
+
+ // RTL needs offset one because the left side of the range should be
+ // exclusive and the right side inclusive, contrary to the normal
+ // behaviour of ranges.
+ if !ltr {
+ idx += 1;
+ }
+
+ self.glyphs[idx].safe_to_break.then(|| idx)
+ }
+}
+
+/// A visual side.
+enum Side {
+ Left,
+ Right,
+}
diff --git a/src/library/transform.rs b/src/library/transform.rs
index 9ba71ecf..6a6b4034 100644
--- a/src/library/transform.rs
+++ b/src/library/transform.rs
@@ -26,15 +26,15 @@ pub fn rotate(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
}
fn transform_impl(args: &mut Args, transform: Transform) -> TypResult<Value> {
- let body: Template = args.expect("body")?;
+ let body: Node = args.expect("body")?;
let origin = args
.named("origin")?
.unwrap_or(Spec::splat(None))
.unwrap_or(Align::CENTER_HORIZON);
- Ok(Value::Template(Template::from_inline(move |style| {
- body.pack(style).transformed(transform, origin)
- })))
+ Ok(Value::inline(
+ body.into_block().transformed(transform, origin),
+ ))
}
/// A node that transforms its child without affecting layout.
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
index dbec0a5e..0a2f73f5 100644
--- a/src/parse/mod.rs
+++ b/src/parse/mod.rs
@@ -110,12 +110,13 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
// Hashtag + keyword / identifier.
NodeKind::Ident(_)
| NodeKind::Let
+ | NodeKind::Set
| NodeKind::If
| NodeKind::While
| NodeKind::For
| NodeKind::Import
| NodeKind::Include => {
- let stmt = matches!(token, NodeKind::Let | NodeKind::Import);
+ let stmt = matches!(token, NodeKind::Let | NodeKind::Set | NodeKind::Import);
let group = if stmt { Group::Stmt } else { Group::Expr };
p.start_group(group);
@@ -265,6 +266,7 @@ fn primary(p: &mut Parser, atomic: bool) -> ParseResult {
// Keywords.
Some(NodeKind::Let) => let_expr(p),
+ Some(NodeKind::Set) => set_expr(p),
Some(NodeKind::If) => if_expr(p),
Some(NodeKind::While) => while_expr(p),
Some(NodeKind::For) => for_expr(p),
@@ -507,45 +509,40 @@ fn block(p: &mut Parser) {
/// Parse a function call.
fn call(p: &mut Parser, callee: Marker) -> ParseResult {
- callee.perform(p, NodeKind::Call, |p| match p.peek_direct() {
- Some(NodeKind::LeftParen | NodeKind::LeftBracket) => {
- args(p, true);
- Ok(())
- }
- _ => {
- p.expected_at("argument list");
- Err(())
- }
- })
+ callee.perform(p, NodeKind::Call, |p| args(p, true, true))
}
/// Parse the arguments to a function call.
-fn args(p: &mut Parser, allow_template: bool) {
+fn args(p: &mut Parser, direct: bool, brackets: bool) -> ParseResult {
+ match if direct { p.peek_direct() } else { p.peek() } {
+ Some(NodeKind::LeftParen) => {}
+ Some(NodeKind::LeftBracket) if brackets => {}
+ _ => {
+ p.expected("argument list");
+ return Err(());
+ }
+ }
+
p.perform(NodeKind::CallArgs, |p| {
- if !allow_template || p.peek_direct() == Some(&NodeKind::LeftParen) {
+ if p.at(&NodeKind::LeftParen) {
p.start_group(Group::Paren);
collection(p);
p.end_group();
}
- while allow_template && p.peek_direct() == Some(&NodeKind::LeftBracket) {
+ while brackets && p.peek_direct() == Some(&NodeKind::LeftBracket) {
template(p);
}
- })
+ });
+
+ Ok(())
}
/// Parse a with expression.
fn with_expr(p: &mut Parser, marker: Marker) -> ParseResult {
marker.perform(p, NodeKind::WithExpr, |p| {
p.eat_assert(&NodeKind::With);
-
- if p.at(&NodeKind::LeftParen) {
- args(p, false);
- Ok(())
- } else {
- p.expected("argument list");
- Err(())
- }
+ args(p, false, false)
})
}
@@ -587,6 +584,15 @@ fn let_expr(p: &mut Parser) -> ParseResult {
})
}
+/// Parse a set expression.
+fn set_expr(p: &mut Parser) -> ParseResult {
+ p.perform(NodeKind::SetExpr, |p| {
+ p.eat_assert(&NodeKind::Set);
+ ident(p)?;
+ args(p, true, false)
+ })
+}
+
/// Parse an if expresion.
fn if_expr(p: &mut Parser) -> ParseResult {
p.perform(NodeKind::IfExpr, |p| {
@@ -612,8 +618,7 @@ fn while_expr(p: &mut Parser) -> ParseResult {
p.perform(NodeKind::WhileExpr, |p| {
p.eat_assert(&NodeKind::While);
expr(p)?;
- body(p)?;
- Ok(())
+ body(p)
})
}
@@ -624,8 +629,7 @@ fn for_expr(p: &mut Parser) -> ParseResult {
for_pattern(p)?;
p.eat_expect(&NodeKind::In)?;
expr(p)?;
- body(p)?;
- Ok(())
+ body(p)
})
}
@@ -664,9 +668,7 @@ fn import_expr(p: &mut Parser) -> ParseResult {
};
p.eat_expect(&NodeKind::From)?;
- expr(p)?;
-
- Ok(())
+ expr(p)
})
}
@@ -674,8 +676,7 @@ fn import_expr(p: &mut Parser) -> ParseResult {
fn include_expr(p: &mut Parser) -> ParseResult {
p.perform(NodeKind::IncludeExpr, |p| {
p.eat_assert(&NodeKind::Include);
- expr(p)?;
- Ok(())
+ expr(p)
})
}
diff --git a/src/parse/parser.rs b/src/parse/parser.rs
index 6598b1f2..503158a9 100644
--- a/src/parse/parser.rs
+++ b/src/parse/parser.rs
@@ -125,6 +125,7 @@ impl<'s> Parser<'s> {
}
/// Eat, debug-asserting that the token is the given one.
+ #[track_caller]
pub fn eat_assert(&mut self, t: &NodeKind) {
debug_assert_eq!(self.peek(), Some(t));
self.eat();
@@ -199,6 +200,7 @@ impl<'s> Parser<'s> {
/// to `end_group`.
///
/// This panics if the current token does not start the given group.
+ #[track_caller]
pub fn start_group(&mut self, kind: Group) {
self.groups.push(GroupEntry { kind, prev_mode: self.tokens.mode() });
self.tokens.set_mode(match kind {
@@ -220,6 +222,7 @@ impl<'s> Parser<'s> {
/// End the parsing of a group.
///
/// This panics if no group was started.
+ #[track_caller]
pub fn end_group(&mut self) {
let group_mode = self.tokens.mode();
let group = self.groups.pop().expect("no started group");
diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs
index 07a6fe12..27ec046d 100644
--- a/src/parse/tokens.rs
+++ b/src/parse/tokens.rs
@@ -527,6 +527,7 @@ fn keyword(ident: &str) -> Option<NodeKind> {
"or" => NodeKind::Or,
"with" => NodeKind::With,
"let" => NodeKind::Let,
+ "set" => NodeKind::Set,
"if" => NodeKind::If,
"else" => NodeKind::Else,
"for" => NodeKind::For,
diff --git a/src/source.rs b/src/source.rs
index 509b0a76..432688a0 100644
--- a/src/source.rs
+++ b/src/source.rs
@@ -149,7 +149,12 @@ impl SourceFile {
Self::new(SourceId(0), Path::new(""), src.into())
}
- /// The file's abstract syntax tree.
+ /// The root node of the file's untyped green tree.
+ pub fn root(&self) -> &Rc<GreenNode> {
+ &self.root
+ }
+
+ /// 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/style/mod.rs b/src/style/mod.rs
deleted file mode 100644
index 45dbeb54..00000000
--- a/src/style/mod.rs
+++ /dev/null
@@ -1,419 +0,0 @@
-//! Style properties.
-
-mod paper;
-
-pub use paper::*;
-
-use std::fmt::{self, Debug, Formatter};
-use std::rc::Rc;
-
-use ttf_parser::Tag;
-
-use crate::eval::Smart;
-use crate::font::*;
-use crate::geom::*;
-use crate::util::EcoString;
-
-/// Defines a set of properties a template can be instantiated with.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct Style {
- /// The page settings.
- pub page: Rc<PageStyle>,
- /// The paragraph settings.
- pub par: Rc<ParStyle>,
- /// The current text settings.
- pub text: Rc<TextStyle>,
-}
-
-impl Style {
- /// Access the `page` style mutably.
- pub fn page_mut(&mut self) -> &mut PageStyle {
- Rc::make_mut(&mut self.page)
- }
-
- /// Access the `par` style mutably.
- pub fn par_mut(&mut self) -> &mut ParStyle {
- Rc::make_mut(&mut self.par)
- }
-
- /// Access the `text` style mutably.
- pub fn text_mut(&mut self) -> &mut TextStyle {
- Rc::make_mut(&mut self.text)
- }
-
- /// The resolved line spacing.
- pub fn leading(&self) -> Length {
- self.par.leading.resolve(self.text.size)
- }
-
- /// The resolved paragraph spacing.
- pub fn par_spacing(&self) -> Length {
- self.par.spacing.resolve(self.text.size)
- }
-}
-
-impl Default for Style {
- fn default() -> Self {
- Self {
- page: Rc::new(PageStyle::default()),
- par: Rc::new(ParStyle::default()),
- text: Rc::new(TextStyle::default()),
- }
- }
-}
-
-/// Defines style properties of pages.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct PageStyle {
- /// The class of this page.
- pub class: PaperClass,
- /// The width and height of the page.
- pub size: Size,
- /// The amount of white space on each side of the page. If a side is set to
- /// `None`, the default for the paper class is used.
- pub margins: Sides<Smart<Linear>>,
- /// The background fill of the page.
- pub fill: Option<Paint>,
-}
-
-impl PageStyle {
- /// The resolved margins.
- pub fn margins(&self) -> Sides<Linear> {
- let default = self.class.default_margins();
- Sides {
- left: self.margins.left.unwrap_or(default.left),
- top: self.margins.top.unwrap_or(default.top),
- right: self.margins.right.unwrap_or(default.right),
- bottom: self.margins.bottom.unwrap_or(default.bottom),
- }
- }
-}
-
-impl Default for PageStyle {
- fn default() -> Self {
- let paper = Paper::A4;
- Self {
- class: paper.class(),
- size: paper.size(),
- margins: Sides::splat(Smart::Auto),
- fill: None,
- }
- }
-}
-
-/// Defines style properties of paragraphs.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct ParStyle {
- /// The direction for text and inline objects.
- pub dir: Dir,
- /// How to align text and inline objects in their line.
- pub align: Align,
- /// The spacing between lines (dependent on scaled font size).
- pub leading: Linear,
- /// The spacing between paragraphs (dependent on scaled font size).
- pub spacing: Linear,
-}
-
-impl Default for ParStyle {
- fn default() -> Self {
- Self {
- dir: Dir::LTR,
- align: Align::Left,
- leading: Relative::new(0.65).into(),
- spacing: Relative::new(1.2).into(),
- }
- }
-}
-
-/// Defines style properties of text.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct TextStyle {
- /// The font size.
- pub size: Length,
- /// The selected font variant (the final variant also depends on `strong`
- /// and `emph`).
- pub variant: FontVariant,
- /// The top end of the text bounding box.
- pub top_edge: VerticalFontMetric,
- /// The bottom end of the text bounding box.
- pub bottom_edge: VerticalFontMetric,
- /// Glyph color.
- pub fill: Paint,
- /// A list of font families with generic class definitions (the final
- /// family list also depends on `monospace`).
- pub families: Rc<FamilyStyle>,
- /// OpenType features.
- pub features: Rc<FontFeatures>,
- /// The amount of space that should be added between character.
- pub tracking: Em,
- /// Whether 300 extra font weight should be added to what is defined by the
- /// `variant`.
- pub strong: bool,
- /// Whether the the font style defined by the `variant` should be inverted.
- pub emph: bool,
- /// Whether a monospace font should be preferred.
- pub monospace: bool,
- /// Whether font fallback to a base list should occur.
- pub fallback: bool,
-}
-
-impl TextStyle {
- /// The resolved variant with `strong` and `emph` factored in.
- pub fn variant(&self) -> FontVariant {
- let mut variant = self.variant;
-
- if self.strong {
- variant.weight = variant.weight.thicken(300);
- }
-
- if self.emph {
- variant.style = match variant.style {
- FontStyle::Normal => FontStyle::Italic,
- FontStyle::Italic => FontStyle::Normal,
- FontStyle::Oblique => FontStyle::Normal,
- }
- }
-
- variant
- }
-
- /// The resolved family iterator.
- pub fn families(&self) -> impl Iterator<Item = &str> + Clone {
- let head = if self.monospace {
- self.families.monospace.as_slice()
- } else {
- &[]
- };
-
- let core = self.families.list.iter().flat_map(move |family| {
- match family {
- FontFamily::Named(name) => std::slice::from_ref(name),
- FontFamily::Serif => &self.families.serif,
- FontFamily::SansSerif => &self.families.sans_serif,
- FontFamily::Monospace => &self.families.monospace,
- }
- });
-
- let tail = if self.fallback {
- self.families.base.as_slice()
- } else {
- &[]
- };
-
- head.iter().chain(core).chain(tail).map(EcoString::as_str)
- }
-
- /// Access the `families` style mutably.
- pub fn families_mut(&mut self) -> &mut FamilyStyle {
- Rc::make_mut(&mut self.families)
- }
-
- /// Access the font `features` mutably.
- pub fn features_mut(&mut self) -> &mut FontFeatures {
- Rc::make_mut(&mut self.features)
- }
-}
-
-impl Default for TextStyle {
- fn default() -> Self {
- Self {
- size: Length::pt(11.0),
- variant: FontVariant {
- style: FontStyle::Normal,
- weight: FontWeight::REGULAR,
- stretch: FontStretch::NORMAL,
- },
- top_edge: VerticalFontMetric::CapHeight,
- bottom_edge: VerticalFontMetric::Baseline,
- fill: RgbaColor::BLACK.into(),
- families: Rc::new(FamilyStyle::default()),
- features: Rc::new(FontFeatures::default()),
- tracking: Em::zero(),
- strong: false,
- emph: false,
- monospace: false,
- fallback: true,
- }
- }
-}
-
-/// Font list with family definitions.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct FamilyStyle {
- /// The user-defined list of font families.
- pub list: Vec<FontFamily>,
- /// Definition of serif font families.
- pub serif: Vec<EcoString>,
- /// Definition of sans-serif font families.
- pub sans_serif: Vec<EcoString>,
- /// Definition of monospace font families used for raw text.
- pub monospace: Vec<EcoString>,
- /// Base fonts that are tried as last resort.
- pub base: Vec<EcoString>,
-}
-
-impl Default for FamilyStyle {
- fn default() -> Self {
- Self {
- list: vec![FontFamily::SansSerif],
- serif: vec!["ibm plex serif".into()],
- sans_serif: vec!["ibm plex sans".into()],
- monospace: vec!["ibm plex mono".into()],
- base: vec![
- "ibm plex sans".into(),
- "latin modern math".into(),
- "twitter color emoji".into(),
- ],
- }
- }
-}
-
-/// A generic or named font family.
-#[derive(Clone, Eq, PartialEq, Hash)]
-pub enum FontFamily {
- /// A family that has "serifs", small strokes attached to letters.
- Serif,
- /// A family in which glyphs do not have "serifs", small attached strokes.
- SansSerif,
- /// A family in which (almost) all glyphs are of equal width.
- Monospace,
- /// A specific family with a name.
- Named(EcoString),
-}
-
-impl Debug for FontFamily {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad(match self {
- Self::Serif => "serif",
- Self::SansSerif => "sans-serif",
- Self::Monospace => "monospace",
- Self::Named(s) => s,
- })
- }
-}
-
-/// Whether various kinds of ligatures should appear.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct FontFeatures {
- /// Whether to apply kerning ("kern").
- pub kerning: bool,
- /// Whether the text should use small caps. ("smcp")
- pub smallcaps: bool,
- /// Whether to apply stylistic alternates. ("salt")
- pub alternates: bool,
- /// Which stylistic set to apply. ("ss01" - "ss20")
- pub stylistic_set: Option<StylisticSet>,
- /// Configuration of ligature features.
- pub ligatures: LigatureFeatures,
- /// Configuration of numbers features.
- pub numbers: NumberFeatures,
- /// Raw OpenType features to apply.
- pub raw: Vec<(Tag, u32)>,
-}
-
-impl Default for FontFeatures {
- fn default() -> Self {
- Self {
- kerning: true,
- smallcaps: false,
- alternates: false,
- stylistic_set: None,
- ligatures: LigatureFeatures::default(),
- numbers: NumberFeatures::default(),
- raw: vec![],
- }
- }
-}
-
-/// A stylistic set in a font face.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct StylisticSet(u8);
-
-impl StylisticSet {
- /// Creates a new set, clamping to 1-20.
- pub fn new(index: u8) -> Self {
- Self(index.clamp(1, 20))
- }
-
- /// Get the value, guaranteed to be 1-20.
- pub fn get(self) -> u8 {
- self.0
- }
-}
-
-/// Whether various kinds of ligatures should appear.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct LigatureFeatures {
- /// Standard ligatures. ("liga", "clig")
- pub standard: bool,
- /// Ligatures that should be used sparringly. ("dlig")
- pub discretionary: bool,
- /// Historical ligatures. ("hlig")
- pub historical: bool,
-}
-
-impl Default for LigatureFeatures {
- fn default() -> Self {
- Self {
- standard: true,
- discretionary: false,
- historical: false,
- }
- }
-}
-
-/// Defines the style of numbers.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct NumberFeatures {
- /// Whether to use lining or old-style numbers.
- pub type_: Smart<NumberType>,
- /// Whether to use proportional or tabular numbers.
- pub width: Smart<NumberWidth>,
- /// How to position numbers vertically.
- pub position: NumberPosition,
- /// Whether to have a slash through the zero glyph. ("zero")
- pub slashed_zero: bool,
- /// Whether to convert fractions. ("frac")
- pub fractions: bool,
-}
-
-impl Default for NumberFeatures {
- fn default() -> Self {
- Self {
- type_: Smart::Auto,
- width: Smart::Auto,
- position: NumberPosition::Normal,
- slashed_zero: false,
- fractions: false,
- }
- }
-}
-
-/// Which kind of numbers / figures to select.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum NumberType {
- /// Numbers that fit well with capital text. ("lnum")
- Lining,
- /// Numbers that fit well into flow of upper- and lowercase text. ("onum")
- OldStyle,
-}
-
-/// The width of numbers / figures.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum NumberWidth {
- /// Number widths are glyph specific. ("pnum")
- Proportional,
- /// All numbers are of equal width / monospaced. ("tnum")
- Tabular,
-}
-
-/// How to position numbers.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum NumberPosition {
- /// Numbers are positioned on the same baseline as text.
- Normal,
- /// Numbers are smaller and placed at the bottom. ("subs")
- Subscript,
- /// Numbers are smaller and placed at the top. ("sups")
- Superscript,
-}
diff --git a/src/style/paper.rs b/src/style/paper.rs
deleted file mode 100644
index 252d293d..00000000
--- a/src/style/paper.rs
+++ /dev/null
@@ -1,233 +0,0 @@
-use crate::geom::{Length, Linear, Relative, Sides, Size};
-
-/// Specification of a paper.
-#[derive(Debug, Copy, Clone)]
-pub struct Paper {
- /// The broad class this paper belongs to.
- class: PaperClass,
- /// The width of the paper in millimeters.
- width: f64,
- /// The height of the paper in millimeters.
- height: f64,
-}
-
-/// Defines default margins for a class of related papers.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum PaperClass {
- Custom,
- Base,
- US,
- Newspaper,
- Book,
-}
-
-impl PaperClass {
- /// The default margins for this page class.
- pub fn default_margins(self) -> Sides<Linear> {
- let f = |r| Relative::new(r).into();
- let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b));
- match self {
- Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842),
- Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842),
- Self::US => s(0.1760, 0.1092, 0.1760, 0.0910),
- Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294),
- Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965),
- }
- }
-}
-
-macro_rules! papers {
- ($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => {
- impl Paper {
- /// Parse a paper from its name.
- ///
- /// Both lower and upper case are fine.
- pub fn from_name(name: &str) -> Option<Self> {
- match name.to_lowercase().as_str() {
- $($($pats)* => Some(Self::$var),)*
- _ => None,
- }
- }
-
- /// The class of the paper.
- pub fn class(self) -> PaperClass {
- self.class
- }
-
- /// The size of the paper.
- pub fn size(self) -> Size {
- Size::new(Length::mm(self.width), Length::mm(self.height))
- }
- }
-
- /// Predefined papers.
- ///
- /// Each paper is parsable from its name in kebab-case.
- impl Paper {
- $(papers!(@$var, stringify!($($pats)*), $class, $width, $height);)*
- }
- };
-
- (@$var:ident, $names:expr, $class:ident, $width:expr, $height:expr) => {
- pub const $var: Self = Self {
- class: PaperClass::$class,
- width: $width,
- height: $height,
- };
- };
-}
-
-// All paper sizes in mm.
-//
-// Resources:
-// - https://papersizes.io/
-// - https://en.wikipedia.org/wiki/Paper_size
-// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
-// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
-papers! {
- // ---------------------------------------------------------------------- //
- // ISO 216 A Series
- (A0: Base, 841.0, 1189.0, "a0")
- (A1: Base, 594.0, 841.0, "a1")
- (A2: Base, 420.0, 594.0, "a2")
- (A3: Base, 297.0, 420.0, "a3")
- (A4: Base, 210.0, 297.0, "a4")
- (A5: Base, 148.0, 210.0, "a5")
- (A6: Book, 105.0, 148.0, "a6")
- (A7: Base, 74.0, 105.0, "a7")
- (A8: Base, 52.0, 74.0, "a8")
- (A9: Base, 37.0, 52.0, "a9")
- (A10: Base, 26.0, 37.0, "a10")
- (A11: Base, 18.0, 26.0, "a11")
-
- // ISO 216 B Series
- (ISO_B1: Base, 707.0, 1000.0, "iso-b1")
- (ISO_B2: Base, 500.0, 707.0, "iso-b2")
- (ISO_B3: Base, 353.0, 500.0, "iso-b3")
- (ISO_B4: Base, 250.0, 353.0, "iso-b4")
- (ISO_B5: Book, 176.0, 250.0, "iso-b5")
- (ISO_B6: Book, 125.0, 176.0, "iso-b6")
- (ISO_B7: Base, 88.0, 125.0, "iso-b7")
- (ISO_B8: Base, 62.0, 88.0, "iso-b8")
-
- // ISO 216 C Series
- (ISO_C3: Base, 324.0, 458.0, "iso-c3")
- (ISO_C4: Base, 229.0, 324.0, "iso-c4")
- (ISO_C5: Base, 162.0, 229.0, "iso-c5")
- (ISO_C6: Base, 114.0, 162.0, "iso-c6")
- (ISO_C7: Base, 81.0, 114.0, "iso-c7")
- (ISO_C8: Base, 57.0, 81.0, "iso-c8")
-
- // DIN D Series (extension to ISO)
- (DIN_D3: Base, 272.0, 385.0, "din-d3")
- (DIN_D4: Base, 192.0, 272.0, "din-d4")
- (DIN_D5: Base, 136.0, 192.0, "din-d5")
- (DIN_D6: Base, 96.0, 136.0, "din-d6")
- (DIN_D7: Base, 68.0, 96.0, "din-d7")
- (DIN_D8: Base, 48.0, 68.0, "din-d8")
-
- // SIS (used in academia)
- (SIS_G5: Base, 169.0, 239.0, "sis-g5")
- (SIS_E5: Base, 115.0, 220.0, "sis-e5")
-
- // ANSI Extensions
- (ANSI_A: Base, 216.0, 279.0, "ansi-a")
- (ANSI_B: Base, 279.0, 432.0, "ansi-b")
- (ANSI_C: Base, 432.0, 559.0, "ansi-c")
- (ANSI_D: Base, 559.0, 864.0, "ansi-d")
- (ANSI_E: Base, 864.0, 1118.0, "ansi-e")
-
- // ANSI Architectural Paper
- (ARCH_A: Base, 229.0, 305.0, "arch-a")
- (ARCH_B: Base, 305.0, 457.0, "arch-b")
- (ARCH_C: Base, 457.0, 610.0, "arch-c")
- (ARCH_D: Base, 610.0, 914.0, "arch-d")
- (ARCH_E1: Base, 762.0, 1067.0, "arch-e1")
- (ARCH_E: Base, 914.0, 1219.0, "arch-e")
-
- // JIS B Series
- (JIS_B0: Base, 1030.0, 1456.0, "jis-b0")
- (JIS_B1: Base, 728.0, 1030.0, "jis-b1")
- (JIS_B2: Base, 515.0, 728.0, "jis-b2")
- (JIS_B3: Base, 364.0, 515.0, "jis-b3")
- (JIS_B4: Base, 257.0, 364.0, "jis-b4")
- (JIS_B5: Base, 182.0, 257.0, "jis-b5")
- (JIS_B6: Base, 128.0, 182.0, "jis-b6")
- (JIS_B7: Base, 91.0, 128.0, "jis-b7")
- (JIS_B8: Base, 64.0, 91.0, "jis-b8")
- (JIS_B9: Base, 45.0, 64.0, "jis-b9")
- (JIS_B10: Base, 32.0, 45.0, "jis-b10")
- (JIS_B11: Base, 22.0, 32.0, "jis-b11")
-
- // SAC D Series
- (SAC_D0: Base, 764.0, 1064.0, "sac-d0")
- (SAC_D1: Base, 532.0, 760.0, "sac-d1")
- (SAC_D2: Base, 380.0, 528.0, "sac-d2")
- (SAC_D3: Base, 264.0, 376.0, "sac-d3")
- (SAC_D4: Base, 188.0, 260.0, "sac-d4")
- (SAC_D5: Base, 130.0, 184.0, "sac-d5")
- (SAC_D6: Base, 92.0, 126.0, "sac-d6")
-
- // ISO 7810 ID
- (ISO_ID_1: Base, 85.6, 53.98, "iso-id-1")
- (ISO_ID_2: Base, 74.0, 105.0, "iso-id-2")
- (ISO_ID_3: Base, 88.0, 125.0, "iso-id-3")
-
- // ---------------------------------------------------------------------- //
- // Asia
- (ASIA_F4: Base, 210.0, 330.0, "asia-f4")
-
- // Japan
- (JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4")
- (JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5")
- (JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6")
- (JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4")
- (JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5")
- (JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card")
-
- // China
- (CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card")
-
- // Europe
- (EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card")
-
- // French Traditional (AFNOR)
- (FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière")
- (FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture")
- (FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition")
- (FR_RAISIN: Base, 500.0, 650.0, "fr-raisin")
- (FR_CARRE: Base, 450.0, 560.0, "fr-carré")
- (FR_JESUS: Base, 560.0, 760.0, "fr-jésus")
-
- // United Kingdom Imperial
- (UK_BRIEF: Base, 406.4, 342.9, "uk-brief")
- (UK_DRAFT: Base, 254.0, 406.4, "uk-draft")
- (UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap")
- (UK_QUARTO: Base, 203.2, 254.0, "uk-quarto")
- (UK_CROWN: Base, 508.0, 381.0, "uk-crown")
- (UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a")
- (UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b")
-
- // Unites States
- (US_LETTER: US, 215.9, 279.4, "us-letter")
- (US_LEGAL: US, 215.9, 355.6, "us-legal")
- (US_TABLOID: US, 279.4, 431.8, "us-tabloid")
- (US_EXECUTIVE: US, 184.15, 266.7, "us-executive")
- (US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio")
- (US_STATEMENT: US, 139.7, 215.9, "us-statement")
- (US_LEDGER: US, 431.8, 279.4, "us-ledger")
- (US_OFICIO: US, 215.9, 340.36, "us-oficio")
- (US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter")
- (US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal")
- (US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card")
- (US_DIGEST: Book, 139.7, 215.9, "us-digest")
- (US_TRADE: Book, 152.4, 228.6, "us-trade")
-
- // ---------------------------------------------------------------------- //
- // Other
- (NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact")
- (NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner")
- (NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet")
- (PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9")
- (PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3")
-}
diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs
index 8df25f59..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;
@@ -211,6 +213,8 @@ pub enum Expr {
With(WithExpr),
/// A let expression: `let x = 1`.
Let(LetExpr),
+ /// A set expression: `set text(...)`.
+ Set(SetExpr),
/// An if-else expression: `if x { y } else { z }`.
If(IfExpr),
/// A while loop expression: `while x { y }`.
@@ -238,6 +242,7 @@ impl TypedNode for Expr {
NodeKind::Closure => node.cast().map(Self::Closure),
NodeKind::WithExpr => node.cast().map(Self::With),
NodeKind::LetExpr => node.cast().map(Self::Let),
+ NodeKind::SetExpr => node.cast().map(Self::Set),
NodeKind::IfExpr => node.cast().map(Self::If),
NodeKind::WhileExpr => node.cast().map(Self::While),
NodeKind::ForExpr => node.cast().map(Self::For),
@@ -262,6 +267,7 @@ impl TypedNode for Expr {
Self::Closure(v) => v.as_red(),
Self::With(v) => v.as_red(),
Self::Let(v) => v.as_red(),
+ Self::Set(v) => v.as_red(),
Self::If(v) => v.as_red(),
Self::While(v) => v.as_red(),
Self::For(v) => v.as_red(),
@@ -279,6 +285,7 @@ impl Expr {
Self::Ident(_)
| Self::Call(_)
| Self::Let(_)
+ | Self::Set(_)
| Self::If(_)
| Self::While(_)
| Self::For(_)
@@ -838,6 +845,25 @@ impl LetExpr {
}
node! {
+ /// A set expression: `set text(...)`.
+ SetExpr
+}
+
+impl SetExpr {
+ /// The class to set style properties for.
+ pub fn class(&self) -> Ident {
+ self.0.cast_first_child().expect("set expression is missing class")
+ }
+
+ /// The style properties to set.
+ pub fn args(&self) -> CallArgs {
+ self.0
+ .cast_first_child()
+ .expect("set expression is missing argument list")
+ }
+}
+
+node! {
/// An import expression: `import a, b, c from "utils.typ"`.
ImportExpr
}
diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs
index 22e6cf50..85fbef12 100644
--- a/src/syntax/highlight.rs
+++ b/src/syntax/highlight.rs
@@ -96,22 +96,23 @@ impl Category {
NodeKind::EnDash => Some(Category::Shortcut),
NodeKind::EmDash => Some(Category::Shortcut),
NodeKind::Escape(_) => Some(Category::Escape),
+ NodeKind::Not => Some(Category::Keyword),
+ NodeKind::And => Some(Category::Keyword),
+ NodeKind::Or => Some(Category::Keyword),
+ NodeKind::With => Some(Category::Keyword),
NodeKind::Let => Some(Category::Keyword),
+ NodeKind::Set => Some(Category::Keyword),
NodeKind::If => Some(Category::Keyword),
NodeKind::Else => Some(Category::Keyword),
+ NodeKind::While => Some(Category::Keyword),
NodeKind::For => Some(Category::Keyword),
NodeKind::In => Some(Category::Keyword),
- NodeKind::While => Some(Category::Keyword),
NodeKind::Break => Some(Category::Keyword),
NodeKind::Continue => Some(Category::Keyword),
NodeKind::Return => Some(Category::Keyword),
NodeKind::Import => Some(Category::Keyword),
- NodeKind::Include => Some(Category::Keyword),
NodeKind::From => Some(Category::Keyword),
- NodeKind::Not => Some(Category::Keyword),
- NodeKind::And => Some(Category::Keyword),
- NodeKind::Or => Some(Category::Keyword),
- NodeKind::With => Some(Category::Keyword),
+ NodeKind::Include => Some(Category::Keyword),
NodeKind::Plus => Some(Category::Operator),
NodeKind::Star => Some(Category::Operator),
NodeKind::Slash => Some(Category::Operator),
@@ -139,6 +140,7 @@ impl Category {
Some(Category::Function)
}
NodeKind::WithExpr => Some(Category::Function),
+ NodeKind::SetExpr => Some(Category::Function),
NodeKind::Call => Some(Category::Function),
_ => Some(Category::Variable),
},
@@ -161,21 +163,22 @@ impl Category {
NodeKind::Array => None,
NodeKind::Dict => None,
NodeKind::Named => None,
+ NodeKind::Template => None,
NodeKind::Group => None,
+ NodeKind::Block => None,
NodeKind::Unary => None,
NodeKind::Binary => None,
NodeKind::Call => None,
NodeKind::CallArgs => None,
+ NodeKind::Spread => None,
NodeKind::Closure => None,
NodeKind::ClosureParams => None,
- NodeKind::Spread => None,
- NodeKind::Template => None,
- NodeKind::Block => None,
- NodeKind::ForExpr => None,
- NodeKind::WhileExpr => None,
- NodeKind::IfExpr => None,
- NodeKind::LetExpr => None,
NodeKind::WithExpr => None,
+ NodeKind::LetExpr => None,
+ NodeKind::SetExpr => None,
+ NodeKind::IfExpr => None,
+ NodeKind::WhileExpr => None,
+ NodeKind::ForExpr => None,
NodeKind::ForPattern => None,
NodeKind::ImportExpr => None,
NodeKind::ImportItems => None,
diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs
index e9011a4d..d9ad42a8 100644
--- a/src/syntax/mod.rs
+++ b/src/syntax/mod.rs
@@ -83,19 +83,15 @@ impl Default for Green {
impl Debug for Green {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "{:?}: {}", self.kind(), self.len())?;
- if let Self::Node(n) = self {
- if !n.children.is_empty() {
- f.write_str(" ")?;
- f.debug_list().entries(&n.children).finish()?;
- }
+ match self {
+ Self::Node(node) => node.fmt(f),
+ Self::Token(token) => token.fmt(f),
}
- Ok(())
}
}
/// An inner node in the untyped green tree.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Clone, PartialEq)]
pub struct GreenNode {
/// Node metadata.
data: GreenData,
@@ -145,8 +141,19 @@ impl From<Rc<GreenNode>> for Green {
}
}
+impl Debug for GreenNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.data.fmt(f)?;
+ if !self.children.is_empty() {
+ f.write_str(" ")?;
+ f.debug_list().entries(&self.children).finish()?;
+ }
+ Ok(())
+ }
+}
+
/// Data shared between inner and leaf nodes.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Clone, PartialEq)]
pub struct GreenData {
/// What kind of node this is (each kind would have its own struct in a
/// strongly typed AST).
@@ -178,6 +185,12 @@ impl From<GreenData> for Green {
}
}
+impl Debug for GreenData {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{:?}: {}", self.kind, self.len)
+ }
+}
+
/// A owned wrapper for a green node with span information.
///
/// Owned variant of [`RedRef`]. Can be [cast](Self::cast) to an AST node.
@@ -465,6 +478,8 @@ pub enum NodeKind {
Auto,
/// The `let` keyword.
Let,
+ /// The `set` keyword.
+ Set,
/// The `if` keyword.
If,
/// The `else` keyword.
@@ -552,8 +567,12 @@ pub enum NodeKind {
Dict,
/// A named pair: `thickness: 3pt`.
Named,
+ /// A template expression: `[*Hi* there!]`.
+ Template,
/// A grouped expression: `(1 + 2)`.
Group,
+ /// A block expression: `{ let x = 1; x + 2 }`.
+ Block,
/// A unary operation: `-x`.
Unary,
/// A binary operation: `a + b`.
@@ -562,39 +581,37 @@ pub enum NodeKind {
Call,
/// A function call's argument list: `(x, y)`.
CallArgs,
+ /// Spreaded arguments or a parameter sink: `..x`.
+ Spread,
/// A closure expression: `(x, y) => z`.
Closure,
/// A closure's parameters: `(x, y)`.
ClosureParams,
- /// A parameter sink: `..x`.
- Spread,
- /// A template expression: `[*Hi* there!]`.
- Template,
- /// A block expression: `{ let x = 1; x + 2 }`.
- Block,
- /// A for loop expression: `for x in y { ... }`.
- ForExpr,
- /// A while loop expression: `while x { ... }`.
- WhileExpr,
- /// An if expression: `if x { ... }`.
- IfExpr,
+ /// A with expression: `f with (x, y: 1)`.
+ WithExpr,
/// A let expression: `let x = 1`.
LetExpr,
- /// The `with` expression: `with (1)`.
- WithExpr,
+ /// A set expression: `set text(...)`.
+ SetExpr,
+ /// An if-else expression: `if x { y } else { z }`.
+ IfExpr,
+ /// A while loop expression: `while x { ... }`.
+ WhileExpr,
+ /// A for loop expression: `for x in y { ... }`.
+ ForExpr,
/// A for loop's destructuring pattern: `x` or `x, y`.
ForPattern,
- /// The import expression: `import x from "foo.typ"`.
+ /// An import expression: `import a, b, c from "utils.typ"`.
ImportExpr,
/// Items to import: `a, b, c`.
ImportItems,
- /// The include expression: `include "foo.typ"`.
+ /// An include expression: `include "chapter1.typ"`.
IncludeExpr,
- /// Two slashes followed by inner contents, terminated with a newline:
- /// `//<str>\n`.
+ /// A line comment, two slashes followed by inner contents, terminated with
+ /// a newline: `//<str>\n`.
LineComment,
- /// A slash and a star followed by inner contents, terminated with a star
- /// and a slash: `/*<str>*/`.
+ /// A block comment, a slash and a star followed by inner contents,
+ /// terminated with a star and a slash: `/*<str>*/`.
///
/// The comment can contain nested block comments.
BlockComment,
@@ -616,11 +633,6 @@ pub enum ErrorPos {
}
impl NodeKind {
- /// Whether this is some kind of parenthesis.
- pub fn is_paren(&self) -> bool {
- matches!(self, Self::LeftParen | Self::RightParen)
- }
-
/// Whether this is some kind of bracket.
pub fn is_bracket(&self) -> bool {
matches!(self, Self::LeftBracket | Self::RightBracket)
@@ -631,6 +643,11 @@ impl NodeKind {
matches!(self, Self::LeftBrace | Self::RightBrace)
}
+ /// Whether this is some kind of parenthesis.
+ pub fn is_paren(&self) -> bool {
+ matches!(self, Self::LeftParen | Self::RightParen)
+ }
+
/// Whether this is some kind of error.
pub fn is_error(&self) -> bool {
matches!(self, NodeKind::Error(_, _) | NodeKind::Unknown(_))
@@ -672,6 +689,7 @@ impl NodeKind {
Self::None => "`none`",
Self::Auto => "`auto`",
Self::Let => "keyword `let`",
+ Self::Set => "keyword `set`",
Self::If => "keyword `if`",
Self::Else => "keyword `else`",
Self::For => "keyword `for`",
@@ -712,21 +730,22 @@ impl NodeKind {
Self::Array => "array",
Self::Dict => "dictionary",
Self::Named => "named argument",
+ Self::Template => "template",
Self::Group => "group",
+ Self::Block => "block",
Self::Unary => "unary expression",
Self::Binary => "binary expression",
Self::Call => "call",
Self::CallArgs => "call arguments",
+ Self::Spread => "parameter sink",
Self::Closure => "closure",
Self::ClosureParams => "closure parameters",
- Self::Spread => "parameter sink",
- Self::Template => "template",
- Self::Block => "block",
- Self::ForExpr => "for-loop expression",
- Self::WhileExpr => "while-loop expression",
- Self::IfExpr => "`if` expression",
- Self::LetExpr => "`let` expression",
Self::WithExpr => "`with` expression",
+ Self::LetExpr => "`let` expression",
+ Self::SetExpr => "`set` expression",
+ Self::IfExpr => "`if` expression",
+ Self::WhileExpr => "while-loop expression",
+ Self::ForExpr => "for-loop expression",
Self::ForPattern => "for-loop destructuring pattern",
Self::ImportExpr => "`import` expression",
Self::ImportItems => "import items",
diff --git a/src/syntax/pretty.rs b/src/syntax/pretty.rs
index c453fb56..62ecb8cd 100644
--- a/src/syntax/pretty.rs
+++ b/src/syntax/pretty.rs
@@ -225,6 +225,7 @@ impl Pretty for Expr {
Self::Closure(v) => v.pretty(p),
Self::With(v) => v.pretty(p),
Self::Let(v) => v.pretty(p),
+ Self::Set(v) => v.pretty(p),
Self::If(v) => v.pretty(p),
Self::While(v) => v.pretty(p),
Self::For(v) => v.pretty(p),
@@ -444,6 +445,16 @@ impl Pretty for LetExpr {
}
}
+impl Pretty for SetExpr {
+ fn pretty(&self, p: &mut Printer) {
+ p.push_str("set ");
+ self.class().pretty(p);
+ p.push_str("(");
+ self.args().pretty(p);
+ p.push(')');
+ }
+}
+
impl Pretty for IfExpr {
fn pretty(&self, p: &mut Printer) {
p.push_str("if ");
@@ -639,6 +650,7 @@ mod tests {
// Control flow.
roundtrip("#let x = 1 + 2");
roundtrip("#let f(x) = y");
+ roundtrip("#set text(size: 12pt)");
roundtrip("#if x [y] else [z]");
roundtrip("#if x {} else if y {} else {}");
roundtrip("#while x {y}");
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 6fc1fb59..694c90c8 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -13,18 +13,6 @@ use std::ops::Range;
use std::path::{Component, Path, PathBuf};
use std::rc::Rc;
-/// Additional methods for booleans.
-pub trait BoolExt {
- /// Toggle the value of the bool in place.
- fn flip(&mut self);
-}
-
-impl BoolExt for bool {
- fn flip(&mut self) {
- *self = !*self;
- }
-}
-
/// Additional methods for options.
pub trait OptionExt<T> {
/// Sets `other` as the value if `self` is `None` or if it contains a value
diff --git a/tests/README.md b/tests/README.md
index 57d47fe2..7343d868 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -15,9 +15,19 @@ Running the integration tests (the tests in this directory).
cargo test --test typeset
```
-Running all tests whose names contain the word `filter`.
+Running all tests whose paths contain the string `page` or `stack`.
```bash
-cargo test --test typeset filter
+cargo test --test typeset page stack
+```
+
+Running a test with the exact filename `page.typ`.
+```bash
+cargo test --test typeset -- --exact page.typ
+```
+
+Debug-printing the layout trees for all executed tests.
+```bash
+cargo test --test typeset -- --debug empty.typ
```
To make the integration tests go faster they don't generate PDFs by default.
@@ -39,3 +49,24 @@ oxipng -o max path/to/image.png
# All images
oxipng -r -o max tests/ref
```
+
+## Shorthand for running tests
+If you want to have a quicker way to run the tests, consider adding a shortcut
+to your shell profile so that you can simply write something like:
+```bash
+tests --debug empty.typ
+```
+
+### PowerShell
+Open your PowerShell profile by executing `notepad $profile`.
+```ps
+function tests {
+ cargo test --test typeset -- $args
+}
+```
+
+### Bash
+Open your Bash configuration by executing `nano ~/.bashrc`.
+```bash
+alias tests="cargo test --test typeset --"
+```
diff --git a/tests/ref/elements/circle.png b/tests/ref/elements/circle.png
deleted file mode 100644
index 931debd0..00000000
--- a/tests/ref/elements/circle.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/elements/ellipse.png b/tests/ref/elements/ellipse.png
deleted file mode 100644
index 9a80c4c0..00000000
--- a/tests/ref/elements/ellipse.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/layout/containers.png b/tests/ref/layout/box-block.png
index f6981fe5..f6981fe5 100644
--- a/tests/ref/layout/containers.png
+++ b/tests/ref/layout/box-block.png
Binary files differ
diff --git a/tests/ref/elements/image.png b/tests/ref/layout/image.png
index bdcfd200..bdcfd200 100644
--- a/tests/ref/elements/image.png
+++ b/tests/ref/layout/image.png
Binary files differ
diff --git a/tests/ref/layout/pagebreak.png b/tests/ref/layout/pagebreak.png
index fbb1d00c..a6c0725e 100644
--- a/tests/ref/layout/pagebreak.png
+++ b/tests/ref/layout/pagebreak.png
Binary files differ
diff --git a/tests/ref/layout/background.png b/tests/ref/layout/place-background.png
index 805139f5..805139f5 100644
--- a/tests/ref/layout/background.png
+++ b/tests/ref/layout/place-background.png
Binary files differ
diff --git a/tests/ref/layout/placed.png b/tests/ref/layout/place.png
index 10a0bfd4..4fdb1f70 100644
--- a/tests/ref/layout/placed.png
+++ b/tests/ref/layout/place.png
Binary files differ
diff --git a/tests/ref/layout/aspect.png b/tests/ref/layout/shape-aspect.png
index f76b4980..f76b4980 100644
--- a/tests/ref/layout/aspect.png
+++ b/tests/ref/layout/shape-aspect.png
Binary files differ
diff --git a/tests/ref/layout/shape-circle.png b/tests/ref/layout/shape-circle.png
new file mode 100644
index 00000000..30734cb5
--- /dev/null
+++ b/tests/ref/layout/shape-circle.png
Binary files differ
diff --git a/tests/ref/layout/shape-ellipse.png b/tests/ref/layout/shape-ellipse.png
new file mode 100644
index 00000000..e3c63427
--- /dev/null
+++ b/tests/ref/layout/shape-ellipse.png
Binary files differ
diff --git a/tests/ref/elements/fill-stroke.png b/tests/ref/layout/shape-fill-stroke.png
index 2d04b3dd..2d04b3dd 100644
--- a/tests/ref/elements/fill-stroke.png
+++ b/tests/ref/layout/shape-fill-stroke.png
Binary files differ
diff --git a/tests/ref/elements/rect.png b/tests/ref/layout/shape-rect.png
index b6ab89aa..b6ab89aa 100644
--- a/tests/ref/elements/rect.png
+++ b/tests/ref/layout/shape-rect.png
Binary files differ
diff --git a/tests/ref/elements/square.png b/tests/ref/layout/shape-square.png
index 86bc1ff5..86bc1ff5 100644
--- a/tests/ref/elements/square.png
+++ b/tests/ref/layout/shape-square.png
Binary files differ
diff --git a/tests/ref/layout/spacing.png b/tests/ref/layout/spacing.png
index c653df6d..09f7d6d9 100644
--- a/tests/ref/layout/spacing.png
+++ b/tests/ref/layout/spacing.png
Binary files differ
diff --git a/tests/ref/layout/transform.png b/tests/ref/layout/transform.png
index 5abdef97..2593a3f2 100644
--- a/tests/ref/layout/transform.png
+++ b/tests/ref/layout/transform.png
Binary files differ
diff --git a/tests/ref/markup/heading.png b/tests/ref/markup/heading.png
index c33da420..52911d66 100644
--- a/tests/ref/markup/heading.png
+++ b/tests/ref/markup/heading.png
Binary files differ
diff --git a/tests/ref/style/set-block.png b/tests/ref/style/set-block.png
new file mode 100644
index 00000000..8ee5cfb6
--- /dev/null
+++ b/tests/ref/style/set-block.png
Binary files differ
diff --git a/tests/ref/style/set-site.png b/tests/ref/style/set-site.png
new file mode 100644
index 00000000..affe2e1c
--- /dev/null
+++ b/tests/ref/style/set-site.png
Binary files differ
diff --git a/tests/ref/style/set-toggle.png b/tests/ref/style/set-toggle.png
new file mode 100644
index 00000000..ae8101ca
--- /dev/null
+++ b/tests/ref/style/set-toggle.png
Binary files differ
diff --git a/tests/ref/text/em.png b/tests/ref/text/em.png
new file mode 100644
index 00000000..4c168db6
--- /dev/null
+++ b/tests/ref/text/em.png
Binary files differ
diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png
index 18154a06..20b04a8e 100644
--- a/tests/ref/text/links.png
+++ b/tests/ref/text/links.png
Binary files differ
diff --git a/tests/ref/text/par.png b/tests/ref/text/par.png
index 47f0ed95..03117e67 100644
--- a/tests/ref/text/par.png
+++ b/tests/ref/text/par.png
Binary files differ
diff --git a/tests/ref/text/whitespace.png b/tests/ref/text/whitespace.png
index 7e79c177..36fb2475 100644
--- a/tests/ref/text/whitespace.png
+++ b/tests/ref/text/whitespace.png
Binary files differ
diff --git a/tests/typ/code/call.typ b/tests/typ/code/call.typ
index 2c16af1c..5736c63b 100644
--- a/tests/typ/code/call.typ
+++ b/tests/typ/code/call.typ
@@ -5,7 +5,8 @@
// Ref: true
// Ommitted space.
-[#font(weight:"bold")Bold]
+#let f() = {}
+[#f()*Bold*]
// Call return value of function with body.
#let f(x, body) = (y) => [#x] + body + [#y]
@@ -44,25 +45,25 @@
}
---
-// Error: 2-6 expected function or collection, found boolean
+// Error: 2-6 expected callable or collection, found boolean
{true()}
---
#let x = "x"
-// Error: 1-3 expected function or collection, found string
+// Error: 1-3 expected callable or collection, found string
#x()
---
#let f(x) = x
-// Error: 1-6 expected function or collection, found integer
+// Error: 1-6 expected callable or collection, found integer
#f(1)(2)
---
#let f(x) = x
-// Error: 1-6 expected function or collection, found template
+// Error: 1-6 expected callable or collection, found template
#f[1](2)
---
diff --git a/tests/typ/code/include.typ b/tests/typ/code/include.typ
index 83e00384..1e5d5827 100644
--- a/tests/typ/code/include.typ
+++ b/tests/typ/code/include.typ
@@ -1,7 +1,7 @@
// Test include statements.
---
-#page(width: 200pt)
+#set page(width: 200pt)
= Document
diff --git a/tests/typ/code/ops-invalid.typ b/tests/typ/code/ops-invalid.typ
index 91dd576f..340e4c9f 100644
--- a/tests/typ/code/ops-invalid.typ
+++ b/tests/typ/code/ops-invalid.typ
@@ -26,7 +26,7 @@
{not ()}
---
-// Error: 2-18 cannot apply '<=' to linear and relative
+// Error: 2-18 cannot apply '<=' to relative length and relative
{30% + 1pt <= 40%}
---
diff --git a/tests/typ/code/ops.typ b/tests/typ/code/ops.typ
index 04a72e72..be2cdb48 100644
--- a/tests/typ/code/ops.typ
+++ b/tests/typ/code/ops.typ
@@ -64,7 +64,7 @@
}
// Linears cannot be divided by themselves.
- if type(v) != "linear" {
+ if type(v) != "relative length" {
test(v / v, 1.0)
test(v / v == 1, true)
}
@@ -130,12 +130,14 @@
#test(test == test, true)
#test((() => {}) == (() => {}), false)
-// Templates also compare by identity.
+// Templates compare by shallow equality.
#let t = [a]
#test(t == t, true)
-#test([] == [], false)
+#test([] == [], true)
+#test([a] == [a], true)
#test([] == [a], false)
-#test([a] == [a], false)
+#test([[a]] == [a], false)
+#test(box[] == box[], false)
---
// Test comparison operators.
diff --git a/tests/typ/code/spread.typ b/tests/typ/code/spread.typ
index 41e790a4..5f7d2061 100644
--- a/tests/typ/code/spread.typ
+++ b/tests/typ/code/spread.typ
@@ -4,14 +4,14 @@
---
// Test standard argument overriding.
{
- let font(style: "normal", weight: "regular") = {
+ let f(style: "normal", weight: "regular") = {
"(style: " + style + ", weight: " + weight + ")"
}
- let myfont(..args) = font(weight: "bold", ..args)
- test(myfont(), "(style: normal, weight: bold)")
- test(myfont(weight: "black"), "(style: normal, weight: black)")
- test(myfont(style: "italic"), "(style: italic, weight: bold)")
+ let myf(..args) = f(weight: "bold", ..args)
+ test(myf(), "(style: normal, weight: bold)")
+ test(myf(weight: "black"), "(style: normal, weight: black)")
+ test(myf(style: "italic"), "(style: italic, weight: bold)")
}
---
diff --git a/tests/typ/coma.typ b/tests/typ/coma.typ
index 45d28f9a..ef9e9f86 100644
--- a/tests/typ/coma.typ
+++ b/tests/typ/coma.typ
@@ -1,4 +1,4 @@
-#page(width: 450pt, margins: 1cm)
+#set page(width: 450pt, margins: 1cm)
*Technische Universität Berlin* #h(1fr) *WiSe 2019/2020* \
*Fakultät II, Institut for Mathematik* #h(1fr) Woche 3 \
@@ -8,7 +8,8 @@ Ola Nordmann, John Doe
#v(6mm)
#align(center)[
- ==== 3. Übungsblatt Computerorientierte Mathematik II #v(4mm)
+ ==== 3. Übungsblatt Computerorientierte Mathematik II
+ #v(4mm)
*Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(4mm)
*Alle Antworten sind zu beweisen.*
]
diff --git a/tests/typ/layout/align.typ b/tests/typ/layout/align.typ
index 13b88ac1..09c4dee5 100644
--- a/tests/typ/layout/align.typ
+++ b/tests/typ/layout/align.typ
@@ -1,7 +1,7 @@
// Test alignment.
---
-#page(height: 100pt)
+#set page(height: 100pt)
#stack(dir: ltr,
align(left, square(size: 15pt, fill: eastern)),
align(center, square(size: 20pt, fill: eastern)),
diff --git a/tests/typ/layout/containers.typ b/tests/typ/layout/box-block.typ
index d4556a0f..14258c1e 100644
--- a/tests/typ/layout/containers.typ
+++ b/tests/typ/layout/box-block.typ
@@ -12,7 +12,7 @@ Apart
---
// Test block over multiple pages.
-#page(height: 60pt)
+#set page(height: 60pt)
First!
#block[
diff --git a/tests/typ/layout/grid-1.typ b/tests/typ/layout/grid-1.typ
index 647e366f..a6596c98 100644
--- a/tests/typ/layout/grid-1.typ
+++ b/tests/typ/layout/grid-1.typ
@@ -3,7 +3,7 @@
---
#let cell(width, color) = rect(width: width, height: 2cm, fill: color)
-#page(width: 100pt, height: 140pt)
+#set page(width: 100pt, height: 140pt)
#grid(
columns: (auto, 1fr, 3fr, 0.25cm, 3%, 2mm + 10%),
cell(0.5cm, rgb("2a631a")),
@@ -31,7 +31,7 @@
)
---
-#page(height: 3cm, margins: 0pt)
+#set page(height: 3cm, margins: 0pt)
#grid(
columns: (1fr,),
rows: (1fr, auto, 2fr),
diff --git a/tests/typ/layout/grid-2.typ b/tests/typ/layout/grid-2.typ
index c522a402..7df64a56 100644
--- a/tests/typ/layout/grid-2.typ
+++ b/tests/typ/layout/grid-2.typ
@@ -1,7 +1,7 @@
// Test using the `grid` function to create a finance table.
---
-#page(width: 12cm, height: 2.5cm)
+#set page(width: 12cm, height: 2.5cm)
#grid(
columns: 5,
column-gutter: (2fr, 1fr, 1fr),
diff --git a/tests/typ/layout/grid-3.typ b/tests/typ/layout/grid-3.typ
index 6f7f6977..8d04722e 100644
--- a/tests/typ/layout/grid-3.typ
+++ b/tests/typ/layout/grid-3.typ
@@ -1,7 +1,7 @@
// Test grid cells that overflow to the next region.
---
-#page(width: 5cm, height: 3cm)
+#set page(width: 5cm, height: 3cm)
#grid(
columns: 2,
row-gutter: 8pt,
@@ -18,7 +18,7 @@
---
// Test a column that starts overflowing right after another row/column did
// that.
-#page(width: 5cm, height: 2cm)
+#set page(width: 5cm, height: 2cm)
#grid(
columns: 4 * (1fr,),
row-gutter: 10pt,
@@ -32,7 +32,7 @@
---
// Test two columns in the same row overflowing by a different amount.
-#page(width: 5cm, height: 2cm)
+#set page(width: 5cm, height: 2cm)
#grid(
columns: 3 * (1fr,),
row-gutter: 8pt,
@@ -48,7 +48,7 @@
---
// Test grid within a grid, overflowing.
-#page(width: 5cm, height: 2.25cm)
+#set page(width: 5cm, height: 2.25cm)
#grid(
columns: 4 * (1fr,),
row-gutter: 10pt,
@@ -62,7 +62,7 @@
---
// Test partition of `fr` units before and after multi-region layout.
-#page(width: 5cm, height: 4cm)
+#set page(width: 5cm, height: 4cm)
#grid(
columns: 2 * (1fr,),
rows: (1fr, 2fr, auto, 1fr, 1cm),
diff --git a/tests/typ/layout/grid-4.typ b/tests/typ/layout/grid-4.typ
index d6aa6358..e02d98c5 100644
--- a/tests/typ/layout/grid-4.typ
+++ b/tests/typ/layout/grid-4.typ
@@ -23,7 +23,7 @@
---
// Test that all three kinds of rows use the correct bases.
-#page(height: 4cm, margins: 0cm)
+#set page(height: 4cm, margins: 0cm)
#grid(
rows: (1cm, 1fr, 1fr, auto),
rect(height: 50%, width: 100%, fill: conifer),
diff --git a/tests/typ/layout/grid-5.typ b/tests/typ/layout/grid-5.typ
index 585fc6ce..db7c525a 100644
--- a/tests/typ/layout/grid-5.typ
+++ b/tests/typ/layout/grid-5.typ
@@ -1,7 +1,7 @@
---
// Test that trailing linebreak doesn't overflow the region.
-#page(height: 2cm)
+#set page(height: 2cm)
#grid[
Hello \
Hello \
@@ -12,7 +12,7 @@
---
// Test that broken cell expands vertically.
-#page(height: 2.25cm)
+#set page(height: 2.25cm)
#grid(
columns: 2,
gutter: 10pt,
diff --git a/tests/typ/elements/image.typ b/tests/typ/layout/image.typ
index 7fddb12d..8817713f 100644
--- a/tests/typ/elements/image.typ
+++ b/tests/typ/layout/image.typ
@@ -7,7 +7,7 @@
#image("../../res/rhino.png")
// Load an RGB JPEG image.
-#page(height: 60pt)
+#set page(height: 60pt)
#image("../../res/tiger.jpg")
---
@@ -25,7 +25,7 @@
---
// Test all three fit modes.
-#page(height: 50pt, margins: 0pt)
+#set page(height: 50pt, margins: 0pt)
#grid(
columns: (1fr, 1fr, 1fr),
rows: 100%,
@@ -37,7 +37,7 @@
---
// Does not fit to remaining height of page.
-#page(height: 60pt)
+#set page(height: 60pt)
Stuff \
Stuff
#image("../../res/rhino.png")
diff --git a/tests/typ/layout/pad.typ b/tests/typ/layout/pad.typ
index 1fa0f79d..502137ec 100644
--- a/tests/typ/layout/pad.typ
+++ b/tests/typ/layout/pad.typ
@@ -19,7 +19,7 @@ Hi #box(pad(left: 10pt)[A]) there
---
// Test that the pad node doesn't consume the whole region.
-#page(height: 6cm)
+#set page(height: 6cm)
#align(left)[Before]
#pad(10pt, image("../../res/tiger.jpg"))
#align(right)[After]
diff --git a/tests/typ/layout/page.typ b/tests/typ/layout/page.typ
index dc6c7c81..35f338f4 100644
--- a/tests/typ/layout/page.typ
+++ b/tests/typ/layout/page.typ
@@ -2,33 +2,33 @@
---
// Set width and height.
-#page(width: 80pt, height: 80pt)
-[#page(width: 40pt) High]
-[#page(height: 40pt) Wide]
+#set page(width: 80pt, height: 80pt)
+[#set page(width: 40pt);High]
+[#set page(height: 40pt);Wide]
// Set all margins at once.
[
- #page(margins: 5pt)
+ #set page(margins: 5pt)
#place(top + left)[TL]
#place(bottom + right)[BR]
]
// Set individual margins.
-#page(height: 40pt)
-[#page(left: 0pt) #align(left)[Left]]
-[#page(right: 0pt) #align(right)[Right]]
-[#page(top: 0pt) #align(top)[Top]]
-[#page(bottom: 0pt) #align(bottom)[Bottom]]
+#set page(height: 40pt)
+[#set page(left: 0pt); #align(left)[Left]]
+[#set page(right: 0pt); #align(right)[Right]]
+[#set page(top: 0pt); #align(top)[Top]]
+[#set page(bottom: 0pt); #align(bottom)[Bottom]]
// Ensure that specific margins override general margins.
-[#page(margins: 0pt, left: 20pt) Overriden]
+[#set page(margins: 0pt, left: 20pt); Overriden]
// Flipped predefined paper.
-[#page(paper: "a11", flip: true) Flipped A11]
+[#set page(paper: "a11", flipped: true);Flipped A11]
---
-#page(width: 80pt, height: 40pt, fill: eastern)
-#font(15pt, "Roboto", fill: white, smallcaps: true)[Typst]
+#set page(width: 80pt, height: 40pt, fill: eastern)
+#text(15pt, "Roboto", fill: white, smallcaps: true)[Typst]
-#page(width: 40pt, fill: none, margins: auto, top: 10pt)
+#set page(width: 40pt, fill: none, margins: auto, top: 10pt)
Hi
diff --git a/tests/typ/layout/pagebreak.typ b/tests/typ/layout/pagebreak.typ
index f9a935bc..9a74d2bf 100644
--- a/tests/typ/layout/pagebreak.typ
+++ b/tests/typ/layout/pagebreak.typ
@@ -3,7 +3,8 @@
---
First of two
#pagebreak()
-#page(height: 40pt)
+#set page(height: 40pt)
+Second of two
---
// Make sure that you can't do page related stuff in a container.
@@ -11,7 +12,7 @@ A
#box[
B
#pagebreak()
- #page("a4")
+ #set page("a4")
]
C
@@ -22,13 +23,13 @@ D
---
// Test a combination of pages with bodies and normal content.
-#page(width: 80pt, height: 30pt)
+#set page(width: 80pt, height: 30pt)
-[#page() First]
-[#page() Second]
+Fi[#set page(width: 80pt);rst]
+[#set page(width: 70pt); Second]
#pagebreak()
#pagebreak()
Fourth
-[#page(height: 25pt)]
+#page(height: 20pt)[]
Sixth
-[#page() Seventh]
+[#set page(); Seventh]
diff --git a/tests/typ/layout/background.typ b/tests/typ/layout/place-background.typ
index 6303a83b..f64bf0ee 100644
--- a/tests/typ/layout/background.typ
+++ b/tests/typ/layout/place-background.typ
@@ -1,8 +1,8 @@
// Test placing a background image on a page.
---
-#page(paper: "a10", flip: true)
-#font(fill: white)
+#set page(paper: "a10", flipped: true)
+#set text(fill: white)
#place(
dx: -10pt,
dy: -10pt,
diff --git a/tests/typ/layout/placed.typ b/tests/typ/layout/place.typ
index 334ff80d..527e0559 100644
--- a/tests/typ/layout/placed.typ
+++ b/tests/typ/layout/place.typ
@@ -1,4 +1,7 @@
-#page("a8")
+// Test the `place` function.
+
+---
+#set page("a8")
#place(bottom + center)[© Typst]
= Placement
@@ -20,3 +23,13 @@ the line breaks still had to be inserted manually.
#place(center, dx: 7pt, dy: 5pt)[Hello]
Hello #h(1fr) Hello
]
+
+---
+// Test how the placed node interacts with paragraph spacing around it.
+#set page("a8", height: 60pt)
+
+First
+
+#place(bottom + right)[Placed]
+
+Second
diff --git a/tests/typ/layout/aspect.typ b/tests/typ/layout/shape-aspect.typ
index c9b8ee41..2c3e9b0c 100644
--- a/tests/typ/layout/aspect.typ
+++ b/tests/typ/layout/shape-aspect.typ
@@ -3,14 +3,14 @@
---
// Test relative width and height and size that is smaller
// than default size.
-#page(width: 120pt, height: 70pt)
+#set page(width: 120pt, height: 70pt)
#square(width: 50%, align(bottom)[A])
#square(height: 50%)
#box(stack(square(size: 10pt), 5pt, square(size: 10pt, [B])))
---
// Test alignment in automatically sized square and circle.
-#font(8pt)
+#set text(8pt)
#square(padding: 4pt)[
Hey there, #align(center + bottom, rotate(180deg, [you!]))
]
@@ -23,19 +23,19 @@
---
// Test square that is limited by region size.
-#page(width: 20pt, height: 10pt, margins: 0pt)
+#set page(width: 20pt, height: 10pt, margins: 0pt)
#stack(dir: ltr, square(fill: forest), square(fill: conifer))
---
// Test different ways of sizing.
-#page(width: 120pt, height: 40pt)
+#set page(width: 120pt, height: 40pt)
#circle(radius: 5pt)
#circle(width: 10%)
#circle(height: 50%)
---
// Test square that is overflowing due to its aspect ratio.
-#page(width: 40pt, height: 20pt, margins: 5pt)
+#set page(width: 40pt, height: 20pt, margins: 5pt)
#square(width: 100%)
#square(width: 100%)[Hello]
diff --git a/tests/typ/elements/circle.typ b/tests/typ/layout/shape-circle.typ
index ad9d3a4e..8b795830 100644
--- a/tests/typ/elements/circle.typ
+++ b/tests/typ/layout/shape-circle.typ
@@ -23,7 +23,7 @@ Center-aligned rect in auto-sized circle.
Rect in auto-sized circle. \
#circle(fill: forest,
rect(fill: conifer, stroke: white, padding: 4pt)[
- #font(8pt)
+ #set text(8pt)
But, soft! what light through yonder window breaks?
]
)
@@ -38,7 +38,7 @@ Expanded by height.
---
// Test relative sizing.
#let centered(body) = align(center + horizon, body)
-#font(fill: white)
+#set text(fill: white)
#rect(width: 100pt, height: 50pt, fill: rgb("aaa"), centered[
#circle(radius: 10pt, fill: eastern, centered[A]) // D=20pt
#circle(height: 60%, fill: eastern, centered[B]) // D=30pt
diff --git a/tests/typ/elements/ellipse.typ b/tests/typ/layout/shape-ellipse.typ
index 39c73a6f..154144c4 100644
--- a/tests/typ/elements/ellipse.typ
+++ b/tests/typ/layout/shape-ellipse.typ
@@ -18,6 +18,6 @@ Rect in ellipse in fixed rect. \
Auto-sized ellipse. \
#ellipse(fill: conifer, stroke: forest, thickness: 3pt, padding: 3pt)[
- #font(8pt)
+ #set text(8pt)
But, soft! what light through yonder window breaks?
]
diff --git a/tests/typ/elements/fill-stroke.typ b/tests/typ/layout/shape-fill-stroke.typ
index 3ae5f987..3ae5f987 100644
--- a/tests/typ/elements/fill-stroke.typ
+++ b/tests/typ/layout/shape-fill-stroke.typ
diff --git a/tests/typ/elements/rect.typ b/tests/typ/layout/shape-rect.typ
index b3d4d286..add39b80 100644
--- a/tests/typ/elements/rect.typ
+++ b/tests/typ/layout/shape-rect.typ
@@ -5,7 +5,7 @@
#rect()
---
-#page(width: 150pt)
+#set page(width: 150pt)
// Fit to text.
#rect(fill: conifer, padding: 3pt)[Textbox]
diff --git a/tests/typ/elements/square.typ b/tests/typ/layout/shape-square.typ
index f09fc3e0..c4ece778 100644
--- a/tests/typ/elements/square.typ
+++ b/tests/typ/layout/shape-square.typ
@@ -8,7 +8,7 @@
---
// Test auto-sized square.
#square(fill: eastern, padding: 5pt)[
- #font(fill: white, weight: "bold")
+ #set text(fill: white, weight: "bold")
Typst
]
@@ -21,14 +21,14 @@
---
// Test text overflowing height.
-#page(width: 75pt, height: 100pt)
+#set page(width: 75pt, height: 100pt)
#square(fill: conifer)[
But, soft! what light through yonder window breaks?
]
---
// Test that square does not overflow page.
-#page(width: 100pt, height: 75pt)
+#set page(width: 100pt, height: 75pt)
#square(fill: conifer)[
But, soft! what light through yonder window breaks?
]
diff --git a/tests/typ/layout/spacing.typ b/tests/typ/layout/spacing.typ
index 7cf6760f..37aa8eaa 100644
--- a/tests/typ/layout/spacing.typ
+++ b/tests/typ/layout/spacing.typ
@@ -18,6 +18,23 @@ Add #h(10pt) #h(10pt) up
| #h(1fr) | #h(2fr) | #h(1fr) |
---
+// Test spacing collapsing with parbreaks.
+#v(0pt)
+A
+#v(0pt)
+B
+#v(0pt)
+
+C #parbreak() D
+
+---
+// Test that spacing can carry paragraph and page style properties.
+
+A[#set par(align: right);#h(1cm)]B
+[#set page(height: 20pt);#v(1cm)]
+B
+
+---
// Missing spacing.
// Error: 11-13 missing argument: spacing
Totally #h() ignored
diff --git a/tests/typ/layout/stack-1.typ b/tests/typ/layout/stack-1.typ
index 02083d2b..a4a0d6b8 100644
--- a/tests/typ/layout/stack-1.typ
+++ b/tests/typ/layout/stack-1.typ
@@ -15,13 +15,13 @@
#let items = for w in widths { (align(right, shaded(w)),) }
-#page(width: 50pt, margins: 0pt)
+#set page(width: 50pt, margins: 0pt)
#stack(dir: btt, ..items)
---
// Test RTL alignment.
-#page(width: 50pt, margins: 5pt)
-#font(8pt)
+#set page(width: 50pt, margins: 5pt)
+#set text(8pt)
#stack(dir: rtl,
align(center, [A]),
align(left, [B]),
@@ -30,8 +30,8 @@
---
// Test spacing.
-#page(width: 50pt, margins: 0pt)
-#par(spacing: 5pt)
+#set page(width: 50pt, margins: 0pt)
+#set par(spacing: 5pt)
#let x = square(size: 10pt, fill: eastern)
#stack(dir: rtl, spacing: 5pt, x, x, x)
@@ -40,7 +40,7 @@
---
// Test overflow.
-#page(width: 50pt, height: 30pt, margins: 0pt)
+#set page(width: 50pt, height: 30pt, margins: 0pt)
#box(stack(
rect(width: 40pt, height: 20pt, fill: conifer),
rect(width: 30pt, height: 13pt, fill: forest),
diff --git a/tests/typ/layout/stack-2.typ b/tests/typ/layout/stack-2.typ
index 2167f48f..f88f7a58 100644
--- a/tests/typ/layout/stack-2.typ
+++ b/tests/typ/layout/stack-2.typ
@@ -1,7 +1,7 @@
// Test fr units in stacks.
---
-#page(height: 3.5cm)
+#set page(height: 3.5cm)
#stack(
dir: ltr,
spacing: 1fr,
@@ -15,8 +15,8 @@ from #h(1fr) the #h(1fr) wonderful
World! 🌍
---
-#page(height: 2cm)
-#font(white)
+#set page(height: 2cm)
+#set text(white)
#rect(fill: forest)[
#v(1fr)
#h(1fr) Hi you! #h(5pt)
diff --git a/tests/typ/layout/transform.typ b/tests/typ/layout/transform.typ
index 5b1fa2a1..82ee1390 100644
--- a/tests/typ/layout/transform.typ
+++ b/tests/typ/layout/transform.typ
@@ -23,13 +23,13 @@
[X]
}
-#font("Latin Modern Math", size)
+#set text("Latin Modern Math", size)
Neither #tex, \
nor #xetex!
---
// Test combination of scaling and rotation.
-#page(height: 80pt)
+#set page(height: 80pt)
#align(center + horizon,
rotate(20deg, scale(70%, image("../../res/tiger.jpg")))
)
@@ -43,7 +43,7 @@ nor #xetex!
---
// Test setting scaling origin.
#let r = rect(width: 100pt, height: 10pt, fill: forest)
-#page(height: 65pt)
+#set page(height: 65pt)
#scale(r, x: 50%, y: 200%, origin: left + top)
#scale(r, x: 50%, origin: center)
#scale(r, x: 50%, y: 200%, origin: right + bottom)
diff --git a/tests/typ/markup/heading.typ b/tests/typ/markup/heading.typ
index cb022617..2ae97aa8 100644
--- a/tests/typ/markup/heading.typ
+++ b/tests/typ/markup/heading.typ
@@ -39,3 +39,12 @@ is not.
= A {
"B"
}
+
+---
+// Test styling.
+= Heading
+
+#set heading(family: "Roboto", fill: eastern)
+
+===== Heading 🌍
+#heading(level: 5)[Heading]
diff --git a/tests/typ/style/set-block.typ b/tests/typ/style/set-block.typ
new file mode 100644
index 00000000..f260acdc
--- /dev/null
+++ b/tests/typ/style/set-block.typ
@@ -0,0 +1,10 @@
+// Test set in code blocks.
+
+---
+// Test that template in block is not affected by set
+// rule in block ...
+A{set text(fill: eastern); [B]}C
+
+---
+// ... no matter the order.
+A{[B]; set text(fill: eastern)}C
diff --git a/tests/typ/style/set-site.typ b/tests/typ/style/set-site.typ
new file mode 100644
index 00000000..0a00e199
--- /dev/null
+++ b/tests/typ/style/set-site.typ
@@ -0,0 +1,30 @@
+// Test that set affects the instantiation site and not the
+// definition site of a template.
+
+---
+// Test that text is affected by instantiation-site bold.
+#let x = [World]
+Hello *{x}*
+
+---
+// Test that lists are affected by correct indents.
+#set par(spacing: 4pt)
+#let fruit = [
+ - Apple
+ - Orange
+ #set list(body-indent: 10pt)
+ - Pear
+]
+
+- Fruit
+[#set list(label-indent: 10pt)
+ #fruit]
+- No more fruit
+
+---
+// Test that that par spacing and text style are respected from
+// the outside, but the more specific fill is respected.
+#set par(spacing: 4pt)
+#set text(style: "italic", fill: eastern)
+#let x = [And the forest #parbreak() lay silent!]
+#text(fill: forest, x)
diff --git a/tests/typ/style/set-toggle.typ b/tests/typ/style/set-toggle.typ
new file mode 100644
index 00000000..9f26bdf7
--- /dev/null
+++ b/tests/typ/style/set-toggle.typ
@@ -0,0 +1,10 @@
+// Test set rules for toggleable booleans.
+
+---
+// Test toggling and untoggling.
+*AB_C*DE
+*_*
+
+---
+// Test toggling and nested templates.
+*A[B*[_C]]D*E
diff --git a/tests/typ/text/baseline.typ b/tests/typ/text/baseline.typ
index f22fce08..7100ab52 100644
--- a/tests/typ/text/baseline.typ
+++ b/tests/typ/text/baseline.typ
@@ -1,4 +1,4 @@
// Test text baseline.
---
-Hi #font(150%)[You], #font(75%)[how are you?]
+Hi #text(150%)[You], #text(75%)[how are you?]
diff --git a/tests/typ/text/basic.typ b/tests/typ/text/basic.typ
index 0c6c8896..a074a046 100644
--- a/tests/typ/text/basic.typ
+++ b/tests/typ/text/basic.typ
@@ -1,7 +1,7 @@
// Test simple text.
---
-#page(width: 250pt, height: 120pt)
+#set page(width: 250pt, height: 120pt)
But, soft! what light through yonder window breaks? It is the east, and Juliet
is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and
diff --git a/tests/typ/text/bidi.typ b/tests/typ/text/bidi.typ
index 7d33aeea..658c7fa6 100644
--- a/tests/typ/text/bidi.typ
+++ b/tests/typ/text/bidi.typ
@@ -2,55 +2,55 @@
---
// Test reordering with different top-level paragraph directions.
-#let text = [Text טֶקסט]
-#font(serif, "Noto Serif Hebrew")
-#par(lang: "he") {text}
-#par(lang: "de") {text}
+#let content = [Text טֶקסט]
+#set text(serif, "Noto Serif Hebrew")
+#par(lang: "he", content)
+#par(lang: "de", content)
---
// Test that consecutive, embedded LTR runs stay LTR.
// Here, we have two runs: "A" and italic "B".
-#let text = [أنت A_B_مطرC]
-#font(serif, "Noto Sans Arabic")
-#par(lang: "ar") {text}
-#par(lang: "de") {text}
+#let content = [أنت A_B_مطرC]
+#set text(serif, "Noto Sans Arabic")
+#par(lang: "ar", content)
+#par(lang: "de", content)
---
// Test that consecutive, embedded RTL runs stay RTL.
// Here, we have three runs: "גֶ", bold "שֶׁ", and "ם".
-#let text = [Aגֶ*שֶׁ*םB]
-#font(serif, "Noto Serif Hebrew")
-#par(lang: "he") {text}
-#par(lang: "de") {text}
+#let content = [Aגֶ*שֶׁ*םB]
+#set text(serif, "Noto Serif Hebrew")
+#par(lang: "he", content)
+#par(lang: "de", content)
---
// Test embedding up to level 4 with isolates.
-#font(serif, "Noto Serif Hebrew", "Twitter Color Emoji")
-#par(dir: rtl)
+#set text(serif, "Noto Serif Hebrew", "Twitter Color Emoji")
+#set par(dir: rtl)
א\u{2066}A\u{2067}Bב\u{2069}?
---
// Test hard line break (leads to two paragraphs in unicode-bidi).
-#font("Noto Sans Arabic", serif)
-#par(lang: "ar")
+#set text("Noto Sans Arabic", serif)
+#set par(lang: "ar")
Life المطر هو الحياة \
الحياة تمطر is rain.
---
// Test spacing.
-#font(serif, "Noto Serif Hebrew")
+#set text(serif, "Noto Serif Hebrew")
L #h(1cm) ריווחR \
Lריווח #h(1cm) R
---
// Test inline object.
-#font("Noto Serif Hebrew", serif)
-#par(lang: "he")
+#set text("Noto Serif Hebrew", serif)
+#set par(lang: "he")
קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים
---
// Test setting a vertical direction.
// Ref: false
-// Error: 11-14 must be horizontal
-#par(dir: ttb)
+// Error: 15-18 must be horizontal
+#set par(dir: ttb)
diff --git a/tests/typ/text/chinese.typ b/tests/typ/text/chinese.typ
index 0800a220..04fa7027 100644
--- a/tests/typ/text/chinese.typ
+++ b/tests/typ/text/chinese.typ
@@ -1,7 +1,7 @@
// Test chinese text from Wikipedia.
---
-#font("Noto Serif CJK SC")
+#set text("Noto Serif CJK SC")
是美国广播公司电视剧《迷失》第3季的第22和23集,也是全剧的第71集和72集
由执行制作人戴蒙·林道夫和卡尔顿·库斯编剧,导演则是另一名执行制作人杰克·本德
diff --git a/tests/typ/text/decorations.typ b/tests/typ/text/decorations.typ
index 9cd7b096..14dfe821 100644
--- a/tests/typ/text/decorations.typ
+++ b/tests/typ/text/decorations.typ
@@ -13,7 +13,7 @@
#underline(red)[Critical information is conveyed here.]
// Inherits font color.
-#font(fill: red, underline[Change with the wind.])
+#text(fill: red, underline[Change with the wind.])
// Both over- and underline.
#overline(underline[Running amongst the wolves.])
diff --git a/tests/typ/text/em.typ b/tests/typ/text/em.typ
new file mode 100644
index 00000000..d9b00f06
--- /dev/null
+++ b/tests/typ/text/em.typ
@@ -0,0 +1,17 @@
+// Test font-relative sizing.
+
+---
+#set text(size: 5pt)
+A // 5pt
+[
+ #set text(size: 200%)
+ B // 10pt
+ [
+ #set text(size: 150% + 1pt)
+ C // 16pt
+ #text(size: 200%)[D] // 32pt
+ E // 16pt
+ ]
+ F // 10pt
+]
+G // 5pt
diff --git a/tests/typ/text/features.typ b/tests/typ/text/features.typ
index d60583d8..3f61873e 100644
--- a/tests/typ/text/features.typ
+++ b/tests/typ/text/features.typ
@@ -2,72 +2,73 @@
---
// Test turning kerning off.
-#font(kerning: true)[Tq] \
-#font(kerning: false)[Tq]
+#text(kerning: true)[Tq] \
+#text(kerning: false)[Tq]
---
// Test smallcaps.
-#font("Roboto")
-#font(smallcaps: true)[Smallcaps]
+#set text("Roboto")
+#text(smallcaps: true)[Smallcaps]
---
// Test alternates and stylistic sets.
-#font("IBM Plex Serif")
-a vs #font(alternates: true)[a] \
-ß vs #font(stylistic-set: 5)[ß]
+#set text("IBM Plex Serif")
+a vs #text(alternates: true)[a] \
+ß vs #text(stylistic-set: 5)[ß]
---
// Test ligatures.
-fi vs. #font(ligatures: false)[No fi] \
+fi vs. #text(ligatures: false)[No fi] \
---
// Test number type.
-#font("Roboto")
-#font(number-type: "old-style") 0123456789 \
-#font(number-type: auto)[0123456789]
+#set text("Roboto")
+#set text(number-type: "old-style")
+0123456789 \
+#text(number-type: auto)[0123456789]
---
// Test number width.
-#font("Roboto")
-#font(number-width: "proportional")[0123456789] \
-#font(number-width: "tabular")[3456789123] \
-#font(number-width: "tabular")[0123456789]
+#set text("Roboto")
+#text(number-width: "proportional")[0123456789] \
+#text(number-width: "tabular")[3456789123] \
+#text(number-width: "tabular")[0123456789]
---
// Test number position.
-#font("IBM Plex Sans")
-#font(number-position: "normal")[C2H4] \
-#font(number-position: "subscript")[C2H4] \
-#font(number-position: "superscript")[C2H4]
+#set text("IBM Plex Sans")
+#text(number-position: "normal")[C2H4] \
+#text(number-position: "subscript")[C2H4] \
+#text(number-position: "superscript")[C2H4]
---
// Test extra number stuff.
-#font("IBM Plex Sans")
-0 vs. #font(slashed-zero: true)[0] \
-1/2 vs. #font(fractions: true)[1/2]
+#set text("IBM Plex Sans")
+0 vs. #text(slashed-zero: true)[0] \
+1/2 vs. #text(fractions: true)[1/2]
---
// Test raw features.
-#font("Roboto")
-#font(features: ("smcp",))[Smcp] \
-fi vs. #font(features: (liga: 0))[No fi]
+#set text("Roboto")
+#text(features: ("smcp",))[Smcp] \
+fi vs. #text(features: (liga: 0))[No fi]
---
-// Error: 22-27 expected integer or none, found boolean
-#font(stylistic-set: false)
+// Error: 26-31 expected integer or none, found boolean
+#set text(stylistic-set: false)
---
-// Error: 22-24 must be between 1 and 20
-#font(stylistic-set: 25)
+// Error: 26-28 must be between 1 and 20
+#set text(stylistic-set: 25)
---
-// Error: 20-21 expected string or auto, found integer
-#font(number-type: 2)
+// Error: 24-25 expected string or auto, found integer
+#set text(number-type: 2)
---
-// Error: 20-31 expected "lining" or "old-style"
-#font(number-type: "different")
+// Error: 24-35 expected "lining" or "old-style"
+#set text(number-type: "different")
---
-// Error: 17-22 expected array of strings or dictionary mapping tags to integers, found boolean
-#font(features: false)
+// Error: 21-26 expected array of strings or dictionary mapping tags to integers, found boolean
+#set text(features: false)
diff --git a/tests/typ/text/font.typ b/tests/typ/text/font.typ
index 5c97d367..71ac8d3d 100644
--- a/tests/typ/text/font.typ
+++ b/tests/typ/text/font.typ
@@ -2,57 +2,57 @@
---
// Set same font size in three different ways.
-#font(20pt)[A]
-#font(200%)[A]
-#font(size: 15pt + 50%)[A]
+#text(20pt)[A]
+#text(200%)[A]
+#text(size: 15pt + 50%)[A]
// Do nothing.
-#font()[Normal]
+#text()[Normal]
// Set style (is available).
-#font(style: "italic")[Italic]
+#text(style: "italic")[Italic]
// Set weight (is available).
-#font(weight: "bold")[Bold]
+#text(weight: "bold")[Bold]
// Set stretch (not available, matching closest).
-#font(stretch: 50%)[Condensed]
+#text(stretch: 50%)[Condensed]
// Set family.
-#font(family: serif)[Serif]
+#text(family: serif)[Serif]
// Emoji.
Emoji: 🐪, 🌋, 🏞
// Math.
-#font("Latin Modern Math")[∫ 𝛼 + 3𝛽 d𝑡]
+#text("Latin Modern Math")[∫ 𝛼 + 3𝛽 d𝑡]
// Colors.
[
- #font(fill: eastern)
- This is #font(rgb("FA644B"))[way more] colorful.
+ #set text(fill: eastern)
+ This is #text(rgb("FA644B"))[way more] colorful.
]
// Disable font fallback beyond the user-specified list.
// Without disabling, Latin Modern Math would come to the rescue.
-#font("PT Sans", "Twitter Color Emoji", fallback: false)
+#set text("PT Sans", "Twitter Color Emoji", fallback: false)
2π = 𝛼 + 𝛽. ✅
---
// Test class definitions.
-#font(sans-serif: "PT Sans")
-#font(family: sans-serif)[Sans-serif.] \
-#font(monospace)[Monospace.] \
-#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.]
+#set text(sans-serif: "PT Sans")
+#text(family: sans-serif)[Sans-serif.] \
+#text(monospace)[Monospace.] \
+#text(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.]
---
// Test top and bottom edge.
-#page(width: 150pt)
-#font(size: 8pt)
+#set page(width: 150pt)
+#set text(size: 8pt)
#let try(top, bottom) = rect(fill: conifer)[
- #font(monospace, top-edge: top, bottom-edge: bottom)
+ #set text(monospace, top-edge: top, bottom-edge: bottom)
From #top to #bottom
]
@@ -64,33 +64,33 @@ Emoji: 🐪, 🌋, 🏞
#try(1pt + 27%, -18%)
---
-// Error: 7-12 unexpected argument
-#font(false)
+// Error: 11-16 unexpected argument
+#set text(false)
---
-// Error: 14-20 expected "normal", "italic" or "oblique"
-#font(style: "bold", weight: "thin")
+// Error: 18-24 expected "normal", "italic" or "oblique"
+#set text(style: "bold", weight: "thin")
---
-// Error: 17-19 expected linear or string, found array
-#font(top-edge: ())
+// Error: 21-23 expected linear or string, found array
+#set text(top-edge: ())
---
-// Error: 17-19 unknown font metric
-#font(top-edge: "")
+// Error: 21-23 unknown font metric
+#set text(top-edge: "")
---
-// Error: 14-15 expected string or array of strings, found integer
-#font(serif: 0)
+// Error: 18-19 expected string or array of strings, found integer
+#set text(serif: 0)
---
-// Error: 19-23 unexpected argument
-#font(size: 10pt, 12pt)
+// Error: 23-27 unexpected argument
+#set text(size: 10pt, 12pt)
---
-// Error: 28-35 unexpected argument
-#font(family: "Helvetica", "Arial")
+// Error: 32-39 unexpected argument
+#set text(family: "Helvetica", "Arial")
---
-// Error: 7-27 unexpected argument
-#font(something: "invalid")
+// Error: 11-31 unexpected argument
+#set text(something: "invalid")
diff --git a/tests/typ/text/links.typ b/tests/typ/text/links.typ
index e5f7affc..f2da8b9c 100644
--- a/tests/typ/text/links.typ
+++ b/tests/typ/text/links.typ
@@ -12,11 +12,18 @@ Contact #link("mailto:hi@typst.app") or call #link("tel:123") for more informati
---
// Styled with underline and color.
-#let link(url, body) = link(url, font(fill: rgb("283663"), underline(body)))
+#let link(url, body) = link(url, text(fill: rgb("283663"), underline(body)))
You could also make the #link("https://html5zombo.com/")[link look way more typical.]
---
// Transformed link.
-#page(height: 60pt)
+#set page(height: 60pt)
#let link = link("https://typst.app/")[LINK]
My cool #move(x: 0.7cm, y: 0.7cm, rotate(10deg, scale(200%, link)))
+
+---
+// Link containing a block.
+#link("https://example.com/", block[
+ My cool rhino
+ #move(x: 10pt, image("../../res/rhino.png", width: 1cm))
+])
diff --git a/tests/typ/text/par.typ b/tests/typ/text/par.typ
index 7b6bd289..8bd43deb 100644
--- a/tests/typ/text/par.typ
+++ b/tests/typ/text/par.typ
@@ -2,20 +2,47 @@
---
// Test ragged-left.
-#par(align: right)
+#set par(align: right)
To the right! Where the sunlight peeks behind the mountain.
---
+// Test that explicit paragraph break respects active styles.
+#set par(spacing: 7pt)
+[#set par(spacing: 100pt);First]
+
+[#set par(spacing: 100pt);Second]
+#set par(spacing: 20pt)
+
+Third
+
+---
+// Test that paragraph spacing uses correct set rule.
+Hello
+
+#set par(spacing: 100pt)
+World
+#set par(spacing: 0pt)
+
+You
+
+---
+// Test that paragraph break due to incompatibility respects
+// spacing defined by the two adjacent paragraphs.
+#let a = [#set par(spacing: 40pt);Hello]
+#let b = [#set par(spacing: 60pt);World]
+{a}{b}
+
+---
// Test weird metrics.
-#par(spacing: 100%, leading: 0pt)
+#set par(spacing: 100%, leading: 0pt)
But, soft! what light through yonder window breaks?
It is the east, and Juliet is the sun.
---
-// Error: 13-16 must be horizontal
-#par(align: top)
+// Error: 17-20 must be horizontal
+#set par(align: top)
---
-// Error: 13-29 expected alignment, found 2d alignment
-#par(align: horizon + center)
+// Error: 17-33 expected alignment, found 2d alignment
+#set par(align: horizon + center)
diff --git a/tests/typ/text/shaping.typ b/tests/typ/text/shaping.typ
index 1a8a7933..bb8f4ce9 100644
--- a/tests/typ/text/shaping.typ
+++ b/tests/typ/text/shaping.typ
@@ -7,11 +7,11 @@
Le fira
// This should just shape nicely.
-#font("Noto Sans Arabic")
+#set text("Noto Sans Arabic")
دع النص يمطر عليك
// This should form a three-member family.
-#font("Twitter Color Emoji")
+#set text("Twitter Color Emoji")
👩‍👩‍👦 🤚🏿
// These two shouldn't be affected by a zero-width joiner.
@@ -20,7 +20,7 @@ Le fira
---
// Test font fallback.
-#font(sans-serif, "Noto Sans Arabic", "Twitter Color Emoji")
+#set text(sans-serif, "Noto Sans Arabic", "Twitter Color Emoji")
// Font fallback for emoji.
A😀B
@@ -40,6 +40,6 @@ A🐈中文B
---
// Test reshaping.
-#font("Noto Serif Hebrew")
-#par(lang: "he")
+#set text("Noto Serif Hebrew")
+#set par(lang: "he")
ס \ טֶ
diff --git a/tests/typ/text/tracking.typ b/tests/typ/text/tracking.typ
index 695e6734..e3ff70ff 100644
--- a/tests/typ/text/tracking.typ
+++ b/tests/typ/text/tracking.typ
@@ -1,12 +1,12 @@
// Test tracking characters apart or together.
---
-#font(tracking: -0.01)
+#set text(tracking: -0.01)
I saw Zoe yӛsterday, on the tram.
---
-I'm in#font(tracking: 0.3)[ spaace]!
+I'm in#text(tracking: 0.3)[ spaace]!
---
-#font("Noto Serif Hebrew", tracking: 0.3)
+#set text("Noto Serif Hebrew", tracking: 0.3)
טֶקסט
diff --git a/tests/typ/text/whitespace.typ b/tests/typ/text/whitespace.typ
index e01b047c..831a5543 100644
--- a/tests/typ/text/whitespace.typ
+++ b/tests/typ/text/whitespace.typ
@@ -1,43 +1,36 @@
// Test whitespace handling.
---
-// Spacing around let.
+// Spacing around code constructs.
A#let x = 1;B #test(x, 1) \
-A #let x = 2;B #test(x, 2) \
-A#let x = 3; B #test(x, 3)
+C #let x = 2;D #test(x, 2) \
+E#if true [F]G \
+H #if true{"I"} J \
+K #if true [L] else []M \
+#let c = true; N#while c [{c = false}O] P \
+#let c = true; Q #while c { c = false; "R" } S \
+T#for _ in (none,) {"U"}V
---
-// Spacing around if-else.
-A#if true [B]C \
-A#if true [B] C \
-A #if true{"B"}C \
-A #if true{"B"} C \
-A#if false [] else [B]C \
-A#if true [B] else [] C
-
----
-// Spacing around while loop.
-#let c = true; A#while c [{c = false}B]C \
-#let c = true; A#while c [{c = false}B] C \
-#let c = true; A #while c { c = false; "B" }C \
-#let c = true; A #while c { c = false; "B" } C
-
----
-// Spacing around for loop.
-A#for _ in (none,) [B]C \
-A#for _ in (none,) [B] C \
-A #for _ in (none,) {"B"}C
+// Test spacing with comments.
+A/**/B/**/C \
+A /**/ B/**/C \
+A /**/B/**/ C
---
// Test that a run consisting only of whitespace isn't trimmed.
-A[#font(serif) ]B
+A[#set text(serif); ]B
---
// Test font change after space.
-Left [#font(serif)Right].
+Left [#set text(serif);Right].
+
+---
+// Test that linebreak consumed surrounding spaces.
+#align(center)[A \ B \ C]
---
-// Test that space at start of line is not trimmed.
+// Test that space at start of non-backslash-linebreak line isn't trimmed.
A{"\n"} B
---
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 792251c3..c53250b3 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -12,20 +12,17 @@ use usvg::FitTo;
use walkdir::WalkDir;
use typst::diag::Error;
-use typst::eval::{Smart, Value};
+use typst::eval::{Smart, Styles, Value};
use typst::font::Face;
use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
-use typst::geom::{
- self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size, Transform,
-};
+use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Size, Transform};
use typst::image::{Image, RasterImage, Svg};
-use typst::layout::layout;
#[cfg(feature = "layout-cache")]
-use typst::library::DocumentNode;
+use typst::layout::RootNode;
+use typst::library::{PageNode, TextNode};
use typst::loading::FsLoader;
use typst::parse::Scanner;
use typst::source::SourceFile;
-use typst::style::Style;
use typst::syntax::Span;
use typst::Context;
@@ -52,7 +49,7 @@ fn main() {
continue;
}
- if args.matches(&src_path.to_string_lossy()) {
+ if args.matches(&src_path) {
filtered.push(src_path);
}
}
@@ -64,12 +61,17 @@ fn main() {
println!("Running {} tests", len);
}
- // We want to have "unbounded" pages, so we allow them to be infinitely
- // large and fit them to match their content.
- let mut style = Style::default();
- style.page_mut().size = Size::new(Length::pt(120.0), Length::inf());
- style.page_mut().margins = Sides::splat(Smart::Custom(Length::pt(10.0).into()));
- style.text_mut().size = Length::pt(10.0);
+ // Set page width to 120pt with 10pt margins, so that the inner page is
+ // exactly 100pt wide. Page height is unbounded and font size is 10pt so
+ // that it multiplies to nice round numbers.
+ let mut styles = Styles::new();
+ styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0)));
+ styles.set(PageNode::HEIGHT, Smart::Auto);
+ styles.set(PageNode::LEFT, Smart::Custom(Length::pt(10.0).into()));
+ styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into()));
+ styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into()));
+ styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into()));
+ styles.set(TextNode::SIZE, Length::pt(10.0).into());
// Hook up an assert function into the global scope.
let mut std = typst::library::new();
@@ -87,10 +89,10 @@ fn main() {
// Create loader and context.
let loader = FsLoader::new().with_path(FONT_DIR).wrap();
- let mut ctx = Context::builder().std(std).style(style).build(loader);
+ let mut ctx = Context::builder().std(std).styles(styles).build(loader);
// Run all the tests.
- let mut ok = true;
+ let mut ok = 0;
for src_path in filtered {
let path = src_path.strip_prefix(TYP_DIR).unwrap();
let png_path = Path::new(PNG_DIR).join(path).with_extension("png");
@@ -98,49 +100,64 @@ fn main() {
let pdf_path =
args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
- ok &= test(
+ ok += test(
&mut ctx,
&src_path,
&png_path,
&ref_path,
pdf_path.as_deref(),
- );
+ args.debug,
+ ) as usize;
}
- if !ok {
+ if len > 1 {
+ println!("{} / {} tests passed.", ok, len);
+ }
+
+ if ok < len {
std::process::exit(1);
}
}
struct Args {
filter: Vec<String>,
+ exact: bool,
+ debug: bool,
pdf: bool,
- perfect: bool,
}
impl Args {
fn new(args: impl Iterator<Item = String>) -> Self {
let mut filter = Vec::new();
- let mut perfect = false;
+ let mut exact = false;
+ let mut debug = false;
let mut pdf = false;
for arg in args {
match arg.as_str() {
+ // Ignore this, its for cargo.
"--nocapture" => {}
+ // Match only the exact filename.
+ "--exact" => exact = true,
+ // Generate PDFs.
"--pdf" => pdf = true,
- "=" => perfect = true,
+ // Debug print the layout trees.
+ "--debug" | "-d" => debug = true,
+ // Everything else is a file filter.
_ => filter.push(arg),
}
}
- Self { filter, pdf, perfect }
+ Self { filter, pdf, debug, exact }
}
- fn matches(&self, name: &str) -> bool {
- if self.perfect {
- self.filter.iter().any(|p| name == p)
+ fn matches(&self, path: &Path) -> bool {
+ if self.exact {
+ let name = path.file_name().unwrap().to_string_lossy();
+ self.filter.iter().any(|v| v == &name)
} else {
- self.filter.is_empty() || self.filter.iter().any(|p| name.contains(p))
+ let path = path.to_string_lossy();
+ self.filter.is_empty() || self.filter.iter().any(|v| path.contains(v))
}
}
}
@@ -151,6 +168,7 @@ fn test(
png_path: &Path,
ref_path: &Path,
pdf_path: Option<&Path>,
+ debug: bool,
) -> bool {
let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path);
println!("Testing {}", name.display());
@@ -179,7 +197,7 @@ fn test(
}
} else {
let (part_ok, compare_here, part_frames) =
- test_part(ctx, src_path, part.into(), i, compare_ref, line);
+ test_part(ctx, src_path, part.into(), i, compare_ref, line, debug);
ok &= part_ok;
compare_ever |= compare_here;
frames.extend(part_frames);
@@ -211,7 +229,10 @@ fn test(
}
if ok {
- println!("\x1b[1ATesting {} ✔", name.display());
+ if !debug {
+ print!("\x1b[1A");
+ }
+ println!("Testing {} ✔", name.display());
}
ok
@@ -224,6 +245,7 @@ fn test_part(
i: usize,
compare_ref: bool,
line: usize,
+ debug: bool,
) -> (bool, bool, Vec<Rc<Frame>>) {
let id = ctx.sources.provide(src_path, src);
let source = ctx.sources.get(id);
@@ -232,12 +254,17 @@ fn test_part(
let compare_ref = local_compare_ref.unwrap_or(compare_ref);
let mut ok = true;
- let (frames, mut errors) = match ctx.execute(id) {
- Ok(document) => {
- let mut frames = layout(ctx, &document);
+ let (frames, mut errors) = match ctx.evaluate(id) {
+ Ok(module) => {
+ let tree = module.into_root();
+ if debug {
+ println!("{:#?}", tree);
+ }
+
+ let mut frames = tree.layout(ctx);
#[cfg(feature = "layout-cache")]
- (ok &= test_incremental(ctx, i, &document, &frames));
+ (ok &= test_incremental(ctx, i, &tree, &frames));
if !compare_ref {
frames.clear();
@@ -285,7 +312,7 @@ fn test_part(
fn test_incremental(
ctx: &mut Context,
i: usize,
- document: &DocumentNode,
+ tree: &RootNode,
frames: &[Rc<Frame>],
) -> bool {
let mut ok = true;
@@ -300,7 +327,7 @@ fn test_incremental(
ctx.layouts.turnaround();
- let cached = silenced(|| layout(ctx, document));
+ let cached = silenced(|| tree.layout(ctx));
let misses = ctx
.layouts
.entries()
@@ -319,7 +346,10 @@ fn test_incremental(
}
if cached != frames {
- println!(" Subtest {} relayout differs from clean pass ❌", i);
+ println!(
+ " Subtest {} relayout differs from clean pass on level {} ❌",
+ i, level
+ );
ok = false;
}
}
@@ -504,7 +534,7 @@ fn draw_text(
let mut x = 0.0;
for glyph in &text.glyphs {
let glyph_id = GlyphId(glyph.id);
- let offset = x + glyph.x_offset.to_length(text.size).to_f32();
+ let offset = x + glyph.x_offset.resolve(text.size).to_f32();
let ts = ts.pre_translate(offset, 0.0);
if let Some(tree) = ttf
@@ -557,7 +587,7 @@ fn draw_text(
}
}
- x += glyph.x_advance.to_length(text.size).to_f32();
+ x += glyph.x_advance.resolve(text.size).to_f32();
}
}