diff options
| author | Martin <mhaug@live.de> | 2021-12-22 20:37:34 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-12-22 20:37:34 +0100 |
| commit | f6c7a8292dc1ab0560408fca9d74505e9d7cf13a (patch) | |
| tree | badd3076f6146cec34c55764600df5124c408521 /src | |
| parent | 738ff7e1f573bef678932b313be9969a17af8d22 (diff) | |
| parent | 438255519e88bb790480306b9a9b452aaf054519 (diff) | |
Merge pull request #51 from typst/set-rules
Set rules
Diffstat (limited to 'src')
46 files changed, 3144 insertions, 2567 deletions
diff --git a/src/eval/class.rs b/src/eval/class.rs new file mode 100644 index 00000000..c4393b8a --- /dev/null +++ b/src/eval/class.rs @@ -0,0 +1,139 @@ +use std::fmt::{self, Debug, Formatter, Write}; +use std::marker::PhantomData; +use std::rc::Rc; + +use super::{Args, EvalContext, Node, Styles}; +use crate::diag::TypResult; +use crate::util::EcoString; + +/// A class of [nodes](Node). +/// +/// You can [construct] an instance of a class in Typst code by invoking the +/// class as a callable. This always produces some node, but not necessarily one +/// of fixed type. For example, the `text` constructor does not actually create +/// a [`TextNode`]. Instead it applies styling to whatever node you pass in and +/// returns it structurally unchanged. +/// +/// The arguments you can pass to a class constructor fall into two categories: +/// Data that is inherent to the instance (e.g. the text of a heading) and style +/// properties (e.g. the fill color of a heading). As the latter are often +/// shared by many instances throughout a document, they can also be +/// conveniently configured through class's [`set`] rule. Then, they apply to +/// all nodes that are instantiated into the template where the `set` was +/// executed. +/// +/// ```typst +/// This is normal. +/// [ +/// #set text(weight: "bold") +/// #set heading(fill: blue) +/// = A blue & bold heading +/// ] +/// Normal again. +/// ``` +/// +/// [construct]: Self::construct +/// [`TextNode`]: crate::library::TextNode +/// [`set`]: Self::set +#[derive(Clone)] +pub struct Class(Rc<Inner<dyn Bounds>>); + +/// The unsized structure behind the [`Rc`]. +struct Inner<T: ?Sized> { + name: EcoString, + shim: T, +} + +impl Class { + /// Create a new class. + pub fn new<T>(name: EcoString) -> Self + where + T: Construct + Set + 'static, + { + // By specializing the shim to `T`, its vtable will contain T's + // `Construct` and `Set` impls (through the `Bounds` trait), enabling us + // to use them in the class's methods. + Self(Rc::new(Inner { name, shim: Shim::<T>(PhantomData) })) + } + + /// The name of the class. + pub fn name(&self) -> &EcoString { + &self.0.name + } + + /// Construct an instance of the class. + /// + /// This parses both property and data arguments (in this order) and styles + /// the node constructed from the data with the style properties. + pub fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + let mut styles = Styles::new(); + self.set(args, &mut styles)?; + let node = self.0.shim.construct(ctx, args)?; + Ok(node.styled(styles)) + } + + /// Execute the class's set rule. + /// + /// This parses property arguments and writes the resulting styles into the + /// given style map. There are no further side effects. + pub fn set(&self, args: &mut Args, styles: &mut Styles) -> TypResult<()> { + self.0.shim.set(args, styles) + } +} + +impl Debug for Class { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("<class ")?; + f.write_str(&self.0.name)?; + f.write_char('>') + } +} + +impl PartialEq for Class { + fn eq(&self, other: &Self) -> bool { + // We cast to thin pointers for comparison because we don't want to + // compare vtables (there can be duplicate vtables across codegen units). + std::ptr::eq( + Rc::as_ptr(&self.0) as *const (), + Rc::as_ptr(&other.0) as *const (), + ) + } +} + +/// Construct an instance of a class. +pub trait Construct { + /// Construct an instance of this class from the arguments. + /// + /// This is passed only the arguments that remain after execution of the + /// class's set rule. + fn construct(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node>; +} + +/// Set style properties of a class. +pub trait Set { + /// Parse the arguments and insert style properties of this class into the + /// given style map. + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()>; +} + +/// Rewires the operations available on a class in an object-safe way. This is +/// only implemented by the zero-sized `Shim` struct. +trait Bounds { + fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node>; + fn set(&self, args: &mut Args, styles: &mut Styles) -> TypResult<()>; +} + +struct Shim<T>(PhantomData<T>); + +impl<T> Bounds for Shim<T> +where + T: Construct + Set, +{ + fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + T::construct(ctx, args) + } + + fn set(&self, args: &mut Args, styles: &mut Styles) -> TypResult<()> { + T::set(args, styles) + } +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index a0c31e98..17cc46ef 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -6,21 +6,24 @@ mod array; mod dict; #[macro_use] mod value; +#[macro_use] +mod styles; mod capture; +mod class; mod function; +mod node; mod ops; mod scope; -mod template; -mod walk; pub use array::*; pub use capture::*; +pub use class::*; pub use dict::*; pub use function::*; +pub use node::*; pub use scope::*; -pub use template::*; +pub use styles::*; pub use value::*; -pub use walk::*; use std::cell::RefMut; use std::collections::HashMap; @@ -33,6 +36,8 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult}; use crate::geom::{Angle, Fractional, Length, Relative}; use crate::image::ImageStore; +use crate::layout::RootNode; +use crate::library::{self, TextNode}; use crate::loading::Loader; use crate::source::{SourceId, SourceStore}; use crate::syntax::ast::*; @@ -40,20 +45,30 @@ use crate::syntax::{Span, Spanned}; use crate::util::{EcoString, RefMutExt}; use crate::Context; -/// Evaluate a parsed source file into a module. -pub fn eval(ctx: &mut Context, source: SourceId, markup: &Markup) -> TypResult<Module> { - let mut ctx = EvalContext::new(ctx, source); - let template = markup.eval(&mut ctx)?; - Ok(Module { scope: ctx.scopes.top, template }) -} - -/// An evaluated module, ready for importing or instantiation. +/// An evaluated module, ready for importing or conversion to a root layout +/// tree. #[derive(Debug, Default, Clone)] pub struct Module { /// The top-level definitions that were bound in this module. pub scope: Scope, - /// The template defined by this module. - pub template: Template, + /// The module's layoutable contents. + pub node: Node, +} + +impl Module { + /// Convert this module's node into a layout tree. + pub fn into_root(self) -> RootNode { + self.node.into_root() + } +} + +/// Evaluate an expression. +pub trait Eval { + /// The output of evaluating the expression. + type Output; + + /// Evaluate the expression to the output value. + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output>; } /// The context for evaluation. @@ -70,8 +85,8 @@ pub struct EvalContext<'a> { pub modules: HashMap<SourceId, Module>, /// The active scopes. pub scopes: Scopes<'a>, - /// The currently built template. - pub template: Template, + /// The active styles. + pub styles: Styles, } impl<'a> EvalContext<'a> { @@ -84,7 +99,7 @@ impl<'a> EvalContext<'a> { route: vec![source], modules: HashMap::new(), scopes: Scopes::new(Some(&ctx.std)), - template: Template::new(), + styles: Styles::new(), } } @@ -115,18 +130,20 @@ impl<'a> EvalContext<'a> { // Prepare the new context. let new_scopes = Scopes::new(self.scopes.base); - let old_scopes = mem::replace(&mut self.scopes, new_scopes); + let prev_scopes = mem::replace(&mut self.scopes, new_scopes); + let prev_styles = mem::take(&mut self.styles); self.route.push(id); // Evaluate the module. - let template = ast.eval(self).trace(|| Tracepoint::Import, span)?; + let node = ast.eval(self).trace(|| Tracepoint::Import, span)?; // Restore the old context. - let new_scopes = mem::replace(&mut self.scopes, old_scopes); + let new_scopes = mem::replace(&mut self.scopes, prev_scopes); + self.styles = prev_styles; self.route.pop().unwrap(); // Save the evaluated module. - let module = Module { scope: new_scopes.top, template }; + let module = Module { scope: new_scopes.top, node }; self.modules.insert(id, module); Ok(id) @@ -145,29 +162,108 @@ impl<'a> EvalContext<'a> { } } -/// Evaluate an expression. -pub trait Eval { - /// The output of evaluating the expression. - type Output; +impl Eval for Markup { + type Output = Node; - /// Evaluate the expression to the output value. - fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output>; + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + let prev = mem::take(&mut ctx.styles); + let nodes = self.nodes(); + let upper = nodes.size_hint().1.unwrap_or_default(); + let mut seq = Vec::with_capacity(upper); + for piece in nodes { + seq.push((piece.eval(ctx)?, ctx.styles.clone())); + } + ctx.styles = prev; + Ok(Node::Sequence(seq)) + } } -impl Eval for Markup { - type Output = Template; +impl Eval for MarkupNode { + type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { - Ok({ - let prev = mem::take(&mut ctx.template); - ctx.template.save(); - self.walk(ctx)?; - ctx.template.restore(); - mem::replace(&mut ctx.template, prev) + Ok(match self { + Self::Space => Node::Space, + Self::Linebreak => Node::Linebreak, + Self::Parbreak => Node::Parbreak, + Self::Strong => { + ctx.styles.toggle(TextNode::STRONG); + Node::new() + } + Self::Emph => { + ctx.styles.toggle(TextNode::EMPH); + Node::new() + } + Self::Text(text) => Node::Text(text.clone()), + Self::Raw(raw) => raw.eval(ctx)?, + Self::Math(math) => math.eval(ctx)?, + Self::Heading(heading) => heading.eval(ctx)?, + Self::List(list) => list.eval(ctx)?, + Self::Enum(enum_) => enum_.eval(ctx)?, + Self::Expr(expr) => expr.eval(ctx)?.show(), + }) + } +} + +impl Eval for RawNode { + type Output = Node; + + fn eval(&self, _: &mut EvalContext) -> TypResult<Self::Output> { + let text = Node::Text(self.text.clone()).monospaced(); + Ok(if self.block { + Node::Block(text.into_block()) + } else { + text }) } } +impl Eval for MathNode { + type Output = Node; + + fn eval(&self, _: &mut EvalContext) -> TypResult<Self::Output> { + let text = Node::Text(self.formula.trim().into()).monospaced(); + Ok(if self.display { + Node::Block(text.into_block()) + } else { + text + }) + } +} + +impl Eval for HeadingNode { + type Output = Node; + + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + Ok(Node::block(library::HeadingNode { + child: self.body().eval(ctx)?.into_block(), + level: self.level(), + })) + } +} + +impl Eval for ListNode { + type Output = Node; + + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + Ok(Node::block(library::ListNode { + child: self.body().eval(ctx)?.into_block(), + labelling: library::Unordered, + })) + } +} + +impl Eval for EnumNode { + type Output = Node; + + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + Ok(Node::block(library::ListNode { + child: self.body().eval(ctx)?.into_block(), + labelling: library::Ordered(self.number()), + })) + } +} + impl Eval for Expr { type Output = Value; @@ -177,7 +273,7 @@ impl Eval for Expr { Self::Ident(v) => v.eval(ctx), Self::Array(v) => v.eval(ctx).map(Value::Array), Self::Dict(v) => v.eval(ctx).map(Value::Dict), - Self::Template(v) => v.eval(ctx).map(Value::Template), + Self::Template(v) => v.eval(ctx).map(Value::Node), Self::Group(v) => v.eval(ctx), Self::Block(v) => v.eval(ctx), Self::Call(v) => v.eval(ctx), @@ -186,6 +282,7 @@ impl Eval for Expr { Self::Unary(v) => v.eval(ctx), Self::Binary(v) => v.eval(ctx), Self::Let(v) => v.eval(ctx), + Self::Set(v) => v.eval(ctx), Self::If(v) => v.eval(ctx), Self::While(v) => v.eval(ctx), Self::For(v) => v.eval(ctx), @@ -244,7 +341,7 @@ impl Eval for DictExpr { } impl Eval for TemplateExpr { - type Output = Template; + type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { self.body().eval(ctx) @@ -372,9 +469,15 @@ impl Eval for CallExpr { Ok(value) } + Value::Class(class) => { + let node = class.construct(ctx, &mut args)?; + args.finish()?; + Ok(Value::Node(node)) + } + v => bail!( self.callee().span(), - "expected function or collection, found {}", + "expected callable or collection, found {}", v.type_name(), ), } @@ -541,6 +644,19 @@ impl Eval for LetExpr { } } +impl Eval for SetExpr { + type Output = Value; + + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + let class = self.class(); + let class = class.eval(ctx)?.cast::<Class>().at(class.span())?; + let mut args = self.args().eval(ctx)?; + class.set(&mut args, &mut ctx.styles)?; + args.finish()?; + Ok(Value::None) + } +} + impl Eval for IfExpr { type Output = Value; @@ -665,7 +781,7 @@ impl Eval for IncludeExpr { let resolved = path.eval(ctx)?.cast::<EcoString>().at(path.span())?; let file = ctx.import(&resolved, path.span())?; let module = &ctx.modules[&file]; - Ok(Value::Template(module.template.clone())) + Ok(Value::Node(module.node.clone())) } } diff --git a/src/eval/node.rs b/src/eval/node.rs new file mode 100644 index 00000000..34a4f275 --- /dev/null +++ b/src/eval/node.rs @@ -0,0 +1,453 @@ +use std::convert::TryFrom; +use std::fmt::Debug; +use std::hash::Hash; +use std::iter::Sum; +use std::mem; +use std::ops::{Add, AddAssign}; + +use super::Styles; +use crate::diag::StrResult; +use crate::geom::SpecAxis; +use crate::layout::{Layout, PackedNode, RootNode}; +use crate::library::{ + FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, SpacingKind, + SpacingNode, TextNode, +}; +use crate::util::EcoString; + +/// A partial representation of a layout node. +/// +/// A node is a composable intermediate representation that can be converted +/// into a proper layout node by lifting it to a [block-level](PackedNode) or +/// [root node](RootNode). +/// +/// When you write `[Hi] + [you]` in Typst, this type's [`Add`] implementation +/// is invoked. There, multiple nodes are combined into a single +/// [`Sequence`](Self::Sequence) node. +#[derive(Debug, PartialEq, Clone, Hash)] +pub enum Node { + /// A word space. + Space, + /// A line break. + Linebreak, + /// A paragraph break. + Parbreak, + /// A page break. + Pagebreak, + /// Plain text. + Text(EcoString), + /// Spacing. + Spacing(SpecAxis, SpacingKind), + /// An inline node. + Inline(PackedNode), + /// A block node. + Block(PackedNode), + /// A page node. + Page(PageNode), + /// Multiple nodes with attached styles. + /// + /// For example, the Typst template `[Hi *you!*]` would result in the + /// sequence: + /// ```ignore + /// Sequence([ + /// (Text("Hi"), {}), + /// (Space, {}), + /// (Text("you!"), { TextNode::STRONG: true }), + /// ]) + /// ``` + /// A sequence may contain nested sequences (meaning this variant + /// effectively allows nodes to form trees). All nested sequences can + /// equivalently be represented as a single flat sequence, but allowing + /// nesting doesn't hurt since we can just recurse into the nested sequences + /// during packing. Also, in theory, this allows better complexity when + /// adding (large) sequence nodes (just like for a text rope). + Sequence(Vec<(Self, Styles)>), +} + +impl Node { + /// Create an empty node. + pub fn new() -> Self { + Self::Sequence(vec![]) + } + + /// Create an inline-level node. + pub fn inline<T>(node: T) -> Self + where + T: Layout + Debug + Hash + 'static, + { + Self::Inline(node.pack()) + } + + /// Create a block-level node. + pub fn block<T>(node: T) -> Self + where + T: Layout + Debug + Hash + 'static, + { + Self::Block(node.pack()) + } + + /// Style this node. + pub fn styled(self, styles: Styles) -> Self { + match self { + Self::Inline(inline) => Self::Inline(inline.styled(styles)), + Self::Block(block) => Self::Block(block.styled(styles)), + Self::Page(page) => Self::Page(page.styled(styles)), + other => Self::Sequence(vec![(other, styles)]), + } + } + + /// Style this node in monospace. + pub fn monospaced(self) -> Self { + self.styled(Styles::one(TextNode::MONOSPACE, true)) + } + + /// Lift to a type-erased block-level node. + pub fn into_block(self) -> PackedNode { + if let Node::Block(packed) = self { + packed + } else { + let mut packer = Packer::new(false); + packer.walk(self, Styles::new()); + packer.into_block() + } + } + + /// Lift to a root layout tree node. + pub fn into_root(self) -> RootNode { + let mut packer = Packer::new(true); + packer.walk(self, Styles::new()); + packer.into_root() + } + + /// Repeat this node `n` times. + pub fn repeat(&self, n: i64) -> StrResult<Self> { + let count = usize::try_from(n) + .map_err(|_| format!("cannot repeat this template {} times", n))?; + + // TODO(style): Make more efficient. + Ok(Self::Sequence(vec![(self.clone(), Styles::new()); count])) + } +} + +impl Default for Node { + fn default() -> Self { + Self::new() + } +} + +impl Add for Node { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + // TODO(style): Make more efficient. + Self::Sequence(vec![(self, Styles::new()), (rhs, Styles::new())]) + } +} + +impl AddAssign for Node { + fn add_assign(&mut self, rhs: Self) { + *self = mem::take(self) + rhs; + } +} + +impl Sum for Node { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + Self::Sequence(iter.map(|n| (n, Styles::new())).collect()) + } +} + +/// Packs a [`Node`] into a flow or root node. +struct Packer { + /// Whether this packer produces a root node. + top: bool, + /// The accumulated page nodes. + pages: Vec<PageNode>, + /// The accumulated flow children. + flow: Builder<FlowChild>, + /// The accumulated paragraph children. + par: Builder<ParChild>, +} + +impl Packer { + /// Start a new node-packing session. + fn new(top: bool) -> Self { + Self { + top, + pages: vec![], + flow: Builder::default(), + par: Builder::default(), + } + } + + /// Finish up and return the resulting flow. + fn into_block(mut self) -> PackedNode { + self.parbreak(None); + FlowNode(self.flow.children).pack() + } + + /// Finish up and return the resulting root node. + fn into_root(mut self) -> RootNode { + self.pagebreak(); + RootNode(self.pages) + } + + /// Consider a node with the given styles. + fn walk(&mut self, node: Node, styles: Styles) { + match node { + Node::Space => { + // A text space is "soft", meaning that it can be eaten up by + // adjacent line breaks or explicit spacings. + self.par.last.soft(ParChild::text(' ', styles)); + } + Node::Linebreak => { + // A line break eats up surrounding text spaces. + self.par.last.hard(); + self.push_inline(ParChild::text('\n', styles)); + self.par.last.hard(); + } + Node::Parbreak => { + // An explicit paragraph break is styled according to the active + // styles (`Some(_)`) whereas paragraph breaks forced by + // incompatibility take their styles from the preceding + // paragraph. + self.parbreak(Some(styles)); + } + Node::Pagebreak => { + // We must set the flow styles after the page break such that an + // empty page created by two page breaks in a row has styles at + // all. + self.pagebreak(); + self.flow.styles = styles; + } + Node::Text(text) => { + self.push_inline(ParChild::text(text, styles)); + } + Node::Spacing(SpecAxis::Horizontal, kind) => { + // Just like a line break, explicit horizontal spacing eats up + // surrounding text spaces. + self.par.last.hard(); + self.push_inline(ParChild::Spacing(SpacingNode { kind, styles })); + self.par.last.hard(); + } + Node::Spacing(SpecAxis::Vertical, kind) => { + // Explicit vertical spacing ends the current paragraph and then + // discards the paragraph break. + self.parbreak(None); + self.make_flow_compatible(&styles); + self.flow + .children + .push(FlowChild::Spacing(SpacingNode { kind, styles })); + self.flow.last.hard(); + } + Node::Inline(inline) => { + self.push_inline(ParChild::Node(inline.styled(styles))); + } + Node::Block(block) => { + self.push_block(block.styled(styles)); + } + Node::Page(page) => { + if self.top { + self.pagebreak(); + self.pages.push(page.styled(styles)); + } else { + let flow = page.child.styled(page.styles); + self.push_block(flow.styled(styles)); + } + } + Node::Sequence(list) => { + // For a list of nodes, we apply the list's styles to each node + // individually. + for (node, mut inner) in list { + inner.apply(&styles); + self.walk(node, inner); + } + } + } + } + + /// Insert an inline-level element into the current paragraph. + fn push_inline(&mut self, child: ParChild) { + if let Some(child) = self.par.last.any() { + self.push_coalescing(child); + } + + // The node must be both compatible with the current page and the + // current paragraph. + self.make_flow_compatible(child.styles()); + self.make_par_compatible(child.styles()); + self.push_coalescing(child); + self.par.last.any(); + } + + /// Push a paragraph child, coalescing text nodes with compatible styles. + fn push_coalescing(&mut self, child: ParChild) { + if let ParChild::Text(right) = &child { + if let Some(ParChild::Text(left)) = self.par.children.last_mut() { + if left.styles.compatible(&right.styles, TextNode::has_property) { + left.text.push_str(&right.text); + return; + } + } + } + + self.par.children.push(child); + } + + /// Insert a block-level element into the current flow. + fn push_block(&mut self, node: PackedNode) { + let placed = node.is::<PlacedNode>(); + + self.parbreak(None); + self.make_flow_compatible(&node.styles); + self.flow.children.extend(self.flow.last.any()); + self.flow.children.push(FlowChild::Node(node)); + self.parbreak(None); + + // Prevent paragraph spacing between the placed node and the paragraph + // below it. + if placed { + self.flow.last.hard(); + } + } + + /// Advance to the next paragraph. + fn parbreak(&mut self, break_styles: Option<Styles>) { + // Erase any styles that will be inherited anyway. + let Builder { mut children, styles, .. } = mem::take(&mut self.par); + for child in &mut children { + child.styles_mut().erase(&styles); + } + + // For explicit paragraph breaks, `break_styles` is already `Some(_)`. + // For page breaks due to incompatibility, we fall back to the styles + // of the preceding paragraph. + let break_styles = break_styles.unwrap_or_else(|| styles.clone()); + + // We don't want empty paragraphs. + if !children.is_empty() { + // The paragraph's children are all compatible with the page, so the + // paragraph is too, meaning we don't need to check or intersect + // anything here. + let par = ParNode(children).pack().styled(styles); + self.flow.children.extend(self.flow.last.any()); + self.flow.children.push(FlowChild::Node(par)); + } + + // Insert paragraph spacing. + self.flow.last.soft(FlowChild::Break(break_styles)); + } + + /// Advance to the next page. + fn pagebreak(&mut self) { + if self.top { + self.parbreak(None); + + // Take the flow and erase any styles that will be inherited anyway. + let Builder { mut children, styles, .. } = mem::take(&mut self.flow); + for child in &mut children { + child.styles_mut().erase(&styles); + } + + let flow = FlowNode(children).pack(); + let page = PageNode { child: flow, styles }; + self.pages.push(page); + } + } + + /// Break to a new paragraph if the `styles` contain paragraph styles that + /// are incompatible with the current paragraph. + fn make_par_compatible(&mut self, styles: &Styles) { + if self.par.children.is_empty() { + self.par.styles = styles.clone(); + return; + } + + if !self.par.styles.compatible(&styles, ParNode::has_property) { + self.parbreak(None); + self.par.styles = styles.clone(); + return; + } + + self.par.styles.intersect(&styles); + } + + /// Break to a new page if the `styles` contain page styles that are + /// incompatible with the current flow. + fn make_flow_compatible(&mut self, styles: &Styles) { + if self.flow.children.is_empty() && self.par.children.is_empty() { + self.flow.styles = styles.clone(); + return; + } + + if self.top && !self.flow.styles.compatible(&styles, PageNode::has_property) { + self.pagebreak(); + self.flow.styles = styles.clone(); + return; + } + + self.flow.styles.intersect(styles); + } +} + +/// Container for building a flow or paragraph. +struct Builder<T> { + /// The intersection of the style properties of all `children`. + styles: Styles, + /// The accumulated flow or paragraph children. + children: Vec<T>, + /// The kind of thing that was last added. + last: Last<T>, +} + +impl<T> Default for Builder<T> { + fn default() -> Self { + Self { + styles: Styles::new(), + children: vec![], + last: Last::None, + } + } +} + +/// The kind of node that was last added to a flow or paragraph. A small finite +/// state machine used to coalesce spaces. +/// +/// Soft nodes can only exist when surrounded by `Any` nodes. Not at the +/// start, end or next to hard nodes. This way, spaces at start and end of +/// paragraphs and next to `#h(..)` goes away. +enum Last<N> { + /// Start state, nothing there. + None, + /// Text or a block node or something. + Any, + /// Hard nodes: Linebreaks and explicit spacing. + Hard, + /// Soft nodes: Word spaces and paragraph breaks. These are saved here + /// temporarily and then applied once an `Any` node appears. + Soft(N), +} + +impl<N> Last<N> { + /// Transition into the `Any` state and return a soft node to really add + /// now if currently in `Soft` state. + fn any(&mut self) -> Option<N> { + match mem::replace(self, Self::Any) { + Self::Soft(soft) => Some(soft), + _ => None, + } + } + + /// Transition into the `Soft` state, but only if in `Any`. Otherwise, the + /// soft node is discarded. + fn soft(&mut self, soft: N) { + if let Self::Any = self { + *self = Self::Soft(soft); + } + } + + /// Transition into the `Hard` state, discarding a possibly existing soft + /// node and preventing further soft nodes from being added. + fn hard(&mut self) { + *self = Self::Hard; + } +} diff --git a/src/eval/ops.rs b/src/eval/ops.rs index ede1230f..23530c10 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -22,9 +22,9 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { (Str(a), Str(b)) => Str(a + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), - (Template(a), Template(b)) => Template(a + b), - (Template(a), Str(b)) => Template(a + b), - (Str(a), Template(b)) => Template(a + b), + (Node(a), Node(b)) => Node(a + b), + (Node(a), Str(b)) => Node(a + super::Node::Text(b)), + (Str(a), Node(b)) => Node(super::Node::Text(a) + b), (a, b) => mismatch!("cannot join {} with {}", a, b), }) } @@ -84,9 +84,9 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { (Str(a), Str(b)) => Str(a + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), - (Template(a), Template(b)) => Template(a + b), - (Template(a), Str(b)) => Template(a + b), - (Str(a), Template(b)) => Template(a + b), + (Node(a), Node(b)) => Node(a + b), + (Node(a), Str(b)) => Node(a + super::Node::Text(b)), + (Str(a), Node(b)) => Node(super::Node::Text(a) + b), (a, b) => { if let (Dyn(a), Dyn(b)) = (&a, &b) { @@ -179,8 +179,8 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { (Int(a), Str(b)) => Str(repeat_str(b, a)?), (Array(a), Int(b)) => Array(a.repeat(b)?), (Int(a), Array(b)) => Array(b.repeat(a)?), - (Template(a), Int(b)) => Template(a.repeat(b)?), - (Int(a), Template(b)) => Template(b.repeat(a)?), + (Node(a), Int(b)) => Node(a.repeat(b)?), + (Int(a), Node(b)) => Node(b.repeat(a)?), (a, b) => mismatch!("cannot multiply {} with {}", a, b), }) @@ -297,7 +297,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { (Str(a), Str(b)) => a == b, (Array(a), Array(b)) => a == b, (Dict(a), Dict(b)) => a == b, - (Template(a), Template(b)) => a == b, + (Node(a), Node(b)) => a == b, (Func(a), Func(b)) => a == b, (Dyn(a), Dyn(b)) => a == b, diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 2290affd..5178c819 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Formatter}; use std::iter; use std::rc::Rc; -use super::{Args, EvalContext, Function, Value}; +use super::{Args, Class, Construct, EvalContext, Function, Set, Value}; use crate::diag::TypResult; use crate::util::EcoString; @@ -88,15 +88,6 @@ impl Scope { self.values.insert(var.into(), Rc::new(cell)); } - /// Define a constant function. - pub fn def_func<F>(&mut self, name: impl Into<EcoString>, f: F) - where - F: Fn(&mut EvalContext, &mut Args) -> TypResult<Value> + 'static, - { - let name = name.into(); - self.def_const(name.clone(), Function::new(Some(name), f)); - } - /// Define a mutable variable with a value. pub fn def_mut(&mut self, var: impl Into<EcoString>, value: impl Into<Value>) { self.values.insert(var.into(), Rc::new(RefCell::new(value.into()))); @@ -107,6 +98,24 @@ impl Scope { self.values.insert(var.into(), slot); } + /// Define a constant function. + pub fn def_func<F>(&mut self, name: &str, f: F) + where + F: Fn(&mut EvalContext, &mut Args) -> TypResult<Value> + 'static, + { + let name = EcoString::from(name); + self.def_const(name.clone(), Function::new(Some(name), f)); + } + + /// Define a constant class. + pub fn def_class<T>(&mut self, name: &str) + where + T: Construct + Set + 'static, + { + let name = EcoString::from(name); + self.def_const(name.clone(), Class::new::<T>(name)); + } + /// Look up the value of a variable. pub fn get(&self, var: &str) -> Option<&Slot> { self.values.get(var) @@ -120,6 +129,7 @@ impl Scope { impl Debug for Scope { fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Scope ")?; f.debug_map() .entries(self.values.iter().map(|(k, v)| (k, v.borrow()))) .finish() diff --git a/src/eval/styles.rs b/src/eval/styles.rs new file mode 100644 index 00000000..1c4b17ae --- /dev/null +++ b/src/eval/styles.rs @@ -0,0 +1,292 @@ +use std::any::{Any, TypeId}; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::rc::Rc; + +// TODO(style): Possible optimizations: +// - Ref-count map for cheaper cloning and smaller footprint +// - Store map in `Option` to make empty maps non-allocating +// - Store small properties inline + +/// A map of style properties. +#[derive(Default, Clone, Hash)] +pub struct Styles { + map: Vec<(StyleId, Entry)>, +} + +impl Styles { + /// Create a new, empty style map. + pub fn new() -> Self { + Self { map: vec![] } + } + + /// Whether this map contains no styles. + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + /// Create a style map with a single property-value pair. + pub fn one<P: Property>(key: P, value: P::Value) -> Self { + let mut styles = Self::new(); + styles.set(key, value); + styles + } + + /// Set the value for a style property. + pub fn set<P: Property>(&mut self, key: P, value: P::Value) { + let id = StyleId::of::<P>(); + for pair in &mut self.map { + if pair.0 == id { + let prev = pair.1.downcast::<P::Value>().unwrap(); + let folded = P::combine(value, prev.clone()); + pair.1 = Entry::new(key, folded); + return; + } + } + + self.map.push((id, Entry::new(key, value))); + } + + /// Set a value for a style property if it is `Some(_)`. + pub fn set_opt<P: Property>(&mut self, key: P, value: Option<P::Value>) { + if let Some(value) = value { + self.set(key, value); + } + } + + /// Toggle a boolean style property. + pub fn toggle<P: Property<Value = bool>>(&mut self, key: P) { + let id = StyleId::of::<P>(); + for (i, pair) in self.map.iter_mut().enumerate() { + if pair.0 == id { + self.map.swap_remove(i); + return; + } + } + + self.map.push((id, Entry::new(key, true))); + } + + /// Get the value of a copyable style property. + /// + /// Returns the property's default value if the map does not contain an + /// entry for it. + pub fn get<P: Property>(&self, key: P) -> P::Value + where + P::Value: Copy, + { + self.get_direct(key) + .map(|&v| P::combine(v, P::default())) + .unwrap_or_else(P::default) + } + + /// Get a reference to a style property. + /// + /// Returns a reference to the property's default value if the map does not + /// contain an entry for it. + pub fn get_ref<P: Property>(&self, key: P) -> &P::Value { + self.get_direct(key).unwrap_or_else(|| P::default_ref()) + } + + /// Get a reference to a style directly in this map (no default value). + fn get_direct<P: Property>(&self, _: P) -> Option<&P::Value> { + self.map + .iter() + .find(|pair| pair.0 == StyleId::of::<P>()) + .and_then(|pair| pair.1.downcast()) + } + + /// Create new styles combining `self` with `outer`. + /// + /// Properties from `self` take precedence over the ones from `outer`. + pub fn chain(&self, outer: &Self) -> Self { + let mut styles = self.clone(); + styles.apply(outer); + styles + } + + /// Apply styles from `outer` in-place. + /// + /// Properties from `self` take precedence over the ones from `outer`. + pub fn apply(&mut self, outer: &Self) { + 'outer: for pair in &outer.map { + for (id, entry) in &mut self.map { + if pair.0 == *id { + entry.apply(&pair.1); + continue 'outer; + } + } + + self.map.push(pair.clone()); + } + } + + /// Keep only those styles that are not also in `other`. + pub fn erase(&mut self, other: &Self) { + self.map.retain(|a| other.map.iter().all(|b| a != b)); + } + + /// Keep only those styles that are also in `other`. + pub fn intersect(&mut self, other: &Self) { + self.map.retain(|a| other.map.iter().any(|b| a == b)); + } + + /// Whether two style maps are equal when filtered down to the given + /// properties. + pub fn compatible<F>(&self, other: &Self, filter: F) -> bool + where + F: Fn(StyleId) -> bool, + { + // TODO(style): Filtered length + one direction equal should suffice. + let f = |e: &&(StyleId, Entry)| filter(e.0); + self.map.iter().filter(f).all(|pair| other.map.contains(pair)) + && other.map.iter().filter(f).all(|pair| self.map.contains(pair)) + } +} + +impl Debug for Styles { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + for pair in &self.map { + writeln!(f, "{:#?}", pair.1)?; + } + Ok(()) + } else { + f.write_str("Styles ")?; + f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish() + } + } +} + +impl PartialEq for Styles { + fn eq(&self, other: &Self) -> bool { + self.compatible(other, |_| true) + } +} + +/// An entry for a single style property. +#[derive(Clone)] +pub(crate) struct Entry(Rc<dyn Bounds>); + +impl Entry { + fn new<P: Property>(key: P, value: P::Value) -> Self { + Self(Rc::new((key, value))) + } + + fn downcast<T: 'static>(&self) -> Option<&T> { + self.0.as_any().downcast_ref() + } + + fn apply(&mut self, outer: &Self) { + *self = self.0.combine(outer); + } +} + +impl Debug for Entry { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.dyn_fmt(f) + } +} + +impl PartialEq for Entry { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(other) + } +} + +impl Hash for Entry { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_u64(self.0.hash64()); + } +} + +trait Bounds: 'static { + fn as_any(&self) -> &dyn Any; + fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result; + fn dyn_eq(&self, other: &Entry) -> bool; + fn hash64(&self) -> u64; + fn combine(&self, outer: &Entry) -> Entry; +} + +// `P` is always zero-sized. We only implement the trait for a pair of key and +// associated value so that `P` is a constrained type parameter that we can use +// in `dyn_fmt` to access the property's name. This way, we can effectively +// store the property's name in its vtable instead of having an actual runtime +// string somewhere in `Entry`. +impl<P: Property> Bounds for (P, P::Value) { + fn as_any(&self) -> &dyn Any { + &self.1 + } + + fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "#[{} = {:?}]", P::NAME, self.1) + } else { + write!(f, "{}: {:?}", P::NAME, self.1) + } + } + + fn dyn_eq(&self, other: &Entry) -> bool { + if let Some(other) = other.downcast::<P::Value>() { + &self.1 == other + } else { + false + } + } + + fn hash64(&self) -> u64 { + // No need to hash the TypeId since there's only one + // valid value type per property. + fxhash::hash64(&self.1) + } + + fn combine(&self, outer: &Entry) -> Entry { + let outer = outer.downcast::<P::Value>().unwrap(); + let combined = P::combine(self.1.clone(), outer.clone()); + Entry::new(self.0, combined) + } +} + +/// Style property keys. +/// +/// This trait is not intended to be implemented manually, but rather through +/// the `#[properties]` proc-macro. +pub trait Property: Copy + 'static { + /// The type of value that is returned when getting this property from a + /// style map. For example, this could be [`Length`](crate::geom::Length) + /// for a `WIDTH` property. + type Value: Debug + Clone + PartialEq + Hash + 'static; + + /// The name of the property, used for debug printing. + const NAME: &'static str; + + /// The default value of the property. + fn default() -> Self::Value; + + /// A static reference to the default value of the property. + /// + /// This is automatically implemented through lazy-initialization in the + /// `#[properties]` macro. This way, expensive defaults don't need to be + /// recreated all the time. + fn default_ref() -> &'static Self::Value; + + /// Fold the property with an outer value. + /// + /// For example, this would combine a relative font size with an outer + /// absolute font size. + #[allow(unused_variables)] + fn combine(inner: Self::Value, outer: Self::Value) -> Self::Value { + inner + } +} + +/// A unique identifier for a style property. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct StyleId(TypeId); + +impl StyleId { + /// The style id of the property. + pub fn of<P: Property>() -> Self { + Self(TypeId::of::<P>()) + } +} diff --git a/src/eval/template.rs b/src/eval/template.rs deleted file mode 100644 index 9c57bbf3..00000000 --- a/src/eval/template.rs +++ /dev/null @@ -1,547 +0,0 @@ -use std::convert::TryFrom; -use std::fmt::{self, Debug, Formatter}; -use std::hash::Hash; -use std::mem; -use std::ops::{Add, AddAssign}; -use std::rc::Rc; - -use crate::diag::StrResult; -use crate::geom::{Align, Dir, Length, Linear, Paint, Sides, Size, SpecAxis}; -use crate::layout::{Layout, PackedNode}; -use crate::library::{ - Decoration, DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, - PlacedNode, Spacing, -}; -use crate::style::Style; -use crate::util::EcoString; - -/// A template value: `[*Hi* there]`. -#[derive(Default, Clone)] -pub struct Template(Rc<Vec<TemplateNode>>); - -/// One node in a template. -#[derive(Clone)] -enum TemplateNode { - /// A word space. - Space, - /// A line break. - Linebreak, - /// A paragraph break. - Parbreak, - /// A page break. - Pagebreak(bool), - /// Plain text. - Text(EcoString), - /// Spacing. - Spacing(SpecAxis, Spacing), - /// A decorated template. - Decorated(Decoration, Template), - /// An inline node builder. - Inline(Rc<dyn Fn(&Style) -> PackedNode>), - /// A block node builder. - Block(Rc<dyn Fn(&Style) -> PackedNode>), - /// Save the current style. - Save, - /// Restore the last saved style. - Restore, - /// A function that can modify the current style. - Modify(Rc<dyn Fn(&mut Style)>), -} - -impl Template { - /// Create a new, empty template. - pub fn new() -> Self { - Self(Rc::new(vec![])) - } - - /// Create a template from a builder for an inline-level node. - pub fn from_inline<F, T>(f: F) -> Self - where - F: Fn(&Style) -> T + 'static, - T: Layout + Debug + Hash + 'static, - { - let node = TemplateNode::Inline(Rc::new(move |s| f(s).pack())); - Self(Rc::new(vec![node])) - } - - /// Create a template from a builder for a block-level node. - pub fn from_block<F, T>(f: F) -> Self - where - F: Fn(&Style) -> T + 'static, - T: Layout + Debug + Hash + 'static, - { - let node = TemplateNode::Block(Rc::new(move |s| f(s).pack())); - Self(Rc::new(vec![node])) - } - - /// Add a word space to the template. - pub fn space(&mut self) { - self.make_mut().push(TemplateNode::Space); - } - - /// Add a line break to the template. - pub fn linebreak(&mut self) { - self.make_mut().push(TemplateNode::Linebreak); - } - - /// Add a paragraph break to the template. - pub fn parbreak(&mut self) { - self.make_mut().push(TemplateNode::Parbreak); - } - - /// Add a page break to the template. - pub fn pagebreak(&mut self, keep: bool) { - self.make_mut().push(TemplateNode::Pagebreak(keep)); - } - - /// Add text to the template. - pub fn text(&mut self, text: impl Into<EcoString>) { - self.make_mut().push(TemplateNode::Text(text.into())); - } - - /// Add text, but in monospace. - pub fn monospace(&mut self, text: impl Into<EcoString>) { - self.save(); - self.modify(|style| style.text_mut().monospace = true); - self.text(text); - self.restore(); - } - - /// Add spacing along an axis. - pub fn spacing(&mut self, axis: SpecAxis, spacing: Spacing) { - self.make_mut().push(TemplateNode::Spacing(axis, spacing)); - } - - /// Register a restorable snapshot. - pub fn save(&mut self) { - self.make_mut().push(TemplateNode::Save); - } - - /// Ensure that later nodes are untouched by style modifications made since - /// the last snapshot. - pub fn restore(&mut self) { - self.make_mut().push(TemplateNode::Restore); - } - - /// Modify the style. - pub fn modify<F>(&mut self, f: F) - where - F: Fn(&mut Style) + 'static, - { - self.make_mut().push(TemplateNode::Modify(Rc::new(f))); - } - - /// Return a new template which is modified from start to end. - pub fn modified<F>(self, f: F) -> Self - where - F: Fn(&mut Style) + 'static, - { - let mut wrapper = Self::new(); - wrapper.save(); - wrapper.modify(f); - wrapper += self; - wrapper.restore(); - wrapper - } - - /// Add a decoration to all contained nodes. - pub fn decorate(self, deco: Decoration) -> Self { - Self(Rc::new(vec![TemplateNode::Decorated(deco, self)])) - } - - /// Pack the template into a layout node. - pub fn pack(&self, style: &Style) -> PackedNode { - if let [TemplateNode::Block(f)] = self.0.as_slice() { - f(style) - } else { - let mut builder = Builder::new(style, false); - builder.template(self); - builder.build_flow().pack() - } - } - - /// Build the layout tree resulting from instantiating the template with the - /// given style. - pub fn to_document(&self, style: &Style) -> DocumentNode { - let mut builder = Builder::new(style, true); - builder.template(self); - builder.build_document() - } - - /// Repeat this template `n` times. - pub fn repeat(&self, n: i64) -> StrResult<Self> { - let count = usize::try_from(n) - .ok() - .and_then(|n| self.0.len().checked_mul(n)) - .ok_or_else(|| format!("cannot repeat this template {} times", n))?; - - Ok(Self(Rc::new( - self.0.iter().cloned().cycle().take(count).collect(), - ))) - } - - /// Return a mutable reference to the inner vector. - fn make_mut(&mut self) -> &mut Vec<TemplateNode> { - Rc::make_mut(&mut self.0) - } -} - -impl Debug for Template { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("<template>") - } -} - -impl PartialEq for Template { - fn eq(&self, other: &Self) -> bool { - Rc::ptr_eq(&self.0, &other.0) - } -} - -impl Add for Template { - type Output = Self; - - fn add(mut self, rhs: Self) -> Self::Output { - self += rhs; - self - } -} - -impl AddAssign for Template { - fn add_assign(&mut self, rhs: Template) { - let sink = Rc::make_mut(&mut self.0); - match Rc::try_unwrap(rhs.0) { - Ok(source) => sink.extend(source), - Err(rc) => sink.extend(rc.iter().cloned()), - } - } -} - -impl Add<EcoString> for Template { - type Output = Self; - - fn add(mut self, rhs: EcoString) -> Self::Output { - Rc::make_mut(&mut self.0).push(TemplateNode::Text(rhs)); - self - } -} - -impl Add<Template> for EcoString { - type Output = Template; - - fn add(self, mut rhs: Template) -> Self::Output { - Rc::make_mut(&mut rhs.0).insert(0, TemplateNode::Text(self)); - rhs - } -} - -/// Transforms from template to layout representation. -struct Builder { - /// The current style. - style: Style, - /// Snapshots of the style. - snapshots: Vec<Style>, - /// The finished page nodes. - finished: Vec<PageNode>, - /// When we are building the top-level layout trees, this contains metrics - /// of the page. While building a flow, this is `None`. - page: Option<PageBuilder>, - /// The currently built flow of paragraphs. - flow: FlowBuilder, -} - -impl Builder { - /// Create a new builder with a base style. - fn new(style: &Style, pages: bool) -> Self { - Self { - style: style.clone(), - snapshots: vec![], - finished: vec![], - page: pages.then(|| PageBuilder::new(style, true)), - flow: FlowBuilder::new(style), - } - } - - /// Build a template. - fn template(&mut self, template: &Template) { - for node in template.0.iter() { - self.node(node); - } - } - - /// Build a template node. - fn node(&mut self, node: &TemplateNode) { - match node { - TemplateNode::Save => self.snapshots.push(self.style.clone()), - TemplateNode::Restore => { - let style = self.snapshots.pop().unwrap(); - let newpage = style.page != self.style.page; - self.style = style; - if newpage { - self.pagebreak(true, false); - } - } - TemplateNode::Space => self.space(), - TemplateNode::Linebreak => self.linebreak(), - TemplateNode::Parbreak => self.parbreak(), - TemplateNode::Pagebreak(keep) => self.pagebreak(*keep, true), - TemplateNode::Text(text) => self.text(text), - TemplateNode::Spacing(axis, amount) => self.spacing(*axis, *amount), - TemplateNode::Decorated(deco, template) => { - self.flow.par.push(ParChild::Decorate(deco.clone())); - self.template(template); - self.flow.par.push(ParChild::Undecorate); - } - TemplateNode::Inline(f) => self.inline(f(&self.style)), - TemplateNode::Block(f) => self.block(f(&self.style)), - TemplateNode::Modify(f) => f(&mut self.style), - } - } - - /// Push a word space into the active paragraph. - fn space(&mut self) { - self.flow.par.push_soft(self.make_text_node(' ')); - } - - /// Apply a forced line break. - fn linebreak(&mut self) { - self.flow.par.push_hard(self.make_text_node('\n')); - } - - /// Apply a forced paragraph break. - fn parbreak(&mut self) { - let amount = self.style.par_spacing(); - self.flow.finish_par(&self.style); - self.flow - .push_soft(FlowChild::Spacing(Spacing::Linear(amount.into()))); - } - - /// Apply a forced page break. - fn pagebreak(&mut self, keep: bool, hard: bool) { - if let Some(builder) = &mut self.page { - let page = mem::replace(builder, PageBuilder::new(&self.style, hard)); - let flow = mem::replace(&mut self.flow, FlowBuilder::new(&self.style)); - self.finished.extend(page.build(flow.build(), keep)); - } - } - - /// Push text into the active paragraph. - fn text(&mut self, text: impl Into<EcoString>) { - self.flow.par.push(self.make_text_node(text)); - } - - /// Push an inline node into the active paragraph. - fn inline(&mut self, node: PackedNode) { - self.flow.par.push(ParChild::Node(node.into())); - } - - /// Push a block node into the active flow, finishing the active paragraph. - fn block(&mut self, node: PackedNode) { - let mut is_placed = false; - if let Some(placed) = node.downcast::<PlacedNode>() { - is_placed = true; - - // This prevents paragraph spacing after the placed node if it - // is completely out-of-flow. - if placed.out_of_flow() { - self.flow.last = Last::None; - } - } - - self.parbreak(); - self.flow.push(FlowChild::Node(node)); - self.parbreak(); - - // This prevents paragraph spacing between the placed node and - // the paragraph below it. - if is_placed { - self.flow.last = Last::None; - } - } - - /// Push spacing into the active paragraph or flow depending on the `axis`. - fn spacing(&mut self, axis: SpecAxis, spacing: Spacing) { - match axis { - SpecAxis::Vertical => { - self.flow.finish_par(&self.style); - self.flow.push_hard(FlowChild::Spacing(spacing)); - } - SpecAxis::Horizontal => { - self.flow.par.push_hard(ParChild::Spacing(spacing)); - } - } - } - - /// Finish building and return the created flow. - fn build_flow(self) -> FlowNode { - assert!(self.page.is_none()); - self.flow.build() - } - - /// Finish building and return the created layout tree. - fn build_document(mut self) -> DocumentNode { - assert!(self.page.is_some()); - self.pagebreak(true, false); - DocumentNode { pages: self.finished } - } - - /// Construct a text node with the given text and settings from the current - /// style. - fn make_text_node(&self, text: impl Into<EcoString>) -> ParChild { - ParChild::Text(text.into(), Rc::clone(&self.style.text)) - } -} - -struct PageBuilder { - size: Size, - padding: Sides<Linear>, - fill: Option<Paint>, - hard: bool, -} - -impl PageBuilder { - fn new(style: &Style, hard: bool) -> Self { - Self { - size: style.page.size, - padding: style.page.margins(), - fill: style.page.fill, - hard, - } - } - - fn build(self, child: FlowNode, keep: bool) -> Option<PageNode> { - let Self { size, padding, fill, hard } = self; - (!child.children.is_empty() || (keep && hard)).then(|| PageNode { - child: child.pack().padded(padding), - size, - fill, - }) - } -} - -struct FlowBuilder { - children: Vec<FlowChild>, - last: Last<FlowChild>, - par: ParBuilder, -} - -impl FlowBuilder { - fn new(style: &Style) -> Self { - Self { - children: vec![], - last: Last::None, - par: ParBuilder::new(style), - } - } - - fn push(&mut self, child: FlowChild) { - self.children.extend(self.last.any()); - self.children.push(child); - } - - fn push_soft(&mut self, child: FlowChild) { - self.last.soft(child); - } - - fn push_hard(&mut self, child: FlowChild) { - self.last.hard(); - self.children.push(child); - } - - fn finish_par(&mut self, style: &Style) { - let par = mem::replace(&mut self.par, ParBuilder::new(style)); - if let Some(par) = par.build() { - self.push(par); - } - } - - fn build(self) -> FlowNode { - let Self { mut children, par, mut last } = self; - if let Some(par) = par.build() { - children.extend(last.any()); - children.push(par); - } - FlowNode { children } - } -} - -struct ParBuilder { - dir: Dir, - align: Align, - leading: Length, - children: Vec<ParChild>, - last: Last<ParChild>, -} - -impl ParBuilder { - fn new(style: &Style) -> Self { - Self { - dir: style.par.dir, - align: style.par.align, - leading: style.leading(), - children: vec![], - last: Last::None, - } - } - - fn push(&mut self, child: ParChild) { - if let Some(soft) = self.last.any() { - self.push_inner(soft); - } - self.push_inner(child); - } - - fn push_soft(&mut self, child: ParChild) { - self.last.soft(child); - } - - fn push_hard(&mut self, child: ParChild) { - self.last.hard(); - self.push_inner(child); - } - - fn push_inner(&mut self, child: ParChild) { - if let ParChild::Text(text2, style2) = &child { - if let Some(ParChild::Text(text1, style1)) = self.children.last_mut() { - if Rc::ptr_eq(style1, style2) { - text1.push_str(text2); - return; - } - } - } - - self.children.push(child); - } - - fn build(self) -> Option<FlowChild> { - let Self { dir, align, leading, children, .. } = self; - (!children.is_empty()) - .then(|| FlowChild::Node(ParNode { dir, align, leading, children }.pack())) - } -} - -/// Finite state machine for spacing coalescing. -enum Last<N> { - None, - Any, - Soft(N), -} - -impl<N> Last<N> { - fn any(&mut self) -> Option<N> { - match mem::replace(self, Self::Any) { - Self::Soft(soft) => Some(soft), - _ => None, - } - } - - fn soft(&mut self, soft: N) { - if let Self::Any = self { - *self = Self::Soft(soft); - } - } - - fn hard(&mut self) { - *self = Self::None; - } -} diff --git a/src/eval/value.rs b/src/eval/value.rs index 16e8b810..0995ab75 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -1,11 +1,13 @@ use std::any::Any; use std::cmp::Ordering; use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; use std::rc::Rc; -use super::{ops, Array, Dict, Function, Template}; +use super::{ops, Array, Class, Dict, Function, Node}; use crate::diag::StrResult; use crate::geom::{Angle, Color, Fractional, Length, Linear, Relative, RgbaColor}; +use crate::layout::Layout; use crate::syntax::Spanned; use crate::util::EcoString; @@ -24,7 +26,7 @@ pub enum Value { Float(f64), /// A length: `12pt`, `3cm`. Length(Length), - /// An angle: `1.5rad`, `90deg`. + /// An angle: `1.5rad`, `90deg`. Angle(Angle), /// A relative value: `50%`. Relative(Relative), @@ -40,15 +42,33 @@ pub enum Value { Array(Array), /// A dictionary value: `(color: #f79143, pattern: dashed)`. Dict(Dict), - /// A template value: `[*Hi* there]`. - Template(Template), + /// A node value: `[*Hi* there]`. + Node(Node), /// An executable function. Func(Function), + /// A class of nodes. + Class(Class), /// A dynamic value. Dyn(Dynamic), } impl Value { + /// Create an inline-level node value. + pub fn inline<T>(node: T) -> Self + where + T: Layout + Debug + Hash + 'static, + { + Self::Node(Node::inline(node)) + } + + /// Create a block-level node value. + pub fn block<T>(node: T) -> Self + where + T: Layout + Debug + Hash + 'static, + { + Self::Node(Node::block(node)) + } + /// The name of the stored value's type. pub fn type_name(&self) -> &'static str { match self { @@ -66,8 +86,9 @@ impl Value { Self::Str(_) => EcoString::TYPE_NAME, Self::Array(_) => Array::TYPE_NAME, Self::Dict(_) => Dict::TYPE_NAME, - Self::Template(_) => Template::TYPE_NAME, + Self::Node(_) => Node::TYPE_NAME, Self::Func(_) => Function::TYPE_NAME, + Self::Class(_) => Class::TYPE_NAME, Self::Dyn(v) => v.type_name(), } } @@ -80,14 +101,28 @@ impl Value { T::cast(self) } + /// Join the value with another value. + pub fn join(self, rhs: Self) -> StrResult<Self> { + ops::join(self, rhs) + } + /// Return the debug representation of the value. pub fn repr(&self) -> EcoString { format_eco!("{:?}", self) } - /// Join the value with another value. - pub fn join(self, rhs: Self) -> StrResult<Self> { - ops::join(self, rhs) + /// Return the display representation of the value. + pub fn show(self) -> Node { + match self { + Value::None => Node::new(), + Value::Int(v) => Node::Text(format_eco!("{}", v)), + Value::Float(v) => Node::Text(format_eco!("{}", v)), + Value::Str(v) => Node::Text(v), + Value::Node(v) => v, + // For values which can't be shown "naturally", we print the + // representation in monospace. + v => Node::Text(v.repr()).monospaced(), + } } } @@ -114,8 +149,9 @@ impl Debug for Value { Self::Str(v) => Debug::fmt(v, f), Self::Array(v) => Debug::fmt(v, f), Self::Dict(v) => Debug::fmt(v, f), - Self::Template(v) => Debug::fmt(v, f), + Self::Node(_) => f.pad("<template>"), Self::Func(v) => Debug::fmt(v, f), + Self::Class(v) => Debug::fmt(v, f), Self::Dyn(v) => Debug::fmt(v, f), } } @@ -354,14 +390,15 @@ primitive! { f64: "float", Float, Int(v) => v as f64 } primitive! { Length: "length", Length } primitive! { Angle: "angle", Angle } primitive! { Relative: "relative", Relative } -primitive! { Linear: "linear", Linear, Length(v) => v.into(), Relative(v) => v.into() } -primitive! { Fractional: "fractional", Fractional } +primitive! { Linear: "relative length", Linear, Length(v) => v.into(), Relative(v) => v.into() } +primitive! { Fractional: "fractional length", Fractional } primitive! { Color: "color", Color } primitive! { EcoString: "string", Str } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } -primitive! { Template: "template", Template } +primitive! { Node: "template", Node } primitive! { Function: "function", Func } +primitive! { Class: "class", Class } impl Cast<Value> for Value { fn is(_: &Value) -> bool { diff --git a/src/eval/walk.rs b/src/eval/walk.rs deleted file mode 100644 index 0898f20b..00000000 --- a/src/eval/walk.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::rc::Rc; - -use super::{Eval, EvalContext, Template, Value}; -use crate::diag::TypResult; -use crate::geom::Spec; -use crate::layout::Layout; -use crate::library::{GridNode, ParChild, ParNode, TrackSizing}; -use crate::syntax::ast::*; -use crate::util::{BoolExt, EcoString}; - -/// Walk markup, filling the currently built template. -pub trait Walk { - /// Walk the node. - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()>; -} - -impl Walk for Markup { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { - for node in self.nodes() { - node.walk(ctx)?; - } - Ok(()) - } -} - -impl Walk for MarkupNode { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { - match self { - Self::Space => ctx.template.space(), - Self::Linebreak => ctx.template.linebreak(), - Self::Parbreak => ctx.template.parbreak(), - Self::Strong => ctx.template.modify(|s| s.text_mut().strong.flip()), - Self::Emph => ctx.template.modify(|s| s.text_mut().emph.flip()), - Self::Text(text) => ctx.template.text(text), - Self::Raw(raw) => raw.walk(ctx)?, - Self::Math(math) => math.walk(ctx)?, - Self::Heading(heading) => heading.walk(ctx)?, - Self::List(list) => list.walk(ctx)?, - Self::Enum(enum_) => enum_.walk(ctx)?, - Self::Expr(expr) => match expr.eval(ctx)? { - Value::None => {} - Value::Int(v) => ctx.template.text(format_eco!("{}", v)), - Value::Float(v) => ctx.template.text(format_eco!("{}", v)), - Value::Str(v) => ctx.template.text(v), - Value::Template(v) => ctx.template += v, - // For values which can't be shown "naturally", we print the - // representation in monospace. - other => ctx.template.monospace(other.repr()), - }, - } - Ok(()) - } -} - -impl Walk for RawNode { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { - if self.block { - ctx.template.parbreak(); - } - - ctx.template.monospace(&self.text); - - if self.block { - ctx.template.parbreak(); - } - - Ok(()) - } -} - -impl Walk for MathNode { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { - if self.display { - ctx.template.parbreak(); - } - - ctx.template.monospace(self.formula.trim()); - - if self.display { - ctx.template.parbreak(); - } - - Ok(()) - } -} - -impl Walk for HeadingNode { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { - let level = self.level(); - let body = self.body().eval(ctx)?; - - ctx.template.parbreak(); - ctx.template.save(); - ctx.template.modify(move |style| { - let text = style.text_mut(); - let upscale = (1.6 - 0.1 * level as f64).max(0.75); - text.size *= upscale; - text.strong = true; - }); - ctx.template += body; - ctx.template.restore(); - ctx.template.parbreak(); - - Ok(()) - } -} - -impl Walk for ListNode { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { - let body = self.body().eval(ctx)?; - walk_item(ctx, EcoString::from('•'), body); - Ok(()) - } -} - -impl Walk for EnumNode { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { - let body = self.body().eval(ctx)?; - let label = format_eco!("{}.", self.number().unwrap_or(1)); - walk_item(ctx, label, body); - Ok(()) - } -} - -fn walk_item(ctx: &mut EvalContext, label: EcoString, body: Template) { - ctx.template += Template::from_block(move |style| { - let label = Layout::pack(ParNode { - dir: style.par.dir, - align: style.par.align, - leading: style.leading(), - children: vec![ParChild::Text(label.clone(), Rc::clone(&style.text))], - }); - - let spacing = style.text.size / 2.0; - GridNode { - tracks: Spec::new(vec![TrackSizing::Auto; 2], vec![]), - gutter: Spec::new(vec![TrackSizing::Linear(spacing.into())], vec![]), - children: vec![label, body.pack(style)], - } - }); -} diff --git a/src/font.rs b/src/font.rs index 5afd1aca..c2e1beac 100644 --- a/src/font.rs +++ b/src/font.rs @@ -283,11 +283,11 @@ impl Face { /// Look up a vertical metric at the given font size. pub fn vertical_metric(&self, metric: VerticalFontMetric, size: Length) -> Length { match metric { - VerticalFontMetric::Ascender => self.ascender.to_length(size), - VerticalFontMetric::CapHeight => self.cap_height.to_length(size), - VerticalFontMetric::XHeight => self.x_height.to_length(size), + VerticalFontMetric::Ascender => self.ascender.resolve(size), + VerticalFontMetric::CapHeight => self.cap_height.resolve(size), + VerticalFontMetric::XHeight => self.x_height.resolve(size), VerticalFontMetric::Baseline => Length::zero(), - VerticalFontMetric::Descender => self.descender.to_length(size), + VerticalFontMetric::Descender => self.descender.resolve(size), VerticalFontMetric::Linear(v) => v.resolve(size), } } diff --git a/src/frame.rs b/src/frame.rs index b0442a06..46f23446 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -107,6 +107,11 @@ impl Frame { wrapper.push(Point::zero(), Element::Group(group)); *self = wrapper; } + + /// Link the whole frame to a resource. + pub fn link(&mut self, url: impl Into<String>) { + self.push(Point::zero(), Element::Link(url.into(), self.size)); + } } impl Debug for Frame { @@ -171,14 +176,19 @@ pub struct Text { pub face_id: FaceId, /// The font size. pub size: Length, - /// The width of the text run. - pub width: Length, /// Glyph color. pub fill: Paint, /// The glyphs. pub glyphs: Vec<Glyph>, } +impl Text { + /// The width of the text run. + pub fn width(&self) -> Length { + self.glyphs.iter().map(|g| g.x_advance.resolve(self.size)).sum() + } +} + /// A glyph in a run of shaped text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Glyph { diff --git a/src/geom/em.rs b/src/geom/em.rs index 05d1d7d5..1868222f 100644 --- a/src/geom/em.rs +++ b/src/geom/em.rs @@ -29,7 +29,7 @@ impl Em { } /// Convert to a length at the given font size. - pub fn to_length(self, font_size: Length) -> Length { + pub fn resolve(self, font_size: Length) -> Length { self.get() * font_size } diff --git a/src/geom/linear.rs b/src/geom/linear.rs index 77923c43..78602d8b 100644 --- a/src/geom/linear.rs +++ b/src/geom/linear.rs @@ -36,6 +36,16 @@ impl Linear { self.rel.resolve(length) + self.abs } + /// Compose with another linear. + /// + /// The resulting linear is (self ∘ inner)(x) = self(inner(x)). + pub fn compose(self, inner: Self) -> Self { + Self { + rel: self.rel * inner.rel, + abs: self.rel.resolve(inner.abs) + self.abs, + } + } + /// Whether both parts are zero. pub fn is_zero(self) -> bool { self.rel.is_zero() && self.abs.is_zero() @@ -155,10 +165,7 @@ impl Mul<Linear> for f64 { type Output = Linear; fn mul(self, other: Linear) -> Linear { - Linear { - rel: self * other.rel, - abs: self * other.abs, - } + other * self } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 9c57152a..114e7491 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -15,17 +15,57 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::rc::Rc; +use crate::eval::Styles; use crate::font::FontStore; use crate::frame::Frame; -use crate::geom::{Align, Linear, Point, Sides, Spec, Transform}; +use crate::geom::{Align, Linear, Point, Sides, Size, Spec, Transform}; use crate::image::ImageStore; -use crate::library::{AlignNode, DocumentNode, PadNode, SizedNode, TransformNode}; +use crate::library::{AlignNode, PadNode, PageNode, SizedNode, TransformNode}; use crate::Context; -/// Layout a document node into a collection of frames. -pub fn layout(ctx: &mut Context, node: &DocumentNode) -> Vec<Rc<Frame>> { - let mut ctx = LayoutContext::new(ctx); - node.layout(&mut ctx) +/// The root layout node, a document consisting of top-level page runs. +#[derive(Hash)] +pub struct RootNode(pub Vec<PageNode>); + +impl RootNode { + /// Layout the document into a sequence of frames, one per page. + pub fn layout(&self, ctx: &mut Context) -> Vec<Rc<Frame>> { + let mut ctx = LayoutContext::new(ctx); + self.0.iter().flat_map(|node| node.layout(&mut ctx)).collect() + } +} + +impl Debug for RootNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Root ")?; + f.debug_list().entries(&self.0).finish() + } +} + +/// A node that can be layouted into a sequence of regions. +/// +/// Layout return one frame per used region alongside constraints that define +/// whether the result is reusable in other regions. +pub trait Layout { + /// Layout the node into the given regions, producing constrained frames. + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>>; + + /// Convert to a packed node. + fn pack(self) -> PackedNode + where + Self: Debug + Hash + Sized + 'static, + { + PackedNode { + #[cfg(feature = "layout-cache")] + hash: self.hash64(), + node: Rc::new(self), + styles: Styles::new(), + } + } } /// The context for layouting. @@ -37,6 +77,9 @@ pub struct LayoutContext<'a> { /// Caches layouting artifacts. #[cfg(feature = "layout-cache")] pub layouts: &'a mut LayoutCache, + /// The inherited style properties. + // TODO(style): This probably shouldn't be here. + pub styles: Styles, /// How deeply nested the current layout tree position is. #[cfg(feature = "layout-cache")] pub level: usize, @@ -50,51 +93,50 @@ impl<'a> LayoutContext<'a> { images: &mut ctx.images, #[cfg(feature = "layout-cache")] layouts: &mut ctx.layouts, + styles: ctx.styles.clone(), #[cfg(feature = "layout-cache")] level: 0, } } } -/// A node that can be layouted into a sequence of regions. +/// A layout node that produces an empty frame. /// -/// Layout return one frame per used region alongside constraints that define -/// whether the result is reusable in other regions. -pub trait Layout { - /// Layout the node into the given regions, producing constrained frames. +/// The packed version of this is returned by [`PackedNode::default`]. +#[derive(Debug, Hash)] +pub struct EmptyNode; + +impl Layout for EmptyNode { fn layout( &self, - ctx: &mut LayoutContext, + _: &mut LayoutContext, regions: &Regions, - ) -> Vec<Constrained<Rc<Frame>>>; - - /// Convert to a packed node. - fn pack(self) -> PackedNode - where - Self: Debug + Hash + Sized + 'static, - { - PackedNode { - #[cfg(feature = "layout-cache")] - hash: { - let mut state = fxhash::FxHasher64::default(); - self.type_id().hash(&mut state); - self.hash(&mut state); - state.finish() - }, - node: Rc::new(self), - } + ) -> Vec<Constrained<Rc<Frame>>> { + let size = regions.expand.select(regions.current, Size::zero()); + let mut cts = Constraints::new(regions.expand); + cts.exact = regions.current.filter(regions.expand); + vec![Frame::new(size).constrain(cts)] } } -/// A packed layouting node with precomputed hash. +/// A packed layouting node with style properties and a precomputed hash. #[derive(Clone)] pub struct PackedNode { + /// The type-erased node. node: Rc<dyn Bounds>, + /// A precomputed hash for the node. #[cfg(feature = "layout-cache")] hash: u64, + /// The node's styles. + pub styles: Styles, } impl PackedNode { + /// Check whether the contained node is a specific layout node. + pub fn is<T: 'static>(&self) -> bool { + self.node.as_any().is::<T>() + } + /// Try to downcast to a specific layout node. pub fn downcast<T>(&self) -> Option<&T> where @@ -103,6 +145,16 @@ impl PackedNode { self.node.as_any().downcast_ref() } + /// Style the node with styles from a style map. + pub fn styled(mut self, styles: Styles) -> Self { + if self.styles.is_empty() { + self.styles = styles; + } else { + self.styles.apply(&styles); + } + self + } + /// Force a size for this node. pub fn sized(self, sizing: Spec<Option<Linear>>) -> Self { if sizing.any(Option::is_some) { @@ -156,12 +208,20 @@ impl Layout for PackedNode { regions: &Regions, ) -> Vec<Constrained<Rc<Frame>>> { #[cfg(not(feature = "layout-cache"))] - return self.node.layout(ctx, regions); + return self.layout_impl(ctx, regions); + + #[cfg(feature = "layout-cache")] + let hash = { + let mut state = fxhash::FxHasher64::default(); + self.hash(&mut state); + ctx.styles.hash(&mut state); + state.finish() + }; #[cfg(feature = "layout-cache")] - ctx.layouts.get(self.hash, regions).unwrap_or_else(|| { + ctx.layouts.get(hash, regions).unwrap_or_else(|| { ctx.level += 1; - let frames = self.node.layout(ctx, regions); + let frames = self.layout_impl(ctx, regions); ctx.level -= 1; let entry = FramesEntry::new(frames.clone(), ctx.level); @@ -177,7 +237,7 @@ impl Layout for PackedNode { panic!("constraints did not match regions they were created for"); } - ctx.layouts.insert(self.hash, entry); + ctx.layouts.insert(hash, entry); frames }) } @@ -190,30 +250,74 @@ impl Layout for PackedNode { } } -impl Hash for PackedNode { - fn hash<H: Hasher>(&self, _state: &mut H) { - #[cfg(feature = "layout-cache")] - _state.write_u64(self.hash); - #[cfg(not(feature = "layout-cache"))] - unimplemented!() +impl PackedNode { + /// Layout the node without checking the cache. + fn layout_impl( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>> { + let new = self.styles.chain(&ctx.styles); + let prev = std::mem::replace(&mut ctx.styles, new); + let frames = self.node.layout(ctx, regions); + ctx.styles = prev; + frames + } +} + +impl Default for PackedNode { + fn default() -> Self { + EmptyNode.pack() } } impl Debug for PackedNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } self.node.fmt(f) } } +impl PartialEq for PackedNode { + fn eq(&self, other: &Self) -> bool { + Rc::as_ptr(&self.node) as *const () == Rc::as_ptr(&other.node) as *const () + } +} + +impl Hash for PackedNode { + fn hash<H: Hasher>(&self, state: &mut H) { + // Hash the node. + #[cfg(feature = "layout-cache")] + state.write_u64(self.hash); + #[cfg(not(feature = "layout-cache"))] + state.write_u64(self.hash64()); + + // Hash the styles. + self.styles.hash(state); + } +} + trait Bounds: Layout + Debug + 'static { fn as_any(&self) -> &dyn Any; + fn hash64(&self) -> u64; } impl<T> Bounds for T where - T: Layout + Debug + 'static, + T: Layout + Hash + Debug + 'static, { fn as_any(&self) -> &dyn Any { self } + + fn hash64(&self) -> u64 { + // Also hash the TypeId since nodes with different types but + // equal data should be different. + let mut state = fxhash::FxHasher64::default(); + self.type_id().hash(&mut state); + self.hash(&mut state); + state.finish() + } } @@ -2,30 +2,32 @@ //! //! # Steps //! - **Parsing:** The parsing step first transforms a plain string into an -//! [iterator of tokens][tokens]. This token stream is [parsed] into [markup]. -//! The syntactical structures describing markup and embedded code can be -//! found in the [syntax] module. +//! [iterator of tokens][tokens]. This token stream is [parsed] into a +//! [green tree]. The green tree itself is untyped, but a typed layer over it +//! is provided in the [AST] module. //! - **Evaluation:** The next step is to [evaluate] the markup. This produces a //! [module], consisting of a scope of values that were exported by the code -//! and a template with the contents of the module. This template can be -//! instantiated with a style to produce a layout tree, a high-level, fully -//! styled representation, rooted in the [document node]. The nodes of this -//! tree are self-contained and order-independent and thus much better suited -//! for layouting than the raw markup. +//! and a [node] with the contents of the module. This node can be converted +//! into a [layout tree], a hierarchical, styled representation of the +//! document. The nodes of this tree are well structured and order-independent +//! and thus much better suited for layouting than the raw markup. //! - **Layouting:** Next, the tree is [layouted] into a portable version of the //! typeset document. The output of this is a collection of [`Frame`]s (one -//! per page), ready for exporting. +//! per page), ready for exporting. This step is supported by an incremental +//! [cache] that enables reuse of intermediate layouting results. //! - **Exporting:** The finished layout can be exported into a supported //! format. Currently, the only supported output format is [PDF]. //! //! [tokens]: parse::Tokens //! [parsed]: parse::parse -//! [markup]: syntax::ast::Markup -//! [evaluate]: eval::eval +//! [green tree]: syntax::GreenNode +//! [AST]: syntax::ast +//! [evaluate]: Context::evaluate //! [module]: eval::Module -//! [layout tree]: layout::LayoutTree -//! [document node]: library::DocumentNode -//! [layouted]: layout::layout +//! [node]: eval::Node +//! [layout tree]: layout::RootNode +//! [layouted]: layout::RootNode::layout +//! [cache]: layout::LayoutCache //! [PDF]: export::pdf #[macro_use] @@ -44,22 +46,19 @@ pub mod library; pub mod loading; pub mod parse; pub mod source; -pub mod style; pub mod syntax; use std::rc::Rc; use crate::diag::TypResult; -use crate::eval::{Module, Scope}; +use crate::eval::{Eval, EvalContext, Module, Scope, Styles}; use crate::font::FontStore; use crate::frame::Frame; use crate::image::ImageStore; #[cfg(feature = "layout-cache")] use crate::layout::{EvictionPolicy, LayoutCache}; -use crate::library::DocumentNode; use crate::loading::Loader; use crate::source::{SourceId, SourceStore}; -use crate::style::Style; /// The core context which holds the loader, configuration and cached artifacts. pub struct Context { @@ -76,8 +75,8 @@ pub struct Context { pub layouts: LayoutCache, /// The standard library scope. std: Scope, - /// The default style. - style: Style, + /// The default styles. + styles: Styles, } impl Context { @@ -96,21 +95,21 @@ impl Context { &self.std } - /// A read-only reference to the style. - pub fn style(&self) -> &Style { - &self.style + /// A read-only reference to the styles. + pub fn styles(&self) -> &Styles { + &self.styles } /// Evaluate a source file and return the resulting module. + /// + /// Returns either a module containing a scope with top-level bindings and a + /// layoutable node or diagnostics in the form of a vector of error message + /// with file and span information. pub fn evaluate(&mut self, id: SourceId) -> TypResult<Module> { - let ast = self.sources.get(id).ast()?; - eval::eval(self, id, &ast) - } - - /// Execute a source file and produce the resulting page nodes. - pub fn execute(&mut self, id: SourceId) -> TypResult<DocumentNode> { - let module = self.evaluate(id)?; - Ok(module.template.to_document(&self.style)) + let markup = self.sources.get(id).ast()?; + let mut ctx = EvalContext::new(self, id); + let node = markup.eval(&mut ctx)?; + Ok(Module { scope: ctx.scopes.top, node }) } /// Typeset a source file into a collection of layouted frames. @@ -119,8 +118,9 @@ impl Context { /// diagnostics in the form of a vector of error message with file and span /// information. pub fn typeset(&mut self, id: SourceId) -> TypResult<Vec<Rc<Frame>>> { - let tree = self.execute(id)?; - let frames = layout::layout(self, &tree); + let module = self.evaluate(id)?; + let tree = module.into_root(); + let frames = tree.layout(self); Ok(frames) } @@ -136,7 +136,7 @@ impl Context { /// This struct is created by [`Context::builder`]. pub struct ContextBuilder { std: Option<Scope>, - style: Option<Style>, + styles: Option<Styles>, #[cfg(feature = "layout-cache")] policy: EvictionPolicy, #[cfg(feature = "layout-cache")] @@ -151,9 +151,9 @@ impl ContextBuilder { self } - /// The initial properties for page size, font selection and so on. - pub fn style(mut self, style: Style) -> Self { - self.style = Some(style); + /// The default properties for page size, font selection and so on. + pub fn styles(mut self, styles: Styles) -> Self { + self.styles = Some(styles); self } @@ -185,7 +185,7 @@ impl ContextBuilder { #[cfg(feature = "layout-cache")] layouts: LayoutCache::new(self.policy, self.max_size), std: self.std.unwrap_or_else(library::new), - style: self.style.unwrap_or_default(), + styles: self.styles.unwrap_or_default(), } } } @@ -194,7 +194,7 @@ impl Default for ContextBuilder { fn default() -> Self { Self { std: None, - style: None, + styles: None, #[cfg(feature = "layout-cache")] policy: EvictionPolicy::default(), #[cfg(feature = "layout-cache")] diff --git a/src/library/align.rs b/src/library/align.rs index 18920369..c16277f6 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -1,29 +1,19 @@ use super::prelude::*; +use super::ParNode; /// `align`: Configure the alignment along the layouting axes. pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - castable! { - Spec<Option<Align>>, - Expected: "1d or 2d alignment", - @align: Align => { - let mut aligns = Spec::default(); - aligns.set(align.axis(), Some(*align)); - aligns - }, - @aligns: Spec<Align> => aligns.map(Some), + let aligns: Spec<_> = args.expect("alignment")?; + let body: Node = args.expect("body")?; + let mut styles = Styles::new(); + if let Some(align) = aligns.x { + styles.set(ParNode::ALIGN, align); } - let aligns = args.expect::<Spec<_>>("alignment")?; - let body = args.expect::<Template>("body")?; - Ok(Value::Template(Template::from_block(move |style| { - let mut style = style.clone(); - if let Some(x) = aligns.x { - style.par_mut().align = x; - } - - body.pack(&style).aligned(aligns) - }))) + Ok(Value::block( + body.into_block().styled(styles).aligned(aligns), + )) } /// A node that aligns its child. @@ -67,3 +57,19 @@ impl Layout for AlignNode { frames } } + +dynamic! { + Align: "alignment", +} + +castable! { + Spec<Option<Align>>, + Expected: "1d or 2d alignment", + @align: Align => { + let mut aligns = Spec::default(); + aligns.set(align.axis(), Some(*align)); + aligns + }, + @aligns: Spec<Align> => aligns.map(Some), + +} diff --git a/src/library/deco.rs b/src/library/deco.rs deleted file mode 100644 index cb065689..00000000 --- a/src/library/deco.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::prelude::*; -use crate::util::EcoString; - -/// `strike`: Typeset striken-through text. -pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - line_impl(args, LineKind::Strikethrough) -} - -/// `underline`: Typeset underlined text. -pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - line_impl(args, LineKind::Underline) -} - -/// `overline`: Typeset text with an overline. -pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - line_impl(args, LineKind::Overline) -} - -fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> { - let stroke = args.named("stroke")?.or_else(|| args.find()); - let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find()); - let offset = args.named("offset")?; - let extent = args.named("extent")?.unwrap_or_default(); - let body: Template = args.expect("body")?; - Ok(Value::Template(body.decorate(Decoration::Line( - LineDecoration { kind, stroke, thickness, offset, extent }, - )))) -} - -/// `link`: Typeset text as a link. -pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let url = args.expect::<EcoString>("url")?; - let body = args.find().unwrap_or_else(|| { - let mut template = Template::new(); - template.text(url.trim_start_matches("mailto:").trim_start_matches("tel:")); - template - }); - - Ok(Value::Template(body.decorate(Decoration::Link(url)))) -} - -/// A decoration for a frame. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum Decoration { - /// A link to an external resource. - Link(EcoString), - /// An underline/strikethrough/overline decoration. - Line(LineDecoration), -} - -impl Decoration { - /// Apply a decoration to a child's frame. - pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) { - match self { - Decoration::Link(href) => { - let link = Element::Link(href.to_string(), frame.size); - frame.push(Point::zero(), link); - } - Decoration::Line(line) => { - line.apply(ctx, frame); - } - } - } -} - -/// Defines a line that is positioned over, under or on top of text. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct LineDecoration { - /// The kind of line. - pub kind: LineKind, - /// Stroke color of the line, defaults to the text color if `None`. - pub stroke: Option<Paint>, - /// Thickness of the line's strokes (dependent on scaled font size), read - /// from the font tables if `None`. - pub thickness: Option<Linear>, - /// Position of the line relative to the baseline (dependent on scaled font - /// size), read from the font tables if `None`. - pub offset: Option<Linear>, - /// Amount that the line will be longer or shorter than its associated text - /// (dependent on scaled font size). - pub extent: Linear, -} - -/// The kind of line decoration. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum LineKind { - /// A line under text. - Underline, - /// A line through text. - Strikethrough, - /// A line over text. - Overline, -} - -impl LineDecoration { - /// Apply a line decoration to a all text elements in a frame. - pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) { - for i in 0 .. frame.elements.len() { - let (pos, child) = &frame.elements[i]; - if let Element::Text(text) = child { - let face = ctx.fonts.get(text.face_id); - let metrics = match self.kind { - LineKind::Underline => face.underline, - LineKind::Strikethrough => face.strikethrough, - LineKind::Overline => face.overline, - }; - - let thickness = self - .thickness - .map(|s| s.resolve(text.size)) - .unwrap_or(metrics.thickness.to_length(text.size)); - - let stroke = Stroke { - paint: self.stroke.unwrap_or(text.fill), - thickness, - }; - - let offset = self - .offset - .map(|s| s.resolve(text.size)) - .unwrap_or(-metrics.position.to_length(text.size)); - - let extent = self.extent.resolve(text.size); - - let subpos = Point::new(pos.x - extent, pos.y + offset); - let target = Point::new(text.width + 2.0 * extent, Length::zero()); - let shape = Shape::stroked(Geometry::Line(target), stroke); - frame.push(subpos, Element::Shape(shape)); - } - } - } -} diff --git a/src/library/document.rs b/src/library/document.rs deleted file mode 100644 index fe01d2df..00000000 --- a/src/library/document.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::prelude::*; -use super::PageNode; - -/// The root layout node, a document consisting of top-level page runs. -#[derive(Debug, Hash)] -pub struct DocumentNode { - /// The page runs. - pub pages: Vec<PageNode>, -} - -impl DocumentNode { - /// Layout the document into a sequence of frames, one per page. - pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { - self.pages.iter().flat_map(|node| node.layout(ctx)).collect() - } -} diff --git a/src/library/flow.rs b/src/library/flow.rs index 98b518b7..cfa761b6 100644 --- a/src/library/flow.rs +++ b/src/library/flow.rs @@ -1,50 +1,14 @@ use std::fmt::{self, Debug, Formatter}; use super::prelude::*; -use super::{AlignNode, ParNode, PlacedNode, Spacing}; - -/// `flow`: A vertical flow of paragraphs and other layout nodes. -pub fn flow(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - enum Child { - Spacing(Spacing), - Any(Template), - } - - castable! { - Child, - Expected: "linear, fractional or template", - Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())), - Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())), - Value::Linear(v) => Self::Spacing(Spacing::Linear(v)), - Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)), - Value::Template(v) => Self::Any(v), - } - - let children: Vec<Child> = args.all().collect(); - - Ok(Value::Template(Template::from_block(move |style| { - let children = children - .iter() - .map(|child| match child { - Child::Spacing(spacing) => FlowChild::Spacing(*spacing), - Child::Any(node) => FlowChild::Node(node.pack(style)), - }) - .collect(); - - FlowNode { children } - }))) -} +use super::{AlignNode, ParNode, PlacedNode, SpacingKind, SpacingNode, TextNode}; /// A vertical flow of content consisting of paragraphs and other layout nodes. /// /// This node is reponsible for layouting both the top-level content flow and /// the contents of boxes. -#[derive(Debug, Hash)] -pub struct FlowNode { - /// The children that compose the flow. There are different kinds of - /// children for different purposes. - pub children: Vec<FlowChild>, -} +#[derive(Hash)] +pub struct FlowNode(pub Vec<FlowChild>); impl Layout for FlowNode { fn layout( @@ -56,19 +20,54 @@ impl Layout for FlowNode { } } +impl Debug for FlowNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Flow ")?; + f.debug_list().entries(&self.0).finish() + } +} + /// A child of a flow node. #[derive(Hash)] pub enum FlowChild { + /// A paragraph/block break. + Break(Styles), /// Vertical spacing between other children. - Spacing(Spacing), + Spacing(SpacingNode), /// An arbitrary node. Node(PackedNode), } +impl FlowChild { + /// A reference to the child's styles. + pub fn styles(&self) -> &Styles { + match self { + Self::Break(styles) => styles, + Self::Spacing(node) => &node.styles, + Self::Node(node) => &node.styles, + } + } + + /// A mutable reference to the child's styles. + pub fn styles_mut(&mut self) -> &mut Styles { + match self { + Self::Break(styles) => styles, + Self::Spacing(node) => &mut node.styles, + Self::Node(node) => &mut node.styles, + } + } +} + impl Debug for FlowChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Spacing(spacing) => spacing.fmt(f), + Self::Break(styles) => { + if f.alternate() { + styles.fmt(f)?; + } + write!(f, "Break") + } + Self::Spacing(node) => node.fmt(f), Self::Node(node) => node.fmt(f), } } @@ -118,7 +117,7 @@ impl<'a> FlowLayouter<'a> { regions.expand.y = false; Self { - children: &flow.children, + children: &flow.0, expand, full, regions, @@ -132,15 +131,21 @@ impl<'a> FlowLayouter<'a> { /// Layout all children. fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> { for child in self.children { - match *child { - FlowChild::Spacing(Spacing::Linear(v)) => { - self.layout_absolute(v); - } - FlowChild::Spacing(Spacing::Fractional(v)) => { - self.items.push(FlowItem::Fractional(v)); - self.fr += v; + match child { + FlowChild::Break(styles) => { + let chain = styles.chain(&ctx.styles); + let em = chain.get(TextNode::SIZE).abs; + let amount = chain.get(ParNode::SPACING).resolve(em); + self.layout_absolute(amount.into()); } - FlowChild::Node(ref node) => { + FlowChild::Spacing(node) => match node.kind { + SpacingKind::Linear(v) => self.layout_absolute(v), + SpacingKind::Fractional(v) => { + self.items.push(FlowItem::Fractional(v)); + self.fr += v; + } + }, + FlowChild::Node(node) => { if self.regions.is_full() { self.finish_region(); } @@ -166,18 +171,25 @@ impl<'a> FlowLayouter<'a> { /// Layout a node. fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode) { + // Placed nodes that are out of flow produce placed items which aren't + // aligned later. if let Some(placed) = node.downcast::<PlacedNode>() { - let frame = node.layout(ctx, &self.regions).remove(0); if placed.out_of_flow() { + let frame = node.layout(ctx, &self.regions).remove(0); self.items.push(FlowItem::Placed(frame.item)); return; } } + // How to align the node. let aligns = Spec::new( // For non-expanding paragraphs it is crucial that we align the // whole paragraph according to its internal alignment. - node.downcast::<ParNode>().map_or(Align::Left, |par| par.align), + if node.is::<ParNode>() { + node.styles.chain(&ctx.styles).get(ParNode::ALIGN) + } else { + Align::Left + }, // Vertical align node alignment is respected by the flow node. node.downcast::<AlignNode>() .and_then(|aligned| aligned.aligns.y) @@ -235,7 +247,7 @@ impl<'a> FlowLayouter<'a> { output.push_frame(pos, frame); } FlowItem::Placed(frame) => { - output.push_frame(Point::with_y(offset), frame); + output.push_frame(Point::zero(), frame); } } } diff --git a/src/library/grid.rs b/src/library/grid.rs index 7a9d88c3..d341cf5b 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -10,7 +10,7 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { Value::Relative(v) => vec![TrackSizing::Linear(v.into())], Value::Linear(v) => vec![TrackSizing::Linear(v)], Value::Fractional(v) => vec![TrackSizing::Fractional(v)], - Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize], + Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?], Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) @@ -39,15 +39,11 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { row_gutter.unwrap_or(base_gutter), ); - let children: Vec<Template> = args.all().collect(); - - Ok(Value::Template(Template::from_block(move |style| { - GridNode { - tracks: tracks.clone(), - gutter: gutter.clone(), - children: children.iter().map(|child| child.pack(style)).collect(), - } - }))) + Ok(Value::block(GridNode { + tracks, + gutter, + children: args.all().map(Node::into_block).collect(), + })) } /// A node that arranges its children in a grid. diff --git a/src/library/heading.rs b/src/library/heading.rs new file mode 100644 index 00000000..96ff2688 --- /dev/null +++ b/src/library/heading.rs @@ -0,0 +1,63 @@ +use super::prelude::*; +use super::{FontFamily, TextNode}; + +/// A section heading. +#[derive(Debug, Hash)] +pub struct HeadingNode { + /// The node that produces the heading's contents. + pub child: PackedNode, + /// The logical nesting depth of the section, starting from one. In the + /// default style, this controls the text size of the heading. + pub level: usize, +} + +#[properties] +impl HeadingNode { + /// The heading's font family. + pub const FAMILY: Smart<String> = Smart::Auto; + /// The fill color of heading in the text. Just the surrounding text color + /// if `auto`. + pub const FILL: Smart<Paint> = Smart::Auto; +} + +impl Construct for HeadingNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + Ok(Node::block(Self { + child: args.expect::<Node>("body")?.into_block(), + level: args.named("level")?.unwrap_or(1), + })) + } +} + +impl Set for HeadingNode { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + styles.set_opt(Self::FAMILY, args.named("family")?); + styles.set_opt(Self::FILL, args.named("fill")?); + Ok(()) + } +} + +impl Layout for HeadingNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>> { + let upscale = (1.6 - 0.1 * self.level as f64).max(0.75); + ctx.styles.set(TextNode::STRONG, true); + ctx.styles.set(TextNode::SIZE, Relative::new(upscale).into()); + + if let Smart::Custom(family) = ctx.styles.get_ref(Self::FAMILY) { + let list: Vec<_> = std::iter::once(FontFamily::named(family)) + .chain(ctx.styles.get_ref(TextNode::FAMILY_LIST).iter().cloned()) + .collect(); + ctx.styles.set(TextNode::FAMILY_LIST, list); + } + + if let Smart::Custom(fill) = ctx.styles.get(Self::FILL) { + ctx.styles.set(TextNode::FILL, fill); + } + + self.child.layout(ctx, regions) + } +} diff --git a/src/library/image.rs b/src/library/image.rs index 08ed5069..efb246a1 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -1,6 +1,7 @@ use std::io; use super::prelude::*; +use super::LinkNode; use crate::diag::Error; use crate::image::ImageId; @@ -20,9 +21,9 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { }) })?; - Ok(Value::Template(Template::from_inline(move |_| { - ImageNode { id, fit }.pack().sized(Spec::new(width, height)) - }))) + Ok(Value::inline( + ImageNode { id, fit }.pack().sized(Spec::new(width, height)), + )) } /// An image node. @@ -85,6 +86,11 @@ impl Layout for ImageNode { frame.clip(); } + // Apply link if it exists. + if let Some(url) = ctx.styles.get_ref(LinkNode::URL) { + frame.link(url); + } + vec![frame.constrain(Constraints::tight(regions))] } } diff --git a/src/library/link.rs b/src/library/link.rs new file mode 100644 index 00000000..40604a62 --- /dev/null +++ b/src/library/link.rs @@ -0,0 +1,28 @@ +use super::prelude::*; +use crate::util::EcoString; + +/// `link`: Link text or other elements. +pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { + let url: String = args.expect::<EcoString>("url")?.into(); + let body = args.find().unwrap_or_else(|| { + let mut text = url.as_str(); + for prefix in ["mailto:", "tel:"] { + text = text.trim_start_matches(prefix); + } + Node::Text(text.into()) + }); + + Ok(Value::Node( + body.styled(Styles::one(LinkNode::URL, Some(url))), + )) +} + +/// Host for link styles. +#[derive(Debug, Hash)] +pub struct LinkNode; + +#[properties] +impl LinkNode { + /// An URL to link to. + pub const URL: Option<String> = None; +} diff --git a/src/library/list.rs b/src/library/list.rs new file mode 100644 index 00000000..25eb3600 --- /dev/null +++ b/src/library/list.rs @@ -0,0 +1,102 @@ +use std::hash::Hash; + +use super::prelude::*; +use super::{GridNode, TextNode, TrackSizing}; + +/// An unordered or ordered list. +#[derive(Debug, Hash)] +pub struct ListNode<L> { + /// The node that produces the item's body. + pub child: PackedNode, + /// The list labelling style -- unordered or ordered. + pub labelling: L, +} + +#[properties] +impl<L: Labelling> ListNode<L> { + /// The indentation of each item's label. + pub const LABEL_INDENT: Linear = Relative::new(0.0).into(); + /// The space between the label and the body of each item. + pub const BODY_INDENT: Linear = Relative::new(0.5).into(); +} + +impl<L: Labelling> Construct for ListNode<L> { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + Ok(args + .all() + .map(|node: Node| { + Node::block(Self { + child: node.into_block(), + labelling: L::default(), + }) + }) + .sum()) + } +} + +impl<L: Labelling> Set for ListNode<L> { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?); + styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?); + Ok(()) + } +} + +impl<L: Labelling> Layout for ListNode<L> { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>> { + let em = ctx.styles.get(TextNode::SIZE).abs; + let label_indent = ctx.styles.get(Self::LABEL_INDENT).resolve(em); + let body_indent = ctx.styles.get(Self::BODY_INDENT).resolve(em); + + let columns = vec![ + TrackSizing::Linear(label_indent.into()), + TrackSizing::Auto, + TrackSizing::Linear(body_indent.into()), + TrackSizing::Auto, + ]; + + let children = vec![ + PackedNode::default(), + Node::Text(self.labelling.label()).into_block(), + PackedNode::default(), + self.child.clone(), + ]; + + GridNode { + tracks: Spec::new(columns, vec![]), + gutter: Spec::default(), + children, + } + .layout(ctx, regions) + } +} + +/// How to label a list. +pub trait Labelling: Debug + Default + Hash + 'static { + /// Return the item's label. + fn label(&self) -> EcoString; +} + +/// Unordered list labelling style. +#[derive(Debug, Default, Hash)] +pub struct Unordered; + +impl Labelling for Unordered { + fn label(&self) -> EcoString { + '•'.into() + } +} + +/// Ordered list labelling style. +#[derive(Debug, Default, Hash)] +pub struct Ordered(pub Option<usize>); + +impl Labelling for Ordered { + fn label(&self) -> EcoString { + format_eco!("{}.", self.0.unwrap_or(1)) + } +} diff --git a/src/library/mod.rs b/src/library/mod.rs index d60a13ea..b2dd0dbe 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -4,11 +4,12 @@ //! definitions. mod align; -mod deco; -mod document; mod flow; mod grid; +mod heading; mod image; +mod link; +mod list; mod pad; mod page; mod par; @@ -23,10 +24,15 @@ mod utility; /// Helpful imports for creating library functionality. mod prelude { + pub use std::fmt::{self, Debug, Formatter}; pub use std::rc::Rc; + pub use typst_macros::properties; + pub use crate::diag::{At, TypResult}; - pub use crate::eval::{Args, EvalContext, Smart, Template, Value}; + pub use crate::eval::{ + Args, Construct, EvalContext, Node, Property, Set, Smart, Styles, Value, + }; pub use crate::frame::*; pub use crate::geom::*; pub use crate::layout::*; @@ -36,10 +42,11 @@ mod prelude { pub use self::image::*; pub use align::*; -pub use deco::*; -pub use document::*; pub use flow::*; pub use grid::*; +pub use heading::*; +pub use link::*; +pub use list::*; pub use pad::*; pub use page::*; pub use par::*; @@ -54,28 +61,38 @@ pub use utility::*; use crate::eval::{Scope, Value}; use crate::geom::*; -use crate::style::FontFamily; /// Construct a scope containing all standard library definitions. pub fn new() -> Scope { let mut std = Scope::new(); - // Text. - std.def_func("font", font); - std.def_func("par", par); + // Classes. + std.def_class::<PageNode>("page"); + std.def_class::<ParNode>("par"); + std.def_class::<TextNode>("text"); + std.def_class::<HeadingNode>("heading"); + std.def_class::<ListNode<Unordered>>("list"); + std.def_class::<ListNode<Ordered>>("enum"); + + // Text functions. + // TODO(style): These should be classes, once that works for inline nodes. std.def_func("strike", strike); std.def_func("underline", underline); std.def_func("overline", overline); std.def_func("link", link); - // Layout. - std.def_func("page", page); + // Break and spacing functions. std.def_func("pagebreak", pagebreak); + std.def_func("parbreak", parbreak); + std.def_func("linebreak", linebreak); std.def_func("h", h); std.def_func("v", v); + + // Layout functions. + // TODO(style): Decide which of these should be classes + // (and which of their properties should be settable). std.def_func("box", box_); std.def_func("block", block); - std.def_func("flow", flow); std.def_func("stack", stack); std.def_func("grid", grid); std.def_func("pad", pad); @@ -84,15 +101,13 @@ pub fn new() -> Scope { std.def_func("move", move_); std.def_func("scale", scale); std.def_func("rotate", rotate); - - // Elements. std.def_func("image", image); std.def_func("rect", rect); std.def_func("square", square); std.def_func("ellipse", ellipse); std.def_func("circle", circle); - // Utility. + // Utility functions. std.def_func("assert", assert); std.def_func("type", type_); std.def_func("repr", repr); @@ -110,14 +125,15 @@ pub fn new() -> Scope { std.def_func("len", len); std.def_func("sorted", sorted); - // Colors. + // Predefined colors. + // TODO: More colors. std.def_const("white", RgbaColor::WHITE); std.def_const("black", RgbaColor::BLACK); std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); - // Arbitrary constants. + // Other constants. std.def_const("ltr", Dir::LTR); std.def_const("rtl", Dir::RTL); std.def_const("ttb", Dir::TTB); @@ -139,17 +155,20 @@ dynamic! { Dir: "direction", } -dynamic! { - Align: "alignment", +castable! { + Paint, + Expected: "color", + Value::Color(color) => Paint::Solid(color), } -dynamic! { - FontFamily: "font family", - Value::Str(string) => Self::Named(string.to_lowercase()), +castable! { + usize, + Expected: "non-negative integer", + Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?, } castable! { - Paint, - Expected: "color", - Value::Color(color) => Paint::Solid(color), + String, + Expected: "string", + Value::Str(string) => string.into(), } diff --git a/src/library/pad.rs b/src/library/pad.rs index 681da73e..75fea2e5 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -7,7 +7,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let top = args.named("top")?; let right = args.named("right")?; let bottom = args.named("bottom")?; - let body: Template = args.expect("body")?; + let body: Node = args.expect("body")?; let padding = Sides::new( left.or(all).unwrap_or_default(), top.or(all).unwrap_or_default(), @@ -15,9 +15,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { bottom.or(all).unwrap_or_default(), ); - Ok(Value::Template(Template::from_block(move |style| { - body.pack(style).padded(padding) - }))) + Ok(Value::block(body.into_block().padded(padding))) } /// A node that adds padding to its child. diff --git a/src/library/page.rs b/src/library/page.rs index 0d29ddb6..0e690770 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -1,115 +1,405 @@ -use super::prelude::*; -use crate::style::{Paper, PaperClass}; - -/// `page`: Configure pages. -pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - castable! { - Paper, - Expected: "string", - Value::Str(string) => Paper::from_name(&string).ok_or("unknown paper")?, - } - - let paper = args.named::<Paper>("paper")?.or_else(|| args.find()); - let width = args.named::<Smart<_>>("width")?; - let height = args.named::<Smart<_>>("height")?; - let flip = args.named("flip")?; - let margins = args.named("margins")?; - let left = args.named("left")?; - let top = args.named("top")?; - let right = args.named("right")?; - let bottom = args.named("bottom")?; - let fill = args.named("fill")?; - - ctx.template.modify(move |style| { - let page = style.page_mut(); - - if let Some(paper) = paper { - page.class = paper.class(); - page.size = paper.size(); - } +#![allow(unused)] - if let Some(width) = width { - page.class = PaperClass::Custom; - page.size.x = width.unwrap_or(Length::inf()); - } +use std::fmt::{self, Display, Formatter}; +use std::str::FromStr; - if let Some(height) = height { - page.class = PaperClass::Custom; - page.size.y = height.unwrap_or(Length::inf()); - } +use super::prelude::*; +use super::PadNode; - if flip.unwrap_or(false) { - std::mem::swap(&mut page.size.x, &mut page.size.y); - } +/// `pagebreak`: Start a new page. +pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { + Ok(Value::Node(Node::Pagebreak)) +} - if let Some(margins) = margins { - page.margins = Sides::splat(margins); - } +/// Layouts its child onto one or multiple pages. +#[derive(Clone, PartialEq, Hash)] +pub struct PageNode { + /// The node producing the content. + pub child: PackedNode, + /// The page's styles. + pub styles: Styles, +} - if let Some(left) = left { - page.margins.left = left; - } +#[properties] +impl PageNode { + /// The unflipped width of the page. + pub const WIDTH: Smart<Length> = Smart::Custom(Paper::default().width()); + /// The unflipped height of the page. + pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::default().height()); + /// The class of paper. Defines the default margins. + pub const CLASS: PaperClass = Paper::default().class(); + /// Whether the page is flipped into landscape orientation. + pub const FLIPPED: bool = false; + /// The left margin. + pub const LEFT: Smart<Linear> = Smart::Auto; + /// The right margin. + pub const RIGHT: Smart<Linear> = Smart::Auto; + /// The top margin. + pub const TOP: Smart<Linear> = Smart::Auto; + /// The bottom margin. + pub const BOTTOM: Smart<Linear> = Smart::Auto; + /// The page's background color. + pub const FILL: Option<Paint> = None; +} - if let Some(top) = top { - page.margins.top = top; - } +impl Construct for PageNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + Ok(Node::Page(Self { + child: args.expect::<Node>("body")?.into_block(), + styles: Styles::new(), + })) + } +} - if let Some(right) = right { - page.margins.right = right; +impl Set for PageNode { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) { + styles.set(Self::CLASS, paper.class()); + styles.set(Self::WIDTH, Smart::Custom(paper.width())); + styles.set(Self::HEIGHT, Smart::Custom(paper.height())); } - if let Some(bottom) = bottom { - page.margins.bottom = bottom; + if let Some(width) = args.named("width")? { + styles.set(Self::CLASS, PaperClass::Custom); + styles.set(Self::WIDTH, width); } - if let Some(fill) = fill { - page.fill = fill; + if let Some(height) = args.named("height")? { + styles.set(Self::CLASS, PaperClass::Custom); + styles.set(Self::HEIGHT, height); } - }); - - ctx.template.pagebreak(false); - - Ok(Value::None) -} -/// `pagebreak`: Start a new page. -pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { - let mut template = Template::new(); - template.pagebreak(true); - Ok(Value::Template(template)) -} + let margins = args.named("margins")?; + styles.set_opt(Self::FLIPPED, args.named("flipped")?); + styles.set_opt(Self::LEFT, args.named("left")?.or(margins)); + styles.set_opt(Self::TOP, args.named("top")?.or(margins)); + styles.set_opt(Self::RIGHT, args.named("right")?.or(margins)); + styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(margins)); + styles.set_opt(Self::FILL, args.named("fill")?); -/// Layouts its children onto one or multiple pages. -#[derive(Debug, Hash)] -pub struct PageNode { - /// The size of the page. - pub size: Size, - /// The background fill. - pub fill: Option<Paint>, - /// The node that produces the actual pages. - pub child: PackedNode, + Ok(()) + } } impl PageNode { + /// Style the node with styles from a style map. + pub fn styled(mut self, styles: Styles) -> Self { + self.styles.apply(&styles); + self + } + /// Layout the page run into a sequence of frames, one per page. pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { + let prev = ctx.styles.clone(); + ctx.styles = self.styles.chain(&ctx.styles); + // When one of the lengths is infinite the page fits its content along // that axis. - let expand = self.size.map(Length::is_finite); - let regions = Regions::repeat(self.size, self.size, expand); + let width = ctx.styles.get(Self::WIDTH).unwrap_or(Length::inf()); + let height = ctx.styles.get(Self::HEIGHT).unwrap_or(Length::inf()); + let mut size = Size::new(width, height); + if ctx.styles.get(Self::FLIPPED) { + std::mem::swap(&mut size.x, &mut size.y); + } + + // Determine the margins. + let class = ctx.styles.get(Self::CLASS); + let default = class.default_margins(); + let padding = Sides { + left: ctx.styles.get(Self::LEFT).unwrap_or(default.left), + right: ctx.styles.get(Self::RIGHT).unwrap_or(default.right), + top: ctx.styles.get(Self::TOP).unwrap_or(default.top), + bottom: ctx.styles.get(Self::BOTTOM).unwrap_or(default.bottom), + }; + + // Pad the child. + let padded = PadNode { child: self.child.clone(), padding }.pack(); // Layout the child. + let expand = size.map(Length::is_finite); + let regions = Regions::repeat(size, size, expand); let mut frames: Vec<_> = - self.child.layout(ctx, ®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") +} diff --git a/src/library/par.rs b/src/library/par.rs index 6abfa7af..26280d8e 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -6,75 +6,81 @@ use unicode_bidi::{BidiInfo, Level}; use xi_unicode::LineBreakIterator; use super::prelude::*; -use super::{shape, Decoration, ShapedText, Spacing}; -use crate::style::TextStyle; +use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode}; use crate::util::{EcoString, RangeExt, RcExt, SliceExt}; -/// `par`: Configure paragraphs. -pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let spacing = args.named("spacing")?; - let leading = args.named("leading")?; - - let mut dir = - args.named("lang")? - .map(|iso: EcoString| match iso.to_lowercase().as_str() { - "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, - "en" | "fr" | "de" => Dir::LTR, - _ => Dir::LTR, - }); - - if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? { - if v.axis() != SpecAxis::Horizontal { - bail!(span, "must be horizontal"); - } - dir = Some(v); - } +/// `parbreak`: Start a new paragraph. +pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { + Ok(Value::Node(Node::Parbreak)) +} - let mut align = None; - if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? { - if v.axis() != SpecAxis::Horizontal { - bail!(span, "must be horizontal"); - } - align = Some(v); - } +/// `linebreak`: Start a new line. +pub fn linebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { + Ok(Value::Node(Node::Linebreak)) +} - ctx.template.modify(move |style| { - let par = style.par_mut(); +/// A node that arranges its children into a paragraph. +#[derive(Hash)] +pub struct ParNode(pub Vec<ParChild>); - if let Some(dir) = dir { - par.dir = dir; - par.align = if dir == Dir::LTR { Align::Left } else { Align::Right }; - } +#[properties] +impl ParNode { + /// The direction for text and inline objects. + pub const DIR: Dir = Dir::LTR; + /// How to align text and inline objects in their line. + pub const ALIGN: Align = Align::Left; + /// The spacing between lines (dependent on scaled font size). + pub const LEADING: Linear = Relative::new(0.65).into(); + /// The spacing between paragraphs (dependent on scaled font size). + pub const SPACING: Linear = Relative::new(1.2).into(); +} - if let Some(align) = align { - par.align = align; - } +impl Construct for ParNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + // Lift to a block so that it doesn't merge with adjacent stuff. + Ok(Node::Block(args.expect::<Node>("body")?.into_block())) + } +} - if let Some(leading) = leading { - par.leading = leading; +impl Set for ParNode { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + let spacing = args.named("spacing")?; + let leading = args.named("leading")?; + + let mut dir = + args.named("lang")? + .map(|iso: EcoString| match iso.to_lowercase().as_str() { + "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, + "en" | "fr" | "de" => Dir::LTR, + _ => Dir::LTR, + }); + + if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? { + if v.axis() != SpecAxis::Horizontal { + bail!(span, "must be horizontal"); + } + dir = Some(v); } - if let Some(spacing) = spacing { - par.spacing = spacing; + let mut align = None; + if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? { + if v.axis() != SpecAxis::Horizontal { + bail!(span, "must be horizontal"); + } + align = Some(v); } - }); - ctx.template.parbreak(); + if let (Some(dir), None) = (dir, align) { + align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right }); + } - Ok(Value::None) -} + styles.set_opt(Self::DIR, dir); + styles.set_opt(Self::ALIGN, align); + styles.set_opt(Self::LEADING, leading); + styles.set_opt(Self::SPACING, spacing); -/// A node that arranges its children into a paragraph. -#[derive(Debug, Hash)] -pub struct ParNode { - /// The text direction (either LTR or RTL). - pub dir: Dir, - /// How to align text in its line. - pub align: Align, - /// The spacing to insert between each line. - pub leading: Length, - /// The children to be arranged in a paragraph. - pub children: Vec<ParChild>, + Ok(()) + } } impl Layout for ParNode { @@ -87,7 +93,7 @@ impl Layout for ParNode { let text = self.collect_text(); // Find out the BiDi embedding levels. - let bidi = BidiInfo::new(&text, Level::from_dir(self.dir)); + let bidi = BidiInfo::new(&text, Level::from_dir(ctx.styles.get(Self::DIR))); // Prepare paragraph layout by building a representation on which we can // do line breaking without layouting each and every line from scratch. @@ -123,38 +129,63 @@ impl ParNode { /// The string representation of each child. fn strings(&self) -> impl Iterator<Item = &str> { - self.children.iter().map(|child| match child { + self.0.iter().map(|child| match child { ParChild::Spacing(_) => " ", - ParChild::Text(ref piece, ..) => piece, - ParChild::Node(..) => "\u{FFFC}", - ParChild::Decorate(_) | ParChild::Undecorate => "", + ParChild::Text(ref node) => &node.text, + ParChild::Node(_) => "\u{FFFC}", }) } } +impl Debug for ParNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Par ")?; + f.debug_list().entries(&self.0).finish() + } +} + /// A child of a paragraph node. #[derive(Hash)] pub enum ParChild { /// Spacing between other nodes. - Spacing(Spacing), + Spacing(SpacingNode), /// A run of text and how to align it in its line. - Text(EcoString, Rc<TextStyle>), + Text(TextNode), /// Any child node and how to align it in its line. Node(PackedNode), - /// A decoration that applies until a matching `Undecorate`. - Decorate(Decoration), - /// The end of a decoration. - Undecorate, +} + +impl ParChild { + /// Create a text child. + pub fn text(text: impl Into<EcoString>, styles: Styles) -> Self { + Self::Text(TextNode { text: text.into(), styles }) + } + + /// A reference to the child's styles. + pub fn styles(&self) -> &Styles { + match self { + Self::Spacing(node) => &node.styles, + Self::Text(node) => &node.styles, + Self::Node(node) => &node.styles, + } + } + + /// A mutable reference to the child's styles. + pub fn styles_mut(&mut self) -> &mut Styles { + match self { + Self::Spacing(node) => &mut node.styles, + Self::Text(node) => &mut node.styles, + Self::Node(node) => &mut node.styles, + } + } } impl Debug for ParChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Spacing(v) => write!(f, "Spacing({:?})", v), - Self::Text(text, _) => write!(f, "Text({:?})", text), + Self::Spacing(node) => node.fmt(f), + Self::Text(node) => node.fmt(f), Self::Node(node) => node.fmt(f), - Self::Decorate(deco) => write!(f, "Decorate({:?})", deco), - Self::Undecorate => write!(f, "Undecorate"), } } } @@ -172,8 +203,6 @@ struct ParLayouter<'a> { items: Vec<ParItem<'a>>, /// The ranges of the items in `bidi.text`. ranges: Vec<Range>, - /// The decorations and the ranges they span. - decos: Vec<(Range, &'a Decoration)>, } /// Range of a substring of text. @@ -201,22 +230,22 @@ impl<'a> ParLayouter<'a> { ) -> Self { let mut items = vec![]; let mut ranges = vec![]; - let mut starts = vec![]; - let mut decos = vec![]; // Layout the children and collect them into items. - for (range, child) in par.ranges().zip(&par.children) { - match *child { - ParChild::Spacing(Spacing::Linear(v)) => { - let resolved = v.resolve(regions.current.x); - items.push(ParItem::Absolute(resolved)); - ranges.push(range); - } - ParChild::Spacing(Spacing::Fractional(v)) => { - items.push(ParItem::Fractional(v)); - ranges.push(range); - } - ParChild::Text(_, ref style) => { + for (range, child) in par.ranges().zip(&par.0) { + match child { + ParChild::Spacing(node) => match node.kind { + SpacingKind::Linear(v) => { + let resolved = v.resolve(regions.current.x); + items.push(ParItem::Absolute(resolved)); + ranges.push(range); + } + SpacingKind::Fractional(v) => { + items.push(ParItem::Fractional(v)); + ranges.push(range); + } + }, + ParChild::Text(node) => { // TODO: Also split by language and script. let mut cursor = range.start; for (level, group) in bidi.levels[range].group_by_key(|&lvl| lvl) { @@ -224,41 +253,27 @@ impl<'a> ParLayouter<'a> { cursor += group.len(); let subrange = start .. cursor; let text = &bidi.text[subrange.clone()]; - let shaped = shape(ctx, text, style, level.dir()); + let styles = node.styles.chain(&ctx.styles); + let shaped = shape(&mut ctx.fonts, text, styles, level.dir()); items.push(ParItem::Text(shaped)); ranges.push(subrange); } } - ParChild::Node(ref node) => { + ParChild::Node(node) => { let size = Size::new(regions.current.x, regions.base.y); let pod = Regions::one(size, regions.base, Spec::splat(false)); let frame = node.layout(ctx, &pod).remove(0); items.push(ParItem::Frame(Rc::take(frame.item))); ranges.push(range); } - ParChild::Decorate(ref deco) => { - starts.push((range.start, deco)); - } - ParChild::Undecorate => { - if let Some((start, deco)) = starts.pop() { - decos.push((start .. range.end, deco)); - } - } } } - for (start, deco) in starts { - decos.push((start .. bidi.text.len(), deco)); - } + let em = ctx.styles.get(TextNode::SIZE).abs; + let align = ctx.styles.get(ParNode::ALIGN); + let leading = ctx.styles.get(ParNode::LEADING).resolve(em); - Self { - align: par.align, - leading: par.leading, - bidi, - items, - ranges, - decos, - } + Self { align, leading, bidi, items, ranges } } /// Find first-fit line breaks and build the paragraph. @@ -430,7 +445,7 @@ impl<'a> LineLayout<'a> { // empty string. if !range.is_empty() || rest.is_empty() { // Reshape that part. - let reshaped = shaped.reshape(ctx, range); + let reshaped = shaped.reshape(&mut ctx.fonts, range); last = Some(ParItem::Text(reshaped)); } @@ -451,7 +466,7 @@ impl<'a> LineLayout<'a> { // Reshape if necessary. if range.len() < shaped.text.len() { if !range.is_empty() { - let reshaped = shaped.reshape(ctx, range); + let reshaped = shaped.reshape(&mut ctx.fonts, range); first = Some(ParItem::Text(reshaped)); } @@ -504,28 +519,19 @@ impl<'a> LineLayout<'a> { let mut output = Frame::new(size); output.baseline = Some(self.baseline); - for (range, item) in self.reordered() { - let mut position = |mut frame: Frame| { - // Decorate. - for (deco_range, deco) in &self.par.decos { - if deco_range.contains(&range.start) { - deco.apply(ctx, &mut frame); - } - } - + for item in self.reordered() { + let mut position = |frame: Frame| { let x = offset + self.par.align.resolve(remaining); let y = self.baseline - frame.baseline(); offset += frame.size.x; - - // Add to the line's frame. output.merge_frame(Point::new(x, y), frame); }; - match *item { - ParItem::Absolute(v) => offset += v, + match item { + ParItem::Absolute(v) => offset += *v, ParItem::Fractional(v) => offset += v.resolve(self.fr, remaining), - ParItem::Text(ref shaped) => position(shaped.build()), - ParItem::Frame(ref frame) => position(frame.clone()), + ParItem::Text(shaped) => position(shaped.build(&ctx.fonts)), + ParItem::Frame(frame) => position(frame.clone()), } } @@ -533,7 +539,7 @@ impl<'a> LineLayout<'a> { } /// Iterate through the line's items in visual order. - fn reordered(&self) -> impl Iterator<Item = (Range, &ParItem<'a>)> { + fn reordered(&self) -> impl Iterator<Item = &ParItem<'a>> { // The bidi crate doesn't like empty lines. let (levels, runs) = if !self.line.is_empty() { // Find the paragraph that contains the line. @@ -548,7 +554,7 @@ impl<'a> LineLayout<'a> { // Compute the reordered ranges in visual order (left to right). self.par.bidi.visual_runs(para, self.line.clone()) } else { - <_>::default() + (vec![], vec![]) }; runs.into_iter() @@ -565,7 +571,7 @@ impl<'a> LineLayout<'a> { Either::Right(range.rev()) } }) - .map(move |idx| (self.ranges[idx].clone(), self.get(idx).unwrap())) + .map(move |idx| self.get(idx).unwrap()) } /// Find the index of the item whose range contains the `text_offset`. diff --git a/src/library/placed.rs b/src/library/placed.rs index 722e0035..589a299b 100644 --- a/src/library/placed.rs +++ b/src/library/placed.rs @@ -6,27 +6,22 @@ pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let aligns = args.find().unwrap_or(Spec::new(Some(Align::Left), None)); let tx = args.named("dx")?.unwrap_or_default(); let ty = args.named("dy")?.unwrap_or_default(); - let body: Template = args.expect("body")?; - Ok(Value::Template(Template::from_block(move |style| { - PlacedNode { - child: body.pack(style).moved(Point::new(tx, ty)).aligned(aligns), - } - }))) + let body: Node = args.expect("body")?; + Ok(Value::block(PlacedNode( + body.into_block().moved(Point::new(tx, ty)).aligned(aligns), + ))) } /// A node that places its child absolutely. #[derive(Debug, Hash)] -pub struct PlacedNode { - /// The node to be placed. - pub child: PackedNode, -} +pub struct PlacedNode(pub PackedNode); impl PlacedNode { /// Whether this node wants to be placed relative to its its parent's base /// origin. instead of relative to the parent's current flow/cursor /// position. pub fn out_of_flow(&self) -> bool { - self.child + self.0 .downcast::<AlignNode>() .map_or(false, |node| node.aligns.y.is_some()) } @@ -48,7 +43,7 @@ impl Layout for PlacedNode { Regions::one(regions.base, regions.base, expand) }; - let mut frames = self.child.layout(ctx, &pod); + let mut frames = self.0.layout(ctx, &pod); let Constrained { item: frame, cts } = &mut frames[0]; // If expansion is off, zero all sizes so that we don't take up any @@ -56,13 +51,6 @@ impl Layout for PlacedNode { let target = regions.expand.select(regions.current, Size::zero()); Rc::make_mut(frame).resize(target, Align::LEFT_TOP); - // Place relative to parent's base origin by offsetting our elements by - // the negative cursor position. - if out_of_flow { - let offset = (regions.current - regions.base).to_point(); - Rc::make_mut(frame).translate(offset); - } - // Set base constraint because our pod size is base and exact // constraints if we needed to expand or offset. *cts = Constraints::new(regions.expand); diff --git a/src/library/shape.rs b/src/library/shape.rs index 61c0d6e3..a9c9b333 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -1,6 +1,7 @@ use std::f64::consts::SQRT_2; use super::prelude::*; +use super::LinkNode; /// `rect`: A rectangle with optional content. pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { @@ -76,20 +77,15 @@ fn shape_impl( } // The shape's contents. - let body = args.find::<Template>(); - - Ok(Value::Template(Template::from_inline(move |style| { - ShapeNode { - kind, - fill, - stroke, - child: body - .as_ref() - .map(|body| body.pack(style).padded(Sides::splat(padding))), - } - .pack() - .sized(Spec::new(width, height)) - }))) + let child = args + .find() + .map(|body: Node| body.into_block().padded(Sides::splat(padding))); + + Ok(Value::inline( + ShapeNode { kind, fill, stroke, child } + .pack() + .sized(Spec::new(width, height)), + )) } /// Places its child into a sizable and fillable shape. @@ -154,9 +150,10 @@ impl Layout for ShapeNode { frames = vec![Frame::new(size).constrain(Constraints::tight(regions))]; } + let frame = Rc::make_mut(&mut frames[0].item); + // Add fill and/or stroke. if self.fill.is_some() || self.stroke.is_some() { - let frame = Rc::make_mut(&mut frames[0].item); let geometry = match self.kind { ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size), ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size), @@ -171,6 +168,11 @@ impl Layout for ShapeNode { frame.prepend(Point::zero(), Element::Shape(shape)); } + // Apply link if it exists. + if let Some(url) = ctx.styles.get_ref(LinkNode::URL) { + frame.link(url); + } + frames } } diff --git a/src/library/sized.rs b/src/library/sized.rs index dfdc721d..6d677ca8 100644 --- a/src/library/sized.rs +++ b/src/library/sized.rs @@ -4,18 +4,16 @@ use super::prelude::*; pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let width = args.named("width")?; let height = args.named("height")?; - let body: Template = args.find().unwrap_or_default(); - Ok(Value::Template(Template::from_inline(move |style| { - body.pack(style).sized(Spec::new(width, height)) - }))) + let body: Node = args.find().unwrap_or_default(); + Ok(Value::inline( + body.into_block().sized(Spec::new(width, height)), + )) } /// `block`: Place content into the flow. pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let body: Template = args.find().unwrap_or_default(); - Ok(Value::Template(Template::from_block(move |style| { - body.pack(style) - }))) + let body: Node = args.find().unwrap_or_default(); + Ok(Value::block(body.into_block())) } /// A node that sizes its child. diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 59911dc7..b5ecce69 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -2,21 +2,41 @@ use super::prelude::*; /// `h`: Horizontal spacing. pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let mut template = Template::new(); - template.spacing(SpecAxis::Horizontal, args.expect("spacing")?); - Ok(Value::Template(template)) + Ok(Value::Node(Node::Spacing( + SpecAxis::Horizontal, + args.expect("spacing")?, + ))) } /// `v`: Vertical spacing. pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let mut template = Template::new(); - template.spacing(SpecAxis::Vertical, args.expect("spacing")?); - Ok(Value::Template(template)) + Ok(Value::Node(Node::Spacing( + SpecAxis::Vertical, + args.expect("spacing")?, + ))) +} + +/// Explicit spacing in a flow or paragraph. +#[derive(Hash)] +pub struct SpacingNode { + /// The kind of spacing. + pub kind: SpacingKind, + /// The spacing's styles. + pub styles: Styles, +} + +impl Debug for SpacingNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } + write!(f, "{:?}", self.kind) + } } /// Kinds of spacing. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Spacing { +pub enum SpacingKind { /// A length stated in absolute values and/or relative to the parent's size. Linear(Linear), /// A length that is the fraction of the remaining free space in the parent. @@ -24,7 +44,7 @@ pub enum Spacing { } castable! { - Spacing, + SpacingKind, Expected: "linear or fractional", Value::Length(v) => Self::Linear(v.into()), Value::Relative(v) => Self::Linear(v.into()), diff --git a/src/library/stack.rs b/src/library/stack.rs index 2b1371ab..8a1f0fd5 100644 --- a/src/library/stack.rs +++ b/src/library/stack.rs @@ -1,54 +1,31 @@ use std::fmt::{self, Debug, Formatter}; use super::prelude::*; -use super::{AlignNode, Spacing}; +use super::{AlignNode, SpacingKind, SpacingNode}; /// `stack`: Stack children along an axis. pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - enum Child { - Spacing(Spacing), - Any(Template), - } - - castable! { - Child, - Expected: "linear, fractional or template", - Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())), - Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())), - Value::Linear(v) => Self::Spacing(Spacing::Linear(v)), - Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)), - Value::Template(v) => Self::Any(v), - } - let dir = args.named("dir")?.unwrap_or(Dir::TTB); let spacing = args.named("spacing")?; - let list: Vec<Child> = args.all().collect(); - Ok(Value::Template(Template::from_block(move |style| { - let mut children = vec![]; - let mut delayed = None; + let mut children = vec![]; + let mut deferred = None; - // Build the list of stack children. - for child in &list { - match child { - Child::Spacing(v) => { - children.push(StackChild::Spacing(*v)); - delayed = None; - } - Child::Any(child) => { - if let Some(v) = delayed { - children.push(StackChild::Spacing(v)); - } - - let node = child.pack(style); - children.push(StackChild::Node(node)); - delayed = spacing; + // Build the list of stack children. + for child in args.all() { + match child { + StackChild::Spacing(_) => deferred = None, + StackChild::Node(_) => { + if let Some(v) = deferred { + children.push(StackChild::spacing(v)); } + deferred = spacing; } } + children.push(child); + } - StackNode { dir, children } - }))) + Ok(Value::block(StackNode { dir, children })) } /// A node that stacks its children. @@ -74,20 +51,37 @@ impl Layout for StackNode { #[derive(Hash)] pub enum StackChild { /// Spacing between other nodes. - Spacing(Spacing), + Spacing(SpacingNode), /// An arbitrary node. Node(PackedNode), } +impl StackChild { + /// Create a spacing node from a spacing kind. + pub fn spacing(kind: SpacingKind) -> Self { + Self::Spacing(SpacingNode { kind, styles: Styles::new() }) + } +} + impl Debug for StackChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Spacing(spacing) => spacing.fmt(f), + Self::Spacing(node) => node.fmt(f), Self::Node(node) => node.fmt(f), } } } +castable! { + StackChild, + Expected: "linear, fractional or template", + Value::Length(v) => Self::spacing(SpacingKind::Linear(v.into())), + Value::Relative(v) => Self::spacing(SpacingKind::Linear(v.into())), + Value::Linear(v) => Self::spacing(SpacingKind::Linear(v)), + Value::Fractional(v) => Self::spacing(SpacingKind::Fractional(v)), + Value::Node(v) => Self::Node(v.into_block()), +} + /// Performs stack layout. struct StackLayouter<'a> { /// The stack node to layout. @@ -148,15 +142,15 @@ impl<'a> StackLayouter<'a> { /// Layout all children. fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> { for child in &self.stack.children { - match *child { - StackChild::Spacing(Spacing::Linear(v)) => { - self.layout_absolute(v); - } - StackChild::Spacing(Spacing::Fractional(v)) => { - self.items.push(StackItem::Fractional(v)); - self.fr += v; - } - StackChild::Node(ref node) => { + match child { + StackChild::Spacing(node) => match node.kind { + SpacingKind::Linear(v) => self.layout_absolute(v), + SpacingKind::Fractional(v) => { + self.items.push(StackItem::Fractional(v)); + self.fr += v; + } + }, + StackChild::Node(node) => { if self.regions.is_full() { self.finish_region(); } diff --git a/src/library/text.rs b/src/library/text.rs index 0790196d..99c68f79 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -1,426 +1,488 @@ use std::borrow::Cow; use std::convert::TryInto; -use std::ops::Range; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{BitXor, Range}; use rustybuzz::{Feature, UnicodeBuffer}; use ttf_parser::Tag; use super::prelude::*; +use super::LinkNode; use crate::font::{ Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight, VerticalFontMetric, }; use crate::geom::{Dir, Em, Length, Point, Size}; -use crate::style::{ - FontFamily, FontFeatures, NumberPosition, NumberType, NumberWidth, Style, - StylisticSet, TextStyle, -}; use crate::util::{EcoString, SliceExt}; -/// `font`: Configure the font. -pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - castable! { - Vec<FontFamily>, - Expected: "string, generic family or array thereof", - Value::Str(string) => vec![FontFamily::Named(string.to_lowercase())], - Value::Array(values) => { - values.into_iter().filter_map(|v| v.cast().ok()).collect() - }, - @family: FontFamily => vec![family.clone()], - } - - castable! { - Vec<EcoString>, - Expected: "string or array of strings", - Value::Str(string) => vec![string.to_lowercase()], - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| string.to_lowercase()) - .collect(), - } - - castable! { - FontStyle, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "italic" => Self::Italic, - "oblique" => Self::Oblique, - _ => Err(r#"expected "normal", "italic" or "oblique""#)?, - }, - } - - castable! { - FontWeight, - Expected: "integer or string", - Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number), - Value::Str(string) => match string.as_str() { - "thin" => Self::THIN, - "extralight" => Self::EXTRALIGHT, - "light" => Self::LIGHT, - "regular" => Self::REGULAR, - "medium" => Self::MEDIUM, - "semibold" => Self::SEMIBOLD, - "bold" => Self::BOLD, - "extrabold" => Self::EXTRABOLD, - "black" => Self::BLACK, - _ => Err("unknown font weight")?, - }, - } - - castable! { - FontStretch, - Expected: "relative", - Value::Relative(v) => Self::from_ratio(v.get() as f32), - } - - castable! { - VerticalFontMetric, - Expected: "linear or string", - Value::Length(v) => Self::Linear(v.into()), - Value::Relative(v) => Self::Linear(v.into()), - Value::Linear(v) => Self::Linear(v), - Value::Str(string) => match string.as_str() { - "ascender" => Self::Ascender, - "cap-height" => Self::CapHeight, - "x-height" => Self::XHeight, - "baseline" => Self::Baseline, - "descender" => Self::Descender, - _ => Err("unknown font metric")?, - }, - } - - castable! { - StylisticSet, - Expected: "integer", - Value::Int(v) => match v { - 1 ..= 20 => Self::new(v as u8), - _ => Err("must be between 1 and 20")?, - }, - } - - castable! { - NumberType, - Expected: "string", - Value::Str(string) => match string.as_str() { - "lining" => Self::Lining, - "old-style" => Self::OldStyle, - _ => Err(r#"expected "lining" or "old-style""#)?, - }, - } - - castable! { - NumberWidth, - Expected: "string", - Value::Str(string) => match string.as_str() { - "proportional" => Self::Proportional, - "tabular" => Self::Tabular, - _ => Err(r#"expected "proportional" or "tabular""#)?, - }, - } - - castable! { - NumberPosition, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "subscript" => Self::Subscript, - "superscript" => Self::Superscript, - _ => Err(r#"expected "normal", "subscript" or "superscript""#)?, - }, - } - - castable! { - Vec<(Tag, u32)>, - Expected: "array of strings or dictionary mapping tags to integers", - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) - .collect(), - Value::Dict(values) => values - .into_iter() - .filter_map(|(k, v)| { - let tag = Tag::from_bytes_lossy(k.as_bytes()); - let num = v.cast::<i64>().ok()?.try_into().ok()?; - Some((tag, num)) - }) - .collect(), +/// `strike`: Typeset striken-through text. +pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { + line_impl(args, LineKind::Strikethrough) +} + +/// `underline`: Typeset underlined text. +pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { + line_impl(args, LineKind::Underline) +} + +/// `overline`: Typeset text with an overline. +pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { + line_impl(args, LineKind::Overline) +} + +fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> { + let stroke = args.named("stroke")?.or_else(|| args.find()); + let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find()); + let offset = args.named("offset")?; + let extent = args.named("extent")?.unwrap_or_default(); + let body: Node = args.expect("body")?; + let deco = LineDecoration { kind, stroke, thickness, offset, extent }; + Ok(Value::Node( + body.styled(Styles::one(TextNode::LINES, vec![deco])), + )) +} + +/// A single run of text with the same style. +#[derive(Hash)] +pub struct TextNode { + /// The run's text. + pub text: EcoString, + /// The run's styles. + pub styles: Styles, +} + +#[properties] +impl TextNode { + /// A prioritized sequence of font families. + pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif]; + /// The serif font family/families. + pub const SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")]; + /// The sans-serif font family/families. + pub const SANS_SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")]; + /// The monospace font family/families. + pub const MONOSPACE_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Mono")]; + /// Whether to allow font fallback when the primary font list contains no + /// match. + pub const FALLBACK: bool = true; + + /// How the font is styled. + pub const STYLE: FontStyle = FontStyle::Normal; + /// The boldness / thickness of the font's glyphs. + pub const WEIGHT: FontWeight = FontWeight::REGULAR; + /// The width of the glyphs. + pub const STRETCH: FontStretch = FontStretch::NORMAL; + /// Whether the font weight should be increased by 300. + #[fold(bool::bitxor)] + pub const STRONG: bool = false; + /// Whether the the font style should be inverted. + #[fold(bool::bitxor)] + pub const EMPH: bool = false; + /// Whether a monospace font should be preferred. + pub const MONOSPACE: bool = false; + /// The glyph fill color. + pub const FILL: Paint = RgbaColor::BLACK.into(); + /// Decorative lines. + #[fold(|a, b| a.into_iter().chain(b).collect())] + pub const LINES: Vec<LineDecoration> = vec![]; + + /// The size of the glyphs. + #[fold(Linear::compose)] + pub const SIZE: Linear = Length::pt(11.0).into(); + /// The amount of space that should be added between characters. + pub const TRACKING: Em = Em::zero(); + /// The top end of the text bounding box. + pub const TOP_EDGE: VerticalFontMetric = VerticalFontMetric::CapHeight; + /// The bottom end of the text bounding box. + pub const BOTTOM_EDGE: VerticalFontMetric = VerticalFontMetric::Baseline; + + /// Whether to apply kerning ("kern"). + pub const KERNING: bool = true; + /// Whether small capital glyphs should be used. ("smcp") + pub const SMALLCAPS: bool = false; + /// Whether to apply stylistic alternates. ("salt") + pub const ALTERNATES: bool = false; + /// Which stylistic set to apply. ("ss01" - "ss20") + pub const STYLISTIC_SET: Option<StylisticSet> = None; + /// Whether standard ligatures are active. ("liga", "clig") + pub const LIGATURES: bool = true; + /// Whether ligatures that should be used sparingly are active. ("dlig") + pub const DISCRETIONARY_LIGATURES: bool = false; + /// Whether historical ligatures are active. ("hlig") + pub const HISTORICAL_LIGATURES: bool = false; + /// Which kind of numbers / figures to select. + pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto; + /// The width of numbers / figures. + pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto; + /// How to position numbers. + pub const NUMBER_POSITION: NumberPosition = NumberPosition::Normal; + /// Whether to have a slash through the zero glyph. ("zero") + pub const SLASHED_ZERO: bool = false; + /// Whether to convert fractions. ("frac") + pub const FRACTIONS: bool = false; + /// Raw OpenType features to apply. + pub const FEATURES: Vec<(Tag, u32)> = vec![]; +} + +impl Construct for TextNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + // We don't need to do anything more here because the whole point of the + // text constructor is to apply the styles and that happens + // automatically during class construction. + args.expect::<Node>("body") } +} - let list = args.named("family")?.or_else(|| { - let families: Vec<_> = args.all().collect(); - (!families.is_empty()).then(|| families) - }); +impl Set for TextNode { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + let list = args.named("family")?.or_else(|| { + let families: Vec<_> = args.all().collect(); + (!families.is_empty()).then(|| families) + }); + + styles.set_opt(Self::FAMILY_LIST, list); + styles.set_opt(Self::SERIF_LIST, args.named("serif")?); + styles.set_opt(Self::SANS_SERIF_LIST, args.named("sans-serif")?); + styles.set_opt(Self::MONOSPACE_LIST, args.named("monospace")?); + styles.set_opt(Self::FALLBACK, args.named("fallback")?); + styles.set_opt(Self::STYLE, args.named("style")?); + styles.set_opt(Self::WEIGHT, args.named("weight")?); + styles.set_opt(Self::STRETCH, args.named("stretch")?); + styles.set_opt(Self::FILL, args.named("fill")?.or_else(|| args.find())); + styles.set_opt(Self::SIZE, args.named("size")?.or_else(|| args.find())); + styles.set_opt(Self::TRACKING, args.named("tracking")?.map(Em::new)); + styles.set_opt(Self::TOP_EDGE, args.named("top-edge")?); + styles.set_opt(Self::BOTTOM_EDGE, args.named("bottom-edge")?); + styles.set_opt(Self::KERNING, args.named("kerning")?); + styles.set_opt(Self::SMALLCAPS, args.named("smallcaps")?); + styles.set_opt(Self::ALTERNATES, args.named("alternates")?); + styles.set_opt(Self::STYLISTIC_SET, args.named("stylistic-set")?); + styles.set_opt(Self::LIGATURES, args.named("ligatures")?); + styles.set_opt( + Self::DISCRETIONARY_LIGATURES, + args.named("discretionary-ligatures")?, + ); + styles.set_opt( + Self::HISTORICAL_LIGATURES, + args.named("historical-ligatures")?, + ); + styles.set_opt(Self::NUMBER_TYPE, args.named("number-type")?); + styles.set_opt(Self::NUMBER_WIDTH, args.named("number-width")?); + styles.set_opt(Self::NUMBER_POSITION, args.named("number-position")?); + styles.set_opt(Self::SLASHED_ZERO, args.named("slashed-zero")?); + styles.set_opt(Self::FRACTIONS, args.named("fractions")?); + styles.set_opt(Self::FEATURES, args.named("features")?); - let serif = args.named("serif")?; - let sans_serif = args.named("sans-serif")?; - let monospace = args.named("monospace")?; - let fallback = args.named("fallback")?; - let style = args.named("style")?; - let weight = args.named("weight")?; - let stretch = args.named("stretch")?; - let size = args.named::<Linear>("size")?.or_else(|| args.find()); - let tracking = args.named("tracking")?.map(Em::new); - let top_edge = args.named("top-edge")?; - let bottom_edge = args.named("bottom-edge")?; - let fill = args.named("fill")?.or_else(|| args.find()); - let kerning = args.named("kerning")?; - let smallcaps = args.named("smallcaps")?; - let alternates = args.named("alternates")?; - let stylistic_set = args.named("stylistic-set")?; - let ligatures = args.named("ligatures")?; - let discretionary_ligatures = args.named("discretionary-ligatures")?; - let historical_ligatures = args.named("historical-ligatures")?; - let number_type = args.named("number-type")?; - let number_width = args.named("number-width")?; - let number_position = args.named("number-position")?; - let slashed_zero = args.named("slashed-zero")?; - let fractions = args.named("fractions")?; - let features = args.named("features")?; - let body = args.find::<Template>(); - - macro_rules! set { - ($target:expr => $source:expr) => { - if let Some(v) = $source { - $target = v; - } - }; + Ok(()) } +} - let f = move |style_: &mut Style| { - let text = style_.text_mut(); - set!(text.families_mut().list => list.clone()); - set!(text.families_mut().serif => serif.clone()); - set!(text.families_mut().sans_serif => sans_serif.clone()); - set!(text.families_mut().monospace => monospace.clone()); - set!(text.fallback => fallback); - set!(text.variant.style => style); - set!(text.variant.weight => weight); - set!(text.variant.stretch => stretch); - set!(text.size => size.map(|v| v.resolve(text.size))); - set!(text.tracking => tracking); - set!(text.top_edge => top_edge); - set!(text.bottom_edge => bottom_edge); - set!(text.fill => fill); - set!(text.features_mut().kerning => kerning); - set!(text.features_mut().smallcaps => smallcaps); - set!(text.features_mut().alternates => alternates); - set!(text.features_mut().stylistic_set => stylistic_set); - set!(text.features_mut().ligatures.standard => ligatures); - set!(text.features_mut().ligatures.discretionary => discretionary_ligatures); - set!(text.features_mut().ligatures.historical => historical_ligatures); - set!(text.features_mut().numbers.type_ => number_type); - set!(text.features_mut().numbers.width => number_width); - set!(text.features_mut().numbers.position => number_position); - set!(text.features_mut().numbers.slashed_zero => slashed_zero); - set!(text.features_mut().numbers.fractions => fractions); - set!(text.features_mut().raw => features.clone()); - }; +impl Debug for TextNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } + write!(f, "Text({:?})", self.text) + } +} - Ok(if let Some(body) = body { - Value::Template(body.modified(f)) - } else { - ctx.template.modify(f); - Value::None - }) +/// A generic or named font family. +#[derive(Clone, Eq, PartialEq, Hash)] +pub enum FontFamily { + /// A family that has "serifs", small strokes attached to letters. + Serif, + /// A family in which glyphs do not have "serifs", small attached strokes. + SansSerif, + /// A family in which (almost) all glyphs are of equal width. + Monospace, + /// A specific font family like "Arial". + Named(NamedFamily), } -/// Shape text into [`ShapedText`]. -pub fn shape<'a>( - ctx: &mut LayoutContext, - text: &'a str, - style: &'a TextStyle, - dir: Dir, -) -> ShapedText<'a> { - let mut glyphs = vec![]; - if !text.is_empty() { - shape_segment( - ctx.fonts, - &mut glyphs, - 0, - text, - style.variant(), - style.families(), - None, - dir, - &tags(&style.features), - ); +impl FontFamily { + /// Create a named font family variant, directly from a string. + pub fn named(string: &str) -> Self { + Self::Named(NamedFamily::new(string)) } +} - track(&mut glyphs, style.tracking); +impl Debug for FontFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Serif => f.pad("serif"), + Self::SansSerif => f.pad("sans-serif"), + Self::Monospace => f.pad("monospace"), + Self::Named(s) => s.fmt(f), + } + } +} - let (size, baseline) = measure(ctx, &glyphs, style); - ShapedText { - text, - dir, - style, - size, - baseline, - glyphs: Cow::Owned(glyphs), +/// A specific font family like "Arial". +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct NamedFamily(String); + +impl NamedFamily { + /// Create a named font family variant. + pub fn new(string: &str) -> Self { + Self(string.to_lowercase()) + } + + /// The lowercased family name. + pub fn as_str(&self) -> &str { + &self.0 } } -/// The result of shaping text. -/// -/// This type contains owned or borrowed shaped text runs, which can be -/// measured, used to reshape substrings more quickly and converted into a -/// frame. -#[derive(Debug, Clone)] -pub struct ShapedText<'a> { - /// The text that was shaped. - pub text: &'a str, - /// The text direction. - pub dir: Dir, - /// The properties used for font selection. - pub style: &'a TextStyle, - /// The font size. - pub size: Size, - /// The baseline from the top of the frame. - pub baseline: Length, - /// The shaped glyphs. - pub glyphs: Cow<'a, [ShapedGlyph]>, +impl Debug for NamedFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } } -/// A single glyph resulting from shaping. -#[derive(Debug, Copy, Clone)] -pub struct ShapedGlyph { - /// The font face the glyph is contained in. - pub face_id: FaceId, - /// The glyph's index in the face. - pub glyph_id: u16, - /// The advance width of the glyph. - pub x_advance: Em, - /// The horizontal offset of the glyph. - pub x_offset: Em, - /// The start index of the glyph in the source text. - pub text_index: usize, - /// Whether splitting the shaping result before this glyph would yield the - /// same results as shaping the parts to both sides of `text_index` - /// separately. - pub safe_to_break: bool, +dynamic! { + FontFamily: "font family", + Value::Str(string) => Self::named(&string), } -impl<'a> ShapedText<'a> { - /// Build the shaped text's frame. - pub fn build(&self) -> Frame { - let mut offset = Length::zero(); - let mut frame = Frame::new(self.size); - frame.baseline = Some(self.baseline); +castable! { + Vec<FontFamily>, + Expected: "string, generic family or array thereof", + Value::Str(string) => vec![FontFamily::named(&string)], + Value::Array(values) => { + values.into_iter().filter_map(|v| v.cast().ok()).collect() + }, + @family: FontFamily => vec![family.clone()], +} - for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { - let pos = Point::new(offset, self.baseline); +castable! { + Vec<NamedFamily>, + Expected: "string or array of strings", + Value::Str(string) => vec![NamedFamily::new(&string)], + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: EcoString| NamedFamily::new(&string)) + .collect(), +} - let mut text = Text { - face_id, - size: self.style.size, - width: Length::zero(), - fill: self.style.fill, - glyphs: vec![], - }; +castable! { + FontStyle, + Expected: "string", + Value::Str(string) => match string.as_str() { + "normal" => Self::Normal, + "italic" => Self::Italic, + "oblique" => Self::Oblique, + _ => Err(r#"expected "normal", "italic" or "oblique""#)?, + }, +} - for glyph in group { - text.glyphs.push(Glyph { - id: glyph.glyph_id, - x_advance: glyph.x_advance, - x_offset: glyph.x_offset, - }); - text.width += glyph.x_advance.to_length(text.size); - } +castable! { + FontWeight, + Expected: "integer or string", + Value::Int(v) => Value::Int(v) + .cast::<usize>()? + .try_into() + .map_or(Self::BLACK, Self::from_number), + Value::Str(string) => match string.as_str() { + "thin" => Self::THIN, + "extralight" => Self::EXTRALIGHT, + "light" => Self::LIGHT, + "regular" => Self::REGULAR, + "medium" => Self::MEDIUM, + "semibold" => Self::SEMIBOLD, + "bold" => Self::BOLD, + "extrabold" => Self::EXTRABOLD, + "black" => Self::BLACK, + _ => Err("unknown font weight")?, + }, +} - offset += text.width; - frame.push(pos, Element::Text(text)); - } +castable! { + FontStretch, + Expected: "relative", + Value::Relative(v) => Self::from_ratio(v.get() as f32), +} - frame +castable! { + VerticalFontMetric, + Expected: "linear or string", + Value::Length(v) => Self::Linear(v.into()), + Value::Relative(v) => Self::Linear(v.into()), + Value::Linear(v) => Self::Linear(v), + Value::Str(string) => match string.as_str() { + "ascender" => Self::Ascender, + "cap-height" => Self::CapHeight, + "x-height" => Self::XHeight, + "baseline" => Self::Baseline, + "descender" => Self::Descender, + _ => Err("unknown font metric")?, + }, +} + +/// A stylistic set in a font face. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct StylisticSet(u8); + +impl StylisticSet { + /// Creates a new set, clamping to 1-20. + pub fn new(index: u8) -> Self { + Self(index.clamp(1, 20)) } - /// Reshape a range of the shaped text, reusing information from this - /// shaping process if possible. - pub fn reshape( - &'a self, - ctx: &mut LayoutContext, - text_range: Range<usize>, - ) -> ShapedText<'a> { - if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { - let (size, baseline) = measure(ctx, glyphs, self.style); - Self { - text: &self.text[text_range], - dir: self.dir, - style: self.style, - size, - baseline, - glyphs: Cow::Borrowed(glyphs), - } - } else { - shape(ctx, &self.text[text_range], self.style, self.dir) - } + /// Get the value, guaranteed to be 1-20. + pub fn get(self) -> u8 { + self.0 } +} - /// Find the subslice of glyphs that represent the given text range if both - /// sides are safe to break. - fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { - let Range { mut start, mut end } = text_range; - if !self.dir.is_positive() { - std::mem::swap(&mut start, &mut end); - } +castable! { + StylisticSet, + Expected: "integer", + Value::Int(v) => match v { + 1 ..= 20 => Self::new(v as u8), + _ => Err("must be between 1 and 20")?, + }, +} - let left = self.find_safe_to_break(start, Side::Left)?; - let right = self.find_safe_to_break(end, Side::Right)?; - Some(&self.glyphs[left .. right]) - } +/// Which kind of numbers / figures to select. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberType { + /// Numbers that fit well with capital text. ("lnum") + Lining, + /// Numbers that fit well into flow of upper- and lowercase text. ("onum") + OldStyle, +} - /// Find the glyph offset matching the text index that is most towards the - /// given side and safe-to-break. - fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { - let ltr = self.dir.is_positive(); +castable! { + NumberType, + Expected: "string", + Value::Str(string) => match string.as_str() { + "lining" => Self::Lining, + "old-style" => Self::OldStyle, + _ => Err(r#"expected "lining" or "old-style""#)?, + }, +} - // Handle edge cases. - let len = self.glyphs.len(); - if text_index == 0 { - return Some(if ltr { 0 } else { len }); - } else if text_index == self.text.len() { - return Some(if ltr { len } else { 0 }); - } +/// The width of numbers / figures. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberWidth { + /// Number widths are glyph specific. ("pnum") + Proportional, + /// All numbers are of equal width / monospaced. ("tnum") + Tabular, +} - // Find any glyph with the text index. - let mut idx = self - .glyphs - .binary_search_by(|g| { - let ordering = g.text_index.cmp(&text_index); - if ltr { ordering } else { ordering.reverse() } - }) - .ok()?; +castable! { + NumberWidth, + Expected: "string", + Value::Str(string) => match string.as_str() { + "proportional" => Self::Proportional, + "tabular" => Self::Tabular, + _ => Err(r#"expected "proportional" or "tabular""#)?, + }, +} - let next = match towards { - Side::Left => usize::checked_sub, - Side::Right => usize::checked_add, - }; +/// How to position numbers. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberPosition { + /// Numbers are positioned on the same baseline as text. + Normal, + /// Numbers are smaller and placed at the bottom. ("subs") + Subscript, + /// Numbers are smaller and placed at the top. ("sups") + Superscript, +} - // Search for the outermost glyph with the text index. - while let Some(next) = next(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) { - break; - } - idx = next; - } +castable! { + NumberPosition, + Expected: "string", + Value::Str(string) => match string.as_str() { + "normal" => Self::Normal, + "subscript" => Self::Subscript, + "superscript" => Self::Superscript, + _ => Err(r#"expected "normal", "subscript" or "superscript""#)?, + }, +} - // RTL needs offset one because the left side of the range should be - // exclusive and the right side inclusive, contrary to the normal - // behaviour of ranges. - if !ltr { - idx += 1; - } +castable! { + Vec<(Tag, u32)>, + Expected: "array of strings or dictionary mapping tags to integers", + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) + .collect(), + Value::Dict(values) => values + .into_iter() + .filter_map(|(k, v)| { + let tag = Tag::from_bytes_lossy(k.as_bytes()); + let num = v.cast::<i64>().ok()?.try_into().ok()?; + Some((tag, num)) + }) + .collect(), +} - self.glyphs[idx].safe_to_break.then(|| idx) - } +/// Defines a line that is positioned over, under or on top of text. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct LineDecoration { + /// The kind of line. + pub kind: LineKind, + /// Stroke color of the line, defaults to the text color if `None`. + pub stroke: Option<Paint>, + /// Thickness of the line's strokes (dependent on scaled font size), read + /// from the font tables if `None`. + pub thickness: Option<Linear>, + /// Position of the line relative to the baseline (dependent on scaled font + /// size), read from the font tables if `None`. + pub offset: Option<Linear>, + /// Amount that the line will be longer or shorter than its associated text + /// (dependent on scaled font size). + pub extent: Linear, } -/// A visual side. -enum Side { - Left, - Right, +/// The kind of line decoration. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum LineKind { + /// A line under text. + Underline, + /// A line through text. + Strikethrough, + /// A line over text. + Overline, +} + +/// Shape text into [`ShapedText`]. +pub fn shape<'a>( + fonts: &mut FontStore, + text: &'a str, + styles: Styles, + dir: Dir, +) -> ShapedText<'a> { + let mut glyphs = vec![]; + if !text.is_empty() { + shape_segment( + fonts, + &mut glyphs, + 0, + text, + variant(&styles), + families(&styles), + None, + dir, + &tags(&styles), + ); + } + + track(&mut glyphs, styles.get(TextNode::TRACKING)); + let (size, baseline) = measure(fonts, &glyphs, &styles); + + ShapedText { + text, + dir, + styles, + size, + baseline, + glyphs: Cow::Owned(glyphs), + } } /// Shape text with font fallback using the `families` iterator. @@ -568,36 +630,40 @@ fn track(glyphs: &mut [ShapedGlyph], tracking: Em) { /// Measure the size and baseline of a run of shaped glyphs with the given /// properties. fn measure( - ctx: &mut LayoutContext, + fonts: &mut FontStore, glyphs: &[ShapedGlyph], - style: &TextStyle, + styles: &Styles, ) -> (Size, Length) { let mut width = Length::zero(); let mut top = Length::zero(); let mut bottom = Length::zero(); + let size = styles.get(TextNode::SIZE).abs; + let top_edge = styles.get(TextNode::TOP_EDGE); + let bottom_edge = styles.get(TextNode::BOTTOM_EDGE); + // Expand top and bottom by reading the face's vertical metrics. let mut expand = |face: &Face| { - top.set_max(face.vertical_metric(style.top_edge, style.size)); - bottom.set_max(-face.vertical_metric(style.bottom_edge, style.size)); + top.set_max(face.vertical_metric(top_edge, size)); + bottom.set_max(-face.vertical_metric(bottom_edge, size)); }; if glyphs.is_empty() { // When there are no glyphs, we just use the vertical metrics of the // first available font. - for family in style.families() { - if let Some(face_id) = ctx.fonts.select(family, style.variant) { - expand(ctx.fonts.get(face_id)); + for family in families(styles) { + if let Some(face_id) = fonts.select(family, variant(styles)) { + expand(fonts.get(face_id)); break; } } } else { for (face_id, group) in glyphs.group_by_key(|g| g.face_id) { - let face = ctx.fonts.get(face_id); + let face = fonts.get(face_id); expand(face); for glyph in group { - width += glyph.x_advance.to_length(style.size); + width += glyph.x_advance.resolve(size); } } } @@ -605,75 +671,320 @@ fn measure( (Size::new(width, top + bottom), top) } +/// Resolve the font variant with `STRONG` and `EMPH` factored in. +fn variant(styles: &Styles) -> FontVariant { + let mut variant = FontVariant::new( + styles.get(TextNode::STYLE), + styles.get(TextNode::WEIGHT), + styles.get(TextNode::STRETCH), + ); + + if styles.get(TextNode::STRONG) { + variant.weight = variant.weight.thicken(300); + } + + if styles.get(TextNode::EMPH) { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + variant +} + +/// Resolve a prioritized iterator over the font families. +fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone { + let head = if styles.get(TextNode::MONOSPACE) { + styles.get_ref(TextNode::MONOSPACE_LIST).as_slice() + } else { + &[] + }; + + let core = styles.get_ref(TextNode::FAMILY_LIST).iter().flat_map(move |family| { + match family { + FontFamily::Named(name) => std::slice::from_ref(name), + FontFamily::Serif => styles.get_ref(TextNode::SERIF_LIST), + FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF_LIST), + FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE_LIST), + } + }); + + let tail: &[&str] = if styles.get(TextNode::FALLBACK) { + &["ibm plex sans", "latin modern math", "twitter color emoji"] + } else { + &[] + }; + + head.iter() + .chain(core) + .map(|named| named.as_str()) + .chain(tail.iter().copied()) +} + /// Collect the tags of the OpenType features to apply. -fn tags(features: &FontFeatures) -> Vec<Feature> { +fn tags(styles: &Styles) -> Vec<Feature> { let mut tags = vec![]; let mut feat = |tag, value| { tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); }; // Features that are on by default in Harfbuzz are only added if disabled. - if !features.kerning { + if !styles.get(TextNode::KERNING) { feat(b"kern", 0); } // Features that are off by default in Harfbuzz are only added if enabled. - if features.smallcaps { + if styles.get(TextNode::SMALLCAPS) { feat(b"smcp", 1); } - if features.alternates { + if styles.get(TextNode::ALTERNATES) { feat(b"salt", 1); } let storage; - if let Some(set) = features.stylistic_set { + if let Some(set) = styles.get(TextNode::STYLISTIC_SET) { storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; feat(&storage, 1); } - if !features.ligatures.standard { + if !styles.get(TextNode::LIGATURES) { feat(b"liga", 0); feat(b"clig", 0); } - if features.ligatures.discretionary { + if styles.get(TextNode::DISCRETIONARY_LIGATURES) { feat(b"dlig", 1); } - if features.ligatures.historical { + if styles.get(TextNode::HISTORICAL_LIGATURES) { feat(b"hilg", 1); } - match features.numbers.type_ { + match styles.get(TextNode::NUMBER_TYPE) { Smart::Auto => {} Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), } - match features.numbers.width { + match styles.get(TextNode::NUMBER_WIDTH) { Smart::Auto => {} Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), } - match features.numbers.position { + match styles.get(TextNode::NUMBER_POSITION) { NumberPosition::Normal => {} NumberPosition::Subscript => feat(b"subs", 1), NumberPosition::Superscript => feat(b"sups", 1), } - if features.numbers.slashed_zero { + if styles.get(TextNode::SLASHED_ZERO) { feat(b"zero", 1); } - if features.numbers.fractions { + if styles.get(TextNode::FRACTIONS) { feat(b"frac", 1); } - for &(tag, value) in features.raw.iter() { + for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() { tags.push(Feature::new(tag, value, ..)) } tags } + +/// The result of shaping text. +/// +/// This type contains owned or borrowed shaped text runs, which can be +/// measured, used to reshape substrings more quickly and converted into a +/// frame. +#[derive(Debug, Clone)] +pub struct ShapedText<'a> { + /// The text that was shaped. + pub text: &'a str, + /// The text direction. + pub dir: Dir, + /// The text's style properties. + // TODO(style): Go back to reference. + pub styles: Styles, + /// The font size. + pub size: Size, + /// The baseline from the top of the frame. + pub baseline: Length, + /// The shaped glyphs. + pub glyphs: Cow<'a, [ShapedGlyph]>, +} + +/// A single glyph resulting from shaping. +#[derive(Debug, Copy, Clone)] +pub struct ShapedGlyph { + /// The font face the glyph is contained in. + pub face_id: FaceId, + /// The glyph's index in the face. + pub glyph_id: u16, + /// The advance width of the glyph. + pub x_advance: Em, + /// The horizontal offset of the glyph. + pub x_offset: Em, + /// The start index of the glyph in the source text. + pub text_index: usize, + /// Whether splitting the shaping result before this glyph would yield the + /// same results as shaping the parts to both sides of `text_index` + /// separately. + pub safe_to_break: bool, +} + +impl<'a> ShapedText<'a> { + /// Build the shaped text's frame. + pub fn build(&self, fonts: &FontStore) -> Frame { + let mut offset = Length::zero(); + let mut frame = Frame::new(self.size); + frame.baseline = Some(self.baseline); + + for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { + let pos = Point::new(offset, self.baseline); + + let size = self.styles.get(TextNode::SIZE).abs; + let fill = self.styles.get(TextNode::FILL); + let glyphs = group + .iter() + .map(|glyph| Glyph { + id: glyph.glyph_id, + x_advance: glyph.x_advance, + x_offset: glyph.x_offset, + }) + .collect(); + + let text = Text { face_id, size, fill, glyphs }; + let width = text.width(); + frame.push(pos, Element::Text(text)); + + // Apply line decorations. + for line in self.styles.get_ref(TextNode::LINES) { + let face = fonts.get(face_id); + let metrics = match line.kind { + LineKind::Underline => face.underline, + LineKind::Strikethrough => face.strikethrough, + LineKind::Overline => face.overline, + }; + + let extent = line.extent.resolve(size); + let offset = line + .offset + .map(|s| s.resolve(size)) + .unwrap_or(-metrics.position.resolve(size)); + + let stroke = Stroke { + paint: line.stroke.unwrap_or(fill), + thickness: line + .thickness + .map(|s| s.resolve(size)) + .unwrap_or(metrics.thickness.resolve(size)), + }; + + let subpos = Point::new(pos.x - extent, pos.y + offset); + let target = Point::new(width + 2.0 * extent, Length::zero()); + let shape = Shape::stroked(Geometry::Line(target), stroke); + frame.push(subpos, Element::Shape(shape)); + } + + offset += width; + } + + // Apply link if it exists. + if let Some(url) = self.styles.get_ref(LinkNode::URL) { + frame.link(url); + } + + frame + } + + /// Reshape a range of the shaped text, reusing information from this + /// shaping process if possible. + pub fn reshape( + &'a self, + fonts: &mut FontStore, + text_range: Range<usize>, + ) -> ShapedText<'a> { + if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { + let (size, baseline) = measure(fonts, glyphs, &self.styles); + Self { + text: &self.text[text_range], + dir: self.dir, + styles: self.styles.clone(), + size, + baseline, + glyphs: Cow::Borrowed(glyphs), + } + } else { + shape(fonts, &self.text[text_range], self.styles.clone(), self.dir) + } + } + + /// Find the subslice of glyphs that represent the given text range if both + /// sides are safe to break. + fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { + let Range { mut start, mut end } = text_range; + if !self.dir.is_positive() { + std::mem::swap(&mut start, &mut end); + } + + let left = self.find_safe_to_break(start, Side::Left)?; + let right = self.find_safe_to_break(end, Side::Right)?; + Some(&self.glyphs[left .. right]) + } + + /// Find the glyph offset matching the text index that is most towards the + /// given side and safe-to-break. + fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { + let ltr = self.dir.is_positive(); + + // Handle edge cases. + let len = self.glyphs.len(); + if text_index == 0 { + return Some(if ltr { 0 } else { len }); + } else if text_index == self.text.len() { + return Some(if ltr { len } else { 0 }); + } + + // Find any glyph with the text index. + let mut idx = self + .glyphs + .binary_search_by(|g| { + let ordering = g.text_index.cmp(&text_index); + if ltr { ordering } else { ordering.reverse() } + }) + .ok()?; + + let next = match towards { + Side::Left => usize::checked_sub, + Side::Right => usize::checked_add, + }; + + // Search for the outermost glyph with the text index. + while let Some(next) = next(idx, 1) { + if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) { + break; + } + idx = next; + } + + // RTL needs offset one because the left side of the range should be + // exclusive and the right side inclusive, contrary to the normal + // behaviour of ranges. + if !ltr { + idx += 1; + } + + self.glyphs[idx].safe_to_break.then(|| idx) + } +} + +/// A visual side. +enum Side { + Left, + Right, +} diff --git a/src/library/transform.rs b/src/library/transform.rs index 9ba71ecf..6a6b4034 100644 --- a/src/library/transform.rs +++ b/src/library/transform.rs @@ -26,15 +26,15 @@ pub fn rotate(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { } fn transform_impl(args: &mut Args, transform: Transform) -> TypResult<Value> { - let body: Template = args.expect("body")?; + let body: Node = args.expect("body")?; let origin = args .named("origin")? .unwrap_or(Spec::splat(None)) .unwrap_or(Align::CENTER_HORIZON); - Ok(Value::Template(Template::from_inline(move |style| { - body.pack(style).transformed(transform, origin) - }))) + Ok(Value::inline( + body.into_block().transformed(transform, origin), + )) } /// A node that transforms its child without affecting layout. diff --git a/src/parse/mod.rs b/src/parse/mod.rs index dbec0a5e..0a2f73f5 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -110,12 +110,13 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) { // Hashtag + keyword / identifier. NodeKind::Ident(_) | NodeKind::Let + | NodeKind::Set | NodeKind::If | NodeKind::While | NodeKind::For | NodeKind::Import | NodeKind::Include => { - let stmt = matches!(token, NodeKind::Let | NodeKind::Import); + let stmt = matches!(token, NodeKind::Let | NodeKind::Set | NodeKind::Import); let group = if stmt { Group::Stmt } else { Group::Expr }; p.start_group(group); @@ -265,6 +266,7 @@ fn primary(p: &mut Parser, atomic: bool) -> ParseResult { // Keywords. Some(NodeKind::Let) => let_expr(p), + Some(NodeKind::Set) => set_expr(p), Some(NodeKind::If) => if_expr(p), Some(NodeKind::While) => while_expr(p), Some(NodeKind::For) => for_expr(p), @@ -507,45 +509,40 @@ fn block(p: &mut Parser) { /// Parse a function call. fn call(p: &mut Parser, callee: Marker) -> ParseResult { - callee.perform(p, NodeKind::Call, |p| match p.peek_direct() { - Some(NodeKind::LeftParen | NodeKind::LeftBracket) => { - args(p, true); - Ok(()) - } - _ => { - p.expected_at("argument list"); - Err(()) - } - }) + callee.perform(p, NodeKind::Call, |p| args(p, true, true)) } /// Parse the arguments to a function call. -fn args(p: &mut Parser, allow_template: bool) { +fn args(p: &mut Parser, direct: bool, brackets: bool) -> ParseResult { + match if direct { p.peek_direct() } else { p.peek() } { + Some(NodeKind::LeftParen) => {} + Some(NodeKind::LeftBracket) if brackets => {} + _ => { + p.expected("argument list"); + return Err(()); + } + } + p.perform(NodeKind::CallArgs, |p| { - if !allow_template || p.peek_direct() == Some(&NodeKind::LeftParen) { + if p.at(&NodeKind::LeftParen) { p.start_group(Group::Paren); collection(p); p.end_group(); } - while allow_template && p.peek_direct() == Some(&NodeKind::LeftBracket) { + while brackets && p.peek_direct() == Some(&NodeKind::LeftBracket) { template(p); } - }) + }); + + Ok(()) } /// Parse a with expression. fn with_expr(p: &mut Parser, marker: Marker) -> ParseResult { marker.perform(p, NodeKind::WithExpr, |p| { p.eat_assert(&NodeKind::With); - - if p.at(&NodeKind::LeftParen) { - args(p, false); - Ok(()) - } else { - p.expected("argument list"); - Err(()) - } + args(p, false, false) }) } @@ -587,6 +584,15 @@ fn let_expr(p: &mut Parser) -> ParseResult { }) } +/// Parse a set expression. +fn set_expr(p: &mut Parser) -> ParseResult { + p.perform(NodeKind::SetExpr, |p| { + p.eat_assert(&NodeKind::Set); + ident(p)?; + args(p, true, false) + }) +} + /// Parse an if expresion. fn if_expr(p: &mut Parser) -> ParseResult { p.perform(NodeKind::IfExpr, |p| { @@ -612,8 +618,7 @@ fn while_expr(p: &mut Parser) -> ParseResult { p.perform(NodeKind::WhileExpr, |p| { p.eat_assert(&NodeKind::While); expr(p)?; - body(p)?; - Ok(()) + body(p) }) } @@ -624,8 +629,7 @@ fn for_expr(p: &mut Parser) -> ParseResult { for_pattern(p)?; p.eat_expect(&NodeKind::In)?; expr(p)?; - body(p)?; - Ok(()) + body(p) }) } @@ -664,9 +668,7 @@ fn import_expr(p: &mut Parser) -> ParseResult { }; p.eat_expect(&NodeKind::From)?; - expr(p)?; - - Ok(()) + expr(p) }) } @@ -674,8 +676,7 @@ fn import_expr(p: &mut Parser) -> ParseResult { fn include_expr(p: &mut Parser) -> ParseResult { p.perform(NodeKind::IncludeExpr, |p| { p.eat_assert(&NodeKind::Include); - expr(p)?; - Ok(()) + expr(p) }) } diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 6598b1f2..503158a9 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -125,6 +125,7 @@ impl<'s> Parser<'s> { } /// Eat, debug-asserting that the token is the given one. + #[track_caller] pub fn eat_assert(&mut self, t: &NodeKind) { debug_assert_eq!(self.peek(), Some(t)); self.eat(); @@ -199,6 +200,7 @@ impl<'s> Parser<'s> { /// to `end_group`. /// /// This panics if the current token does not start the given group. + #[track_caller] pub fn start_group(&mut self, kind: Group) { self.groups.push(GroupEntry { kind, prev_mode: self.tokens.mode() }); self.tokens.set_mode(match kind { @@ -220,6 +222,7 @@ impl<'s> Parser<'s> { /// End the parsing of a group. /// /// This panics if no group was started. + #[track_caller] pub fn end_group(&mut self) { let group_mode = self.tokens.mode(); let group = self.groups.pop().expect("no started group"); diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 07a6fe12..27ec046d 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -527,6 +527,7 @@ fn keyword(ident: &str) -> Option<NodeKind> { "or" => NodeKind::Or, "with" => NodeKind::With, "let" => NodeKind::Let, + "set" => NodeKind::Set, "if" => NodeKind::If, "else" => NodeKind::Else, "for" => NodeKind::For, diff --git a/src/source.rs b/src/source.rs index 509b0a76..432688a0 100644 --- a/src/source.rs +++ b/src/source.rs @@ -149,7 +149,12 @@ impl SourceFile { Self::new(SourceId(0), Path::new(""), src.into()) } - /// The file's abstract syntax tree. + /// The root node of the file's untyped green tree. + pub fn root(&self) -> &Rc<GreenNode> { + &self.root + } + + /// The root node of the file's typed abstract syntax tree. pub fn ast(&self) -> TypResult<Markup> { let red = RedNode::from_root(self.root.clone(), self.id); let errors = red.errors(); diff --git a/src/style/mod.rs b/src/style/mod.rs deleted file mode 100644 index 45dbeb54..00000000 --- a/src/style/mod.rs +++ /dev/null @@ -1,419 +0,0 @@ -//! Style properties. - -mod paper; - -pub use paper::*; - -use std::fmt::{self, Debug, Formatter}; -use std::rc::Rc; - -use ttf_parser::Tag; - -use crate::eval::Smart; -use crate::font::*; -use crate::geom::*; -use crate::util::EcoString; - -/// Defines a set of properties a template can be instantiated with. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Style { - /// The page settings. - pub page: Rc<PageStyle>, - /// The paragraph settings. - pub par: Rc<ParStyle>, - /// The current text settings. - pub text: Rc<TextStyle>, -} - -impl Style { - /// Access the `page` style mutably. - pub fn page_mut(&mut self) -> &mut PageStyle { - Rc::make_mut(&mut self.page) - } - - /// Access the `par` style mutably. - pub fn par_mut(&mut self) -> &mut ParStyle { - Rc::make_mut(&mut self.par) - } - - /// Access the `text` style mutably. - pub fn text_mut(&mut self) -> &mut TextStyle { - Rc::make_mut(&mut self.text) - } - - /// The resolved line spacing. - pub fn leading(&self) -> Length { - self.par.leading.resolve(self.text.size) - } - - /// The resolved paragraph spacing. - pub fn par_spacing(&self) -> Length { - self.par.spacing.resolve(self.text.size) - } -} - -impl Default for Style { - fn default() -> Self { - Self { - page: Rc::new(PageStyle::default()), - par: Rc::new(ParStyle::default()), - text: Rc::new(TextStyle::default()), - } - } -} - -/// Defines style properties of pages. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct PageStyle { - /// The class of this page. - pub class: PaperClass, - /// The width and height of the page. - pub size: Size, - /// The amount of white space on each side of the page. If a side is set to - /// `None`, the default for the paper class is used. - pub margins: Sides<Smart<Linear>>, - /// The background fill of the page. - pub fill: Option<Paint>, -} - -impl PageStyle { - /// The resolved margins. - pub fn margins(&self) -> Sides<Linear> { - let default = self.class.default_margins(); - Sides { - left: self.margins.left.unwrap_or(default.left), - top: self.margins.top.unwrap_or(default.top), - right: self.margins.right.unwrap_or(default.right), - bottom: self.margins.bottom.unwrap_or(default.bottom), - } - } -} - -impl Default for PageStyle { - fn default() -> Self { - let paper = Paper::A4; - Self { - class: paper.class(), - size: paper.size(), - margins: Sides::splat(Smart::Auto), - fill: None, - } - } -} - -/// Defines style properties of paragraphs. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct ParStyle { - /// The direction for text and inline objects. - pub dir: Dir, - /// How to align text and inline objects in their line. - pub align: Align, - /// The spacing between lines (dependent on scaled font size). - pub leading: Linear, - /// The spacing between paragraphs (dependent on scaled font size). - pub spacing: Linear, -} - -impl Default for ParStyle { - fn default() -> Self { - Self { - dir: Dir::LTR, - align: Align::Left, - leading: Relative::new(0.65).into(), - spacing: Relative::new(1.2).into(), - } - } -} - -/// Defines style properties of text. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct TextStyle { - /// The font size. - pub size: Length, - /// The selected font variant (the final variant also depends on `strong` - /// and `emph`). - pub variant: FontVariant, - /// The top end of the text bounding box. - pub top_edge: VerticalFontMetric, - /// The bottom end of the text bounding box. - pub bottom_edge: VerticalFontMetric, - /// Glyph color. - pub fill: Paint, - /// A list of font families with generic class definitions (the final - /// family list also depends on `monospace`). - pub families: Rc<FamilyStyle>, - /// OpenType features. - pub features: Rc<FontFeatures>, - /// The amount of space that should be added between character. - pub tracking: Em, - /// Whether 300 extra font weight should be added to what is defined by the - /// `variant`. - pub strong: bool, - /// Whether the the font style defined by the `variant` should be inverted. - pub emph: bool, - /// Whether a monospace font should be preferred. - pub monospace: bool, - /// Whether font fallback to a base list should occur. - pub fallback: bool, -} - -impl TextStyle { - /// The resolved variant with `strong` and `emph` factored in. - pub fn variant(&self) -> FontVariant { - let mut variant = self.variant; - - if self.strong { - variant.weight = variant.weight.thicken(300); - } - - if self.emph { - variant.style = match variant.style { - FontStyle::Normal => FontStyle::Italic, - FontStyle::Italic => FontStyle::Normal, - FontStyle::Oblique => FontStyle::Normal, - } - } - - variant - } - - /// The resolved family iterator. - pub fn families(&self) -> impl Iterator<Item = &str> + Clone { - let head = if self.monospace { - self.families.monospace.as_slice() - } else { - &[] - }; - - let core = self.families.list.iter().flat_map(move |family| { - match family { - FontFamily::Named(name) => std::slice::from_ref(name), - FontFamily::Serif => &self.families.serif, - FontFamily::SansSerif => &self.families.sans_serif, - FontFamily::Monospace => &self.families.monospace, - } - }); - - let tail = if self.fallback { - self.families.base.as_slice() - } else { - &[] - }; - - head.iter().chain(core).chain(tail).map(EcoString::as_str) - } - - /// Access the `families` style mutably. - pub fn families_mut(&mut self) -> &mut FamilyStyle { - Rc::make_mut(&mut self.families) - } - - /// Access the font `features` mutably. - pub fn features_mut(&mut self) -> &mut FontFeatures { - Rc::make_mut(&mut self.features) - } -} - -impl Default for TextStyle { - fn default() -> Self { - Self { - size: Length::pt(11.0), - variant: FontVariant { - style: FontStyle::Normal, - weight: FontWeight::REGULAR, - stretch: FontStretch::NORMAL, - }, - top_edge: VerticalFontMetric::CapHeight, - bottom_edge: VerticalFontMetric::Baseline, - fill: RgbaColor::BLACK.into(), - families: Rc::new(FamilyStyle::default()), - features: Rc::new(FontFeatures::default()), - tracking: Em::zero(), - strong: false, - emph: false, - monospace: false, - fallback: true, - } - } -} - -/// Font list with family definitions. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct FamilyStyle { - /// The user-defined list of font families. - pub list: Vec<FontFamily>, - /// Definition of serif font families. - pub serif: Vec<EcoString>, - /// Definition of sans-serif font families. - pub sans_serif: Vec<EcoString>, - /// Definition of monospace font families used for raw text. - pub monospace: Vec<EcoString>, - /// Base fonts that are tried as last resort. - pub base: Vec<EcoString>, -} - -impl Default for FamilyStyle { - fn default() -> Self { - Self { - list: vec![FontFamily::SansSerif], - serif: vec!["ibm plex serif".into()], - sans_serif: vec!["ibm plex sans".into()], - monospace: vec!["ibm plex mono".into()], - base: vec![ - "ibm plex sans".into(), - "latin modern math".into(), - "twitter color emoji".into(), - ], - } - } -} - -/// A generic or named font family. -#[derive(Clone, Eq, PartialEq, Hash)] -pub enum FontFamily { - /// A family that has "serifs", small strokes attached to letters. - Serif, - /// A family in which glyphs do not have "serifs", small attached strokes. - SansSerif, - /// A family in which (almost) all glyphs are of equal width. - Monospace, - /// A specific family with a name. - Named(EcoString), -} - -impl Debug for FontFamily { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Serif => "serif", - Self::SansSerif => "sans-serif", - Self::Monospace => "monospace", - Self::Named(s) => s, - }) - } -} - -/// Whether various kinds of ligatures should appear. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct FontFeatures { - /// Whether to apply kerning ("kern"). - pub kerning: bool, - /// Whether the text should use small caps. ("smcp") - pub smallcaps: bool, - /// Whether to apply stylistic alternates. ("salt") - pub alternates: bool, - /// Which stylistic set to apply. ("ss01" - "ss20") - pub stylistic_set: Option<StylisticSet>, - /// Configuration of ligature features. - pub ligatures: LigatureFeatures, - /// Configuration of numbers features. - pub numbers: NumberFeatures, - /// Raw OpenType features to apply. - pub raw: Vec<(Tag, u32)>, -} - -impl Default for FontFeatures { - fn default() -> Self { - Self { - kerning: true, - smallcaps: false, - alternates: false, - stylistic_set: None, - ligatures: LigatureFeatures::default(), - numbers: NumberFeatures::default(), - raw: vec![], - } - } -} - -/// A stylistic set in a font face. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct StylisticSet(u8); - -impl StylisticSet { - /// Creates a new set, clamping to 1-20. - pub fn new(index: u8) -> Self { - Self(index.clamp(1, 20)) - } - - /// Get the value, guaranteed to be 1-20. - pub fn get(self) -> u8 { - self.0 - } -} - -/// Whether various kinds of ligatures should appear. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct LigatureFeatures { - /// Standard ligatures. ("liga", "clig") - pub standard: bool, - /// Ligatures that should be used sparringly. ("dlig") - pub discretionary: bool, - /// Historical ligatures. ("hlig") - pub historical: bool, -} - -impl Default for LigatureFeatures { - fn default() -> Self { - Self { - standard: true, - discretionary: false, - historical: false, - } - } -} - -/// Defines the style of numbers. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct NumberFeatures { - /// Whether to use lining or old-style numbers. - pub type_: Smart<NumberType>, - /// Whether to use proportional or tabular numbers. - pub width: Smart<NumberWidth>, - /// How to position numbers vertically. - pub position: NumberPosition, - /// Whether to have a slash through the zero glyph. ("zero") - pub slashed_zero: bool, - /// Whether to convert fractions. ("frac") - pub fractions: bool, -} - -impl Default for NumberFeatures { - fn default() -> Self { - Self { - type_: Smart::Auto, - width: Smart::Auto, - position: NumberPosition::Normal, - slashed_zero: false, - fractions: false, - } - } -} - -/// Which kind of numbers / figures to select. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum NumberType { - /// Numbers that fit well with capital text. ("lnum") - Lining, - /// Numbers that fit well into flow of upper- and lowercase text. ("onum") - OldStyle, -} - -/// The width of numbers / figures. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum NumberWidth { - /// Number widths are glyph specific. ("pnum") - Proportional, - /// All numbers are of equal width / monospaced. ("tnum") - Tabular, -} - -/// How to position numbers. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum NumberPosition { - /// Numbers are positioned on the same baseline as text. - Normal, - /// Numbers are smaller and placed at the bottom. ("subs") - Subscript, - /// Numbers are smaller and placed at the top. ("sups") - Superscript, -} diff --git a/src/style/paper.rs b/src/style/paper.rs deleted file mode 100644 index 252d293d..00000000 --- a/src/style/paper.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::geom::{Length, Linear, Relative, Sides, Size}; - -/// Specification of a paper. -#[derive(Debug, Copy, Clone)] -pub struct Paper { - /// The broad class this paper belongs to. - class: PaperClass, - /// The width of the paper in millimeters. - width: f64, - /// The height of the paper in millimeters. - height: f64, -} - -/// Defines default margins for a class of related papers. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum PaperClass { - Custom, - Base, - US, - Newspaper, - Book, -} - -impl PaperClass { - /// The default margins for this page class. - pub fn default_margins(self) -> Sides<Linear> { - let f = |r| Relative::new(r).into(); - let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b)); - match self { - Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842), - Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842), - Self::US => s(0.1760, 0.1092, 0.1760, 0.0910), - Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294), - Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965), - } - } -} - -macro_rules! papers { - ($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => { - impl Paper { - /// Parse a paper from its name. - /// - /// Both lower and upper case are fine. - pub fn from_name(name: &str) -> Option<Self> { - match name.to_lowercase().as_str() { - $($($pats)* => Some(Self::$var),)* - _ => None, - } - } - - /// The class of the paper. - pub fn class(self) -> PaperClass { - self.class - } - - /// The size of the paper. - pub fn size(self) -> Size { - Size::new(Length::mm(self.width), Length::mm(self.height)) - } - } - - /// Predefined papers. - /// - /// Each paper is parsable from its name in kebab-case. - impl Paper { - $(papers!(@$var, stringify!($($pats)*), $class, $width, $height);)* - } - }; - - (@$var:ident, $names:expr, $class:ident, $width:expr, $height:expr) => { - pub const $var: Self = Self { - class: PaperClass::$class, - width: $width, - height: $height, - }; - }; -} - -// All paper sizes in mm. -// -// Resources: -// - https://papersizes.io/ -// - https://en.wikipedia.org/wiki/Paper_size -// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm -// - https://vintagepaper.co/blogs/news/traditional-paper-sizes -papers! { - // ---------------------------------------------------------------------- // - // ISO 216 A Series - (A0: Base, 841.0, 1189.0, "a0") - (A1: Base, 594.0, 841.0, "a1") - (A2: Base, 420.0, 594.0, "a2") - (A3: Base, 297.0, 420.0, "a3") - (A4: Base, 210.0, 297.0, "a4") - (A5: Base, 148.0, 210.0, "a5") - (A6: Book, 105.0, 148.0, "a6") - (A7: Base, 74.0, 105.0, "a7") - (A8: Base, 52.0, 74.0, "a8") - (A9: Base, 37.0, 52.0, "a9") - (A10: Base, 26.0, 37.0, "a10") - (A11: Base, 18.0, 26.0, "a11") - - // ISO 216 B Series - (ISO_B1: Base, 707.0, 1000.0, "iso-b1") - (ISO_B2: Base, 500.0, 707.0, "iso-b2") - (ISO_B3: Base, 353.0, 500.0, "iso-b3") - (ISO_B4: Base, 250.0, 353.0, "iso-b4") - (ISO_B5: Book, 176.0, 250.0, "iso-b5") - (ISO_B6: Book, 125.0, 176.0, "iso-b6") - (ISO_B7: Base, 88.0, 125.0, "iso-b7") - (ISO_B8: Base, 62.0, 88.0, "iso-b8") - - // ISO 216 C Series - (ISO_C3: Base, 324.0, 458.0, "iso-c3") - (ISO_C4: Base, 229.0, 324.0, "iso-c4") - (ISO_C5: Base, 162.0, 229.0, "iso-c5") - (ISO_C6: Base, 114.0, 162.0, "iso-c6") - (ISO_C7: Base, 81.0, 114.0, "iso-c7") - (ISO_C8: Base, 57.0, 81.0, "iso-c8") - - // DIN D Series (extension to ISO) - (DIN_D3: Base, 272.0, 385.0, "din-d3") - (DIN_D4: Base, 192.0, 272.0, "din-d4") - (DIN_D5: Base, 136.0, 192.0, "din-d5") - (DIN_D6: Base, 96.0, 136.0, "din-d6") - (DIN_D7: Base, 68.0, 96.0, "din-d7") - (DIN_D8: Base, 48.0, 68.0, "din-d8") - - // SIS (used in academia) - (SIS_G5: Base, 169.0, 239.0, "sis-g5") - (SIS_E5: Base, 115.0, 220.0, "sis-e5") - - // ANSI Extensions - (ANSI_A: Base, 216.0, 279.0, "ansi-a") - (ANSI_B: Base, 279.0, 432.0, "ansi-b") - (ANSI_C: Base, 432.0, 559.0, "ansi-c") - (ANSI_D: Base, 559.0, 864.0, "ansi-d") - (ANSI_E: Base, 864.0, 1118.0, "ansi-e") - - // ANSI Architectural Paper - (ARCH_A: Base, 229.0, 305.0, "arch-a") - (ARCH_B: Base, 305.0, 457.0, "arch-b") - (ARCH_C: Base, 457.0, 610.0, "arch-c") - (ARCH_D: Base, 610.0, 914.0, "arch-d") - (ARCH_E1: Base, 762.0, 1067.0, "arch-e1") - (ARCH_E: Base, 914.0, 1219.0, "arch-e") - - // JIS B Series - (JIS_B0: Base, 1030.0, 1456.0, "jis-b0") - (JIS_B1: Base, 728.0, 1030.0, "jis-b1") - (JIS_B2: Base, 515.0, 728.0, "jis-b2") - (JIS_B3: Base, 364.0, 515.0, "jis-b3") - (JIS_B4: Base, 257.0, 364.0, "jis-b4") - (JIS_B5: Base, 182.0, 257.0, "jis-b5") - (JIS_B6: Base, 128.0, 182.0, "jis-b6") - (JIS_B7: Base, 91.0, 128.0, "jis-b7") - (JIS_B8: Base, 64.0, 91.0, "jis-b8") - (JIS_B9: Base, 45.0, 64.0, "jis-b9") - (JIS_B10: Base, 32.0, 45.0, "jis-b10") - (JIS_B11: Base, 22.0, 32.0, "jis-b11") - - // SAC D Series - (SAC_D0: Base, 764.0, 1064.0, "sac-d0") - (SAC_D1: Base, 532.0, 760.0, "sac-d1") - (SAC_D2: Base, 380.0, 528.0, "sac-d2") - (SAC_D3: Base, 264.0, 376.0, "sac-d3") - (SAC_D4: Base, 188.0, 260.0, "sac-d4") - (SAC_D5: Base, 130.0, 184.0, "sac-d5") - (SAC_D6: Base, 92.0, 126.0, "sac-d6") - - // ISO 7810 ID - (ISO_ID_1: Base, 85.6, 53.98, "iso-id-1") - (ISO_ID_2: Base, 74.0, 105.0, "iso-id-2") - (ISO_ID_3: Base, 88.0, 125.0, "iso-id-3") - - // ---------------------------------------------------------------------- // - // Asia - (ASIA_F4: Base, 210.0, 330.0, "asia-f4") - - // Japan - (JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4") - (JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5") - (JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6") - (JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4") - (JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5") - (JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card") - - // China - (CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card") - - // Europe - (EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card") - - // French Traditional (AFNOR) - (FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière") - (FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture") - (FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition") - (FR_RAISIN: Base, 500.0, 650.0, "fr-raisin") - (FR_CARRE: Base, 450.0, 560.0, "fr-carré") - (FR_JESUS: Base, 560.0, 760.0, "fr-jésus") - - // United Kingdom Imperial - (UK_BRIEF: Base, 406.4, 342.9, "uk-brief") - (UK_DRAFT: Base, 254.0, 406.4, "uk-draft") - (UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap") - (UK_QUARTO: Base, 203.2, 254.0, "uk-quarto") - (UK_CROWN: Base, 508.0, 381.0, "uk-crown") - (UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a") - (UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b") - - // Unites States - (US_LETTER: US, 215.9, 279.4, "us-letter") - (US_LEGAL: US, 215.9, 355.6, "us-legal") - (US_TABLOID: US, 279.4, 431.8, "us-tabloid") - (US_EXECUTIVE: US, 184.15, 266.7, "us-executive") - (US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio") - (US_STATEMENT: US, 139.7, 215.9, "us-statement") - (US_LEDGER: US, 431.8, 279.4, "us-ledger") - (US_OFICIO: US, 215.9, 340.36, "us-oficio") - (US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter") - (US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal") - (US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card") - (US_DIGEST: Book, 139.7, 215.9, "us-digest") - (US_TRADE: Book, 152.4, 228.6, "us-trade") - - // ---------------------------------------------------------------------- // - // Other - (NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact") - (NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner") - (NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet") - (PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9") - (PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3") -} diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 8df25f59..ae8ecdc9 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -1,4 +1,6 @@ //! A typed layer over the red-green tree. +//! +//! The AST is rooted in the [`Markup`] node. use std::ops::Deref; @@ -211,6 +213,8 @@ pub enum Expr { With(WithExpr), /// A let expression: `let x = 1`. Let(LetExpr), + /// A set expression: `set text(...)`. + Set(SetExpr), /// An if-else expression: `if x { y } else { z }`. If(IfExpr), /// A while loop expression: `while x { y }`. @@ -238,6 +242,7 @@ impl TypedNode for Expr { NodeKind::Closure => node.cast().map(Self::Closure), NodeKind::WithExpr => node.cast().map(Self::With), NodeKind::LetExpr => node.cast().map(Self::Let), + NodeKind::SetExpr => node.cast().map(Self::Set), NodeKind::IfExpr => node.cast().map(Self::If), NodeKind::WhileExpr => node.cast().map(Self::While), NodeKind::ForExpr => node.cast().map(Self::For), @@ -262,6 +267,7 @@ impl TypedNode for Expr { Self::Closure(v) => v.as_red(), Self::With(v) => v.as_red(), Self::Let(v) => v.as_red(), + Self::Set(v) => v.as_red(), Self::If(v) => v.as_red(), Self::While(v) => v.as_red(), Self::For(v) => v.as_red(), @@ -279,6 +285,7 @@ impl Expr { Self::Ident(_) | Self::Call(_) | Self::Let(_) + | Self::Set(_) | Self::If(_) | Self::While(_) | Self::For(_) @@ -838,6 +845,25 @@ impl LetExpr { } node! { + /// A set expression: `set text(...)`. + SetExpr +} + +impl SetExpr { + /// The class to set style properties for. + pub fn class(&self) -> Ident { + self.0.cast_first_child().expect("set expression is missing class") + } + + /// The style properties to set. + pub fn args(&self) -> CallArgs { + self.0 + .cast_first_child() + .expect("set expression is missing argument list") + } +} + +node! { /// An import expression: `import a, b, c from "utils.typ"`. ImportExpr } diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index 22e6cf50..85fbef12 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -96,22 +96,23 @@ impl Category { NodeKind::EnDash => Some(Category::Shortcut), NodeKind::EmDash => Some(Category::Shortcut), NodeKind::Escape(_) => Some(Category::Escape), + NodeKind::Not => Some(Category::Keyword), + NodeKind::And => Some(Category::Keyword), + NodeKind::Or => Some(Category::Keyword), + NodeKind::With => Some(Category::Keyword), NodeKind::Let => Some(Category::Keyword), + NodeKind::Set => Some(Category::Keyword), NodeKind::If => Some(Category::Keyword), NodeKind::Else => Some(Category::Keyword), + NodeKind::While => Some(Category::Keyword), NodeKind::For => Some(Category::Keyword), NodeKind::In => Some(Category::Keyword), - NodeKind::While => Some(Category::Keyword), NodeKind::Break => Some(Category::Keyword), NodeKind::Continue => Some(Category::Keyword), NodeKind::Return => Some(Category::Keyword), NodeKind::Import => Some(Category::Keyword), - NodeKind::Include => Some(Category::Keyword), NodeKind::From => Some(Category::Keyword), - NodeKind::Not => Some(Category::Keyword), - NodeKind::And => Some(Category::Keyword), - NodeKind::Or => Some(Category::Keyword), - NodeKind::With => Some(Category::Keyword), + NodeKind::Include => Some(Category::Keyword), NodeKind::Plus => Some(Category::Operator), NodeKind::Star => Some(Category::Operator), NodeKind::Slash => Some(Category::Operator), @@ -139,6 +140,7 @@ impl Category { Some(Category::Function) } NodeKind::WithExpr => Some(Category::Function), + NodeKind::SetExpr => Some(Category::Function), NodeKind::Call => Some(Category::Function), _ => Some(Category::Variable), }, @@ -161,21 +163,22 @@ impl Category { NodeKind::Array => None, NodeKind::Dict => None, NodeKind::Named => None, + NodeKind::Template => None, NodeKind::Group => None, + NodeKind::Block => None, NodeKind::Unary => None, NodeKind::Binary => None, NodeKind::Call => None, NodeKind::CallArgs => None, + NodeKind::Spread => None, NodeKind::Closure => None, NodeKind::ClosureParams => None, - NodeKind::Spread => None, - NodeKind::Template => None, - NodeKind::Block => None, - NodeKind::ForExpr => None, - NodeKind::WhileExpr => None, - NodeKind::IfExpr => None, - NodeKind::LetExpr => None, NodeKind::WithExpr => None, + NodeKind::LetExpr => None, + NodeKind::SetExpr => None, + NodeKind::IfExpr => None, + NodeKind::WhileExpr => None, + NodeKind::ForExpr => None, NodeKind::ForPattern => None, NodeKind::ImportExpr => None, NodeKind::ImportItems => None, diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index e9011a4d..d9ad42a8 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -83,19 +83,15 @@ impl Default for Green { impl Debug for Green { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{:?}: {}", self.kind(), self.len())?; - if let Self::Node(n) = self { - if !n.children.is_empty() { - f.write_str(" ")?; - f.debug_list().entries(&n.children).finish()?; - } + match self { + Self::Node(node) => node.fmt(f), + Self::Token(token) => token.fmt(f), } - Ok(()) } } /// An inner node in the untyped green tree. -#[derive(Debug, Clone, PartialEq)] +#[derive(Clone, PartialEq)] pub struct GreenNode { /// Node metadata. data: GreenData, @@ -145,8 +141,19 @@ impl From<Rc<GreenNode>> for Green { } } +impl Debug for GreenNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.data.fmt(f)?; + if !self.children.is_empty() { + f.write_str(" ")?; + f.debug_list().entries(&self.children).finish()?; + } + Ok(()) + } +} + /// Data shared between inner and leaf nodes. -#[derive(Debug, Clone, PartialEq)] +#[derive(Clone, PartialEq)] pub struct GreenData { /// What kind of node this is (each kind would have its own struct in a /// strongly typed AST). @@ -178,6 +185,12 @@ impl From<GreenData> for Green { } } +impl Debug for GreenData { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{:?}: {}", self.kind, self.len) + } +} + /// A owned wrapper for a green node with span information. /// /// Owned variant of [`RedRef`]. Can be [cast](Self::cast) to an AST node. @@ -465,6 +478,8 @@ pub enum NodeKind { Auto, /// The `let` keyword. Let, + /// The `set` keyword. + Set, /// The `if` keyword. If, /// The `else` keyword. @@ -552,8 +567,12 @@ pub enum NodeKind { Dict, /// A named pair: `thickness: 3pt`. Named, + /// A template expression: `[*Hi* there!]`. + Template, /// A grouped expression: `(1 + 2)`. Group, + /// A block expression: `{ let x = 1; x + 2 }`. + Block, /// A unary operation: `-x`. Unary, /// A binary operation: `a + b`. @@ -562,39 +581,37 @@ pub enum NodeKind { Call, /// A function call's argument list: `(x, y)`. CallArgs, + /// Spreaded arguments or a parameter sink: `..x`. + Spread, /// A closure expression: `(x, y) => z`. Closure, /// A closure's parameters: `(x, y)`. ClosureParams, - /// A parameter sink: `..x`. - Spread, - /// A template expression: `[*Hi* there!]`. - Template, - /// A block expression: `{ let x = 1; x + 2 }`. - Block, - /// A for loop expression: `for x in y { ... }`. - ForExpr, - /// A while loop expression: `while x { ... }`. - WhileExpr, - /// An if expression: `if x { ... }`. - IfExpr, + /// A with expression: `f with (x, y: 1)`. + WithExpr, /// A let expression: `let x = 1`. LetExpr, - /// The `with` expression: `with (1)`. - WithExpr, + /// A set expression: `set text(...)`. + SetExpr, + /// An if-else expression: `if x { y } else { z }`. + IfExpr, + /// A while loop expression: `while x { ... }`. + WhileExpr, + /// A for loop expression: `for x in y { ... }`. + ForExpr, /// A for loop's destructuring pattern: `x` or `x, y`. ForPattern, - /// The import expression: `import x from "foo.typ"`. + /// An import expression: `import a, b, c from "utils.typ"`. ImportExpr, /// Items to import: `a, b, c`. ImportItems, - /// The include expression: `include "foo.typ"`. + /// An include expression: `include "chapter1.typ"`. IncludeExpr, - /// Two slashes followed by inner contents, terminated with a newline: - /// `//<str>\n`. + /// A line comment, two slashes followed by inner contents, terminated with + /// a newline: `//<str>\n`. LineComment, - /// A slash and a star followed by inner contents, terminated with a star - /// and a slash: `/*<str>*/`. + /// A block comment, a slash and a star followed by inner contents, + /// terminated with a star and a slash: `/*<str>*/`. /// /// The comment can contain nested block comments. BlockComment, @@ -616,11 +633,6 @@ pub enum ErrorPos { } impl NodeKind { - /// Whether this is some kind of parenthesis. - pub fn is_paren(&self) -> bool { - matches!(self, Self::LeftParen | Self::RightParen) - } - /// Whether this is some kind of bracket. pub fn is_bracket(&self) -> bool { matches!(self, Self::LeftBracket | Self::RightBracket) @@ -631,6 +643,11 @@ impl NodeKind { matches!(self, Self::LeftBrace | Self::RightBrace) } + /// Whether this is some kind of parenthesis. + pub fn is_paren(&self) -> bool { + matches!(self, Self::LeftParen | Self::RightParen) + } + /// Whether this is some kind of error. pub fn is_error(&self) -> bool { matches!(self, NodeKind::Error(_, _) | NodeKind::Unknown(_)) @@ -672,6 +689,7 @@ impl NodeKind { Self::None => "`none`", Self::Auto => "`auto`", Self::Let => "keyword `let`", + Self::Set => "keyword `set`", Self::If => "keyword `if`", Self::Else => "keyword `else`", Self::For => "keyword `for`", @@ -712,21 +730,22 @@ impl NodeKind { Self::Array => "array", Self::Dict => "dictionary", Self::Named => "named argument", + Self::Template => "template", Self::Group => "group", + Self::Block => "block", Self::Unary => "unary expression", Self::Binary => "binary expression", Self::Call => "call", Self::CallArgs => "call arguments", + Self::Spread => "parameter sink", Self::Closure => "closure", Self::ClosureParams => "closure parameters", - Self::Spread => "parameter sink", - Self::Template => "template", - Self::Block => "block", - Self::ForExpr => "for-loop expression", - Self::WhileExpr => "while-loop expression", - Self::IfExpr => "`if` expression", - Self::LetExpr => "`let` expression", Self::WithExpr => "`with` expression", + Self::LetExpr => "`let` expression", + Self::SetExpr => "`set` expression", + Self::IfExpr => "`if` expression", + Self::WhileExpr => "while-loop expression", + Self::ForExpr => "for-loop expression", Self::ForPattern => "for-loop destructuring pattern", Self::ImportExpr => "`import` expression", Self::ImportItems => "import items", diff --git a/src/syntax/pretty.rs b/src/syntax/pretty.rs index c453fb56..62ecb8cd 100644 --- a/src/syntax/pretty.rs +++ b/src/syntax/pretty.rs @@ -225,6 +225,7 @@ impl Pretty for Expr { Self::Closure(v) => v.pretty(p), Self::With(v) => v.pretty(p), Self::Let(v) => v.pretty(p), + Self::Set(v) => v.pretty(p), Self::If(v) => v.pretty(p), Self::While(v) => v.pretty(p), Self::For(v) => v.pretty(p), @@ -444,6 +445,16 @@ impl Pretty for LetExpr { } } +impl Pretty for SetExpr { + fn pretty(&self, p: &mut Printer) { + p.push_str("set "); + self.class().pretty(p); + p.push_str("("); + self.args().pretty(p); + p.push(')'); + } +} + impl Pretty for IfExpr { fn pretty(&self, p: &mut Printer) { p.push_str("if "); @@ -639,6 +650,7 @@ mod tests { // Control flow. roundtrip("#let x = 1 + 2"); roundtrip("#let f(x) = y"); + roundtrip("#set text(size: 12pt)"); roundtrip("#if x [y] else [z]"); roundtrip("#if x {} else if y {} else {}"); roundtrip("#while x {y}"); diff --git a/src/util/mod.rs b/src/util/mod.rs index 6fc1fb59..694c90c8 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -13,18 +13,6 @@ use std::ops::Range; use std::path::{Component, Path, PathBuf}; use std::rc::Rc; -/// Additional methods for booleans. -pub trait BoolExt { - /// Toggle the value of the bool in place. - fn flip(&mut self); -} - -impl BoolExt for bool { - fn flip(&mut self) { - *self = !*self; - } -} - /// Additional methods for options. pub trait OptionExt<T> { /// Sets `other` as the value if `self` is `None` or if it contains a value |
