@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
);
|
||||
|
13
macros/Cargo.toml
Normal file
@ -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"] }
|
171
macros/src/lib.rs
Normal file
@ -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))
|
||||
}
|
139
src/eval/class.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
198
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;
|
||||
|
||||
/// Evaluate the expression to the output value.
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output>;
|
||||
}
|
||||
|
||||
impl Eval for Markup {
|
||||
type Output = Template;
|
||||
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)
|
||||
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 MarkupNode {
|
||||
type Output = Node;
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
453
src/eval/node.rs
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
||||
|
@ -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()
|
||||
|
292
src/eval/styles.rs
Normal file
@ -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>())
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
141
src/eval/walk.rs
@ -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)],
|
||||
}
|
||||
});
|
||||
}
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
14
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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,44 +15,30 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// The context for layouting.
|
||||
pub struct LayoutContext<'a> {
|
||||
/// Stores parsed font faces.
|
||||
pub fonts: &'a mut FontStore,
|
||||
/// Stores decoded images.
|
||||
pub images: &'a mut ImageStore,
|
||||
/// Caches layouting artifacts.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub layouts: &'a mut LayoutCache,
|
||||
/// How deeply nested the current layout tree position is.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub level: usize,
|
||||
}
|
||||
|
||||
impl<'a> LayoutContext<'a> {
|
||||
/// Create a new layout context.
|
||||
pub fn new(ctx: &'a mut Context) -> Self {
|
||||
Self {
|
||||
fonts: &mut ctx.fonts,
|
||||
images: &mut ctx.images,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
layouts: &mut ctx.layouts,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
level: 0,
|
||||
}
|
||||
impl Debug for RootNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("Root ")?;
|
||||
f.debug_list().entries(&self.0).finish()
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,26 +61,82 @@ pub trait Layout {
|
||||
{
|
||||
PackedNode {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: {
|
||||
let mut state = fxhash::FxHasher64::default();
|
||||
self.type_id().hash(&mut state);
|
||||
self.hash(&mut state);
|
||||
state.finish()
|
||||
},
|
||||
hash: self.hash64(),
|
||||
node: Rc::new(self),
|
||||
styles: Styles::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A packed layouting node with precomputed hash.
|
||||
/// The context for layouting.
|
||||
pub struct LayoutContext<'a> {
|
||||
/// Stores parsed font faces.
|
||||
pub fonts: &'a mut FontStore,
|
||||
/// Stores decoded images.
|
||||
pub images: &'a mut ImageStore,
|
||||
/// Caches layouting artifacts.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub layouts: &'a mut LayoutCache,
|
||||
/// The inherited style properties.
|
||||
// TODO(style): This probably shouldn't be here.
|
||||
pub styles: Styles,
|
||||
/// How deeply nested the current layout tree position is.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub level: usize,
|
||||
}
|
||||
|
||||
impl<'a> LayoutContext<'a> {
|
||||
/// Create a new layout context.
|
||||
pub fn new(ctx: &'a mut Context) -> Self {
|
||||
Self {
|
||||
fonts: &mut ctx.fonts,
|
||||
images: &mut ctx.images,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
layouts: &mut ctx.layouts,
|
||||
styles: ctx.styles.clone(),
|
||||
#[cfg(feature = "layout-cache")]
|
||||
level: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A layout node that produces an empty frame.
|
||||
///
|
||||
/// The packed version of this is returned by [`PackedNode::default`].
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct EmptyNode;
|
||||
|
||||
impl Layout for EmptyNode {
|
||||
fn layout(
|
||||
&self,
|
||||
_: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let size = regions.expand.select(regions.current, Size::zero());
|
||||
let mut cts = Constraints::new(regions.expand);
|
||||
cts.exact = regions.current.filter(regions.expand);
|
||||
vec![Frame::new(size).constrain(cts)]
|
||||
}
|
||||
}
|
||||
|
||||
/// A packed layouting node with 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")]
|
||||
ctx.layouts.get(self.hash, regions).unwrap_or_else(|| {
|
||||
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(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()
|
||||
}
|
||||
}
|
||||
|
78
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")]
|
||||
|
@ -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),
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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);
|
||||
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::Spacing(Spacing::Fractional(v)) => {
|
||||
self.items.push(FlowItem::Fractional(v));
|
||||
self.fr += v;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
63
src/library/heading.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use super::prelude::*;
|
||||
use super::{FontFamily, TextNode};
|
||||
|
||||
/// A section heading.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct HeadingNode {
|
||||
/// The node that produces the heading's contents.
|
||||
pub child: PackedNode,
|
||||
/// The logical nesting depth of the section, starting from one. In the
|
||||
/// default style, this controls the text size of the heading.
|
||||
pub level: usize,
|
||||
}
|
||||
|
||||
#[properties]
|
||||
impl HeadingNode {
|
||||
/// The heading's font family.
|
||||
pub const FAMILY: Smart<String> = Smart::Auto;
|
||||
/// The fill color of heading in the text. Just the surrounding text color
|
||||
/// if `auto`.
|
||||
pub const FILL: Smart<Paint> = Smart::Auto;
|
||||
}
|
||||
|
||||
impl Construct for HeadingNode {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
Ok(Node::block(Self {
|
||||
child: args.expect::<Node>("body")?.into_block(),
|
||||
level: args.named("level")?.unwrap_or(1),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Set for HeadingNode {
|
||||
fn set(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)
|
||||
}
|
||||
}
|
@ -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))]
|
||||
}
|
||||
}
|
||||
|
28
src/library/link.rs
Normal file
@ -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;
|
||||
}
|
102
src/library/list.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use std::hash::Hash;
|
||||
|
||||
use super::prelude::*;
|
||||
use super::{GridNode, TextNode, TrackSizing};
|
||||
|
||||
/// An unordered or ordered list.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct ListNode<L> {
|
||||
/// The node that produces the item's body.
|
||||
pub child: PackedNode,
|
||||
/// The list labelling style -- unordered or ordered.
|
||||
pub labelling: L,
|
||||
}
|
||||
|
||||
#[properties]
|
||||
impl<L: Labelling> ListNode<L> {
|
||||
/// The indentation of each item's label.
|
||||
pub const LABEL_INDENT: Linear = Relative::new(0.0).into();
|
||||
/// The space between the label and the body of each item.
|
||||
pub const BODY_INDENT: Linear = Relative::new(0.5).into();
|
||||
}
|
||||
|
||||
impl<L: Labelling> Construct for ListNode<L> {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
Ok(args
|
||||
.all()
|
||||
.map(|node: Node| {
|
||||
Node::block(Self {
|
||||
child: node.into_block(),
|
||||
labelling: L::default(),
|
||||
})
|
||||
})
|
||||
.sum())
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: Labelling> Set for ListNode<L> {
|
||||
fn set(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))
|
||||
}
|
||||
}
|
@ -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",
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
FontFamily: "font family",
|
||||
Value::Str(string) => Self::Named(string.to_lowercase()),
|
||||
}
|
||||
|
||||
castable! {
|
||||
Paint,
|
||||
Expected: "color",
|
||||
Value::Color(color) => Paint::Solid(color),
|
||||
}
|
||||
|
||||
castable! {
|
||||
usize,
|
||||
Expected: "non-negative integer",
|
||||
Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?,
|
||||
}
|
||||
|
||||
castable! {
|
||||
String,
|
||||
Expected: "string",
|
||||
Value::Str(string) => string.into(),
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -1,115 +1,405 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if let Some(width) = width {
|
||||
page.class = PaperClass::Custom;
|
||||
page.size.x = width.unwrap_or(Length::inf());
|
||||
}
|
||||
|
||||
if let Some(height) = height {
|
||||
page.class = PaperClass::Custom;
|
||||
page.size.y = height.unwrap_or(Length::inf());
|
||||
}
|
||||
|
||||
if flip.unwrap_or(false) {
|
||||
std::mem::swap(&mut page.size.x, &mut page.size.y);
|
||||
}
|
||||
|
||||
if let Some(margins) = margins {
|
||||
page.margins = Sides::splat(margins);
|
||||
}
|
||||
|
||||
if let Some(left) = left {
|
||||
page.margins.left = left;
|
||||
}
|
||||
|
||||
if let Some(top) = top {
|
||||
page.margins.top = top;
|
||||
}
|
||||
|
||||
if let Some(right) = right {
|
||||
page.margins.right = right;
|
||||
}
|
||||
|
||||
if let Some(bottom) = bottom {
|
||||
page.margins.bottom = bottom;
|
||||
}
|
||||
|
||||
if let Some(fill) = fill {
|
||||
page.fill = fill;
|
||||
}
|
||||
});
|
||||
|
||||
ctx.template.pagebreak(false);
|
||||
|
||||
Ok(Value::None)
|
||||
}
|
||||
use super::PadNode;
|
||||
|
||||
/// `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))
|
||||
Ok(Value::Node(Node::Pagebreak))
|
||||
}
|
||||
|
||||
/// Layouts its children onto one or multiple pages.
|
||||
#[derive(Debug, Hash)]
|
||||
/// Layouts its child onto one or multiple pages.
|
||||
#[derive(Clone, PartialEq, 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.
|
||||
/// The node producing the content.
|
||||
pub child: PackedNode,
|
||||
/// The page's styles.
|
||||
pub styles: Styles,
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
|
||||
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(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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(width) = args.named("width")? {
|
||||
styles.set(Self::CLASS, PaperClass::Custom);
|
||||
styles.set(Self::WIDTH, width);
|
||||
}
|
||||
|
||||
if let Some(height) = args.named("height")? {
|
||||
styles.set(Self::CLASS, PaperClass::Custom);
|
||||
styles.set(Self::HEIGHT, height);
|
||||
}
|
||||
|
||||
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")?);
|
||||
|
||||
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, ®ions).into_iter().map(|c| c.item).collect();
|
||||
padded.layout(ctx, ®ions).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")
|
||||
}
|
||||
|
@ -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")?;
|
||||
/// `parbreak`: Start a new paragraph.
|
||||
pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
|
||||
Ok(Value::Node(Node::Parbreak))
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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.modify(move |style| {
|
||||
let par = style.par_mut();
|
||||
|
||||
if let Some(dir) = dir {
|
||||
par.dir = dir;
|
||||
par.align = if dir == Dir::LTR { Align::Left } else { Align::Right };
|
||||
}
|
||||
|
||||
if let Some(align) = align {
|
||||
par.align = align;
|
||||
}
|
||||
|
||||
if let Some(leading) = leading {
|
||||
par.leading = leading;
|
||||
}
|
||||
|
||||
if let Some(spacing) = spacing {
|
||||
par.spacing = spacing;
|
||||
}
|
||||
});
|
||||
|
||||
ctx.template.parbreak();
|
||||
|
||||
Ok(Value::None)
|
||||
/// `linebreak`: Start a new line.
|
||||
pub fn linebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
|
||||
Ok(Value::Node(Node::Linebreak))
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
#[derive(Hash)]
|
||||
pub struct ParNode(pub Vec<ParChild>);
|
||||
|
||||
#[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();
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if let (Some(dir), None) = (dir, align) {
|
||||
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
|
||||
}
|
||||
|
||||
styles.set_opt(Self::DIR, dir);
|
||||
styles.set_opt(Self::ALIGN, align);
|
||||
styles.set_opt(Self::LEADING, leading);
|
||||
styles.set_opt(Self::SPACING, spacing);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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`.
|
||||
|
@ -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);
|
||||
|
@ -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>();
|
||||
let child = args
|
||||
.find()
|
||||
.map(|body: Node| body.into_block().padded(Sides::splat(padding)));
|
||||
|
||||
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))
|
||||
})))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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()),
|
||||
|
@ -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();
|
||||
}
|
||||
|
1093
src/library/text.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.
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
419
src/style/mod.rs
@ -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,
|
||||
}
|
@ -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")
|
||||
}
|
@ -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(_)
|
||||
@ -837,6 +844,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
|
||||
|
@ -96,22 +96,23 @@ impl Category {
|
||||
NodeKind::EnDash => Some(Category::Shortcut),
|
||||
NodeKind::EmDash => Some(Category::Shortcut),
|
||||
NodeKind::Escape(_) => Some(Category::Escape),
|
||||
NodeKind::Let => Some(Category::Keyword),
|
||||
NodeKind::If => Some(Category::Keyword),
|
||||
NodeKind::Else => 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::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::Break => Some(Category::Keyword),
|
||||
NodeKind::Continue => Some(Category::Keyword),
|
||||
NodeKind::Return => Some(Category::Keyword),
|
||||
NodeKind::Import => Some(Category::Keyword),
|
||||
NodeKind::From => 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,
|
||||
|
@ -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",
|
||||
|
@ -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}");
|
||||
|
@ -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
|
||||
|
@ -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 --"
|
||||
```
|
||||
|
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
tests/ref/layout/shape-circle.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
tests/ref/layout/shape-ellipse.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 8.9 KiB |
BIN
tests/ref/style/set-block.png
Normal file
After Width: | Height: | Size: 812 B |
BIN
tests/ref/style/set-site.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
tests/ref/style/set-toggle.png
Normal file
After Width: | Height: | Size: 968 B |
BIN
tests/ref/text/em.png
Normal file
After Width: | Height: | Size: 878 B |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.6 KiB |
@ -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)
|
||||
|
||||
---
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Test include statements.
|
||||
|
||||
---
|
||||
#page(width: 200pt)
|
||||
#set page(width: 200pt)
|
||||
|
||||
= Document
|
||||
|
||||
|
@ -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%}
|
||||
|
||||
---
|
||||
|
@ -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.
|
||||
|
@ -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)")
|
||||
}
|
||||
|
||||
---
|
||||
|
@ -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.*
|
||||
]
|
||||
|
@ -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)),
|
||||
|
@ -12,7 +12,7 @@ Apart
|
||||
---
|
||||
// Test block over multiple pages.
|
||||
|
||||
#page(height: 60pt)
|
||||
#set page(height: 60pt)
|
||||
|
||||
First!
|
||||
#block[
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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")
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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,
|
@ -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
|
@ -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]
|
||||
|
@ -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
|
@ -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?
|
||||
]
|
@ -5,7 +5,7 @@
|
||||
#rect()
|
||||
|
||||
---
|
||||
#page(width: 150pt)
|
||||
#set page(width: 150pt)
|
||||
|
||||
// Fit to text.
|
||||
#rect(fill: conifer, padding: 3pt)[Textbox]
|
@ -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?
|
||||
]
|