summaryrefslogtreecommitdiff
path: root/src/library
diff options
context:
space:
mode:
authorMartin <mhaug@live.de>2021-12-22 20:37:34 +0100
committerGitHub <noreply@github.com>2021-12-22 20:37:34 +0100
commitf6c7a8292dc1ab0560408fca9d74505e9d7cf13a (patch)
treebadd3076f6146cec34c55764600df5124c408521 /src/library
parent738ff7e1f573bef678932b313be9969a17af8d22 (diff)
parent438255519e88bb790480306b9a9b452aaf054519 (diff)
Merge pull request #51 from typst/set-rules
Set rules
Diffstat (limited to 'src/library')
-rw-r--r--src/library/align.rs44
-rw-r--r--src/library/deco.rs132
-rw-r--r--src/library/document.rs16
-rw-r--r--src/library/flow.rs118
-rw-r--r--src/library/grid.rs16
-rw-r--r--src/library/heading.rs63
-rw-r--r--src/library/image.rs12
-rw-r--r--src/library/link.rs28
-rw-r--r--src/library/list.rs102
-rw-r--r--src/library/mod.rs69
-rw-r--r--src/library/pad.rs6
-rw-r--r--src/library/page.rs450
-rw-r--r--src/library/par.rs262
-rw-r--r--src/library/placed.rs26
-rw-r--r--src/library/shape.rs32
-rw-r--r--src/library/sized.rs14
-rw-r--r--src/library/spacing.rs36
-rw-r--r--src/library/stack.rs90
-rw-r--r--src/library/text.rs1101
-rw-r--r--src/library/transform.rs8
20 files changed, 1658 insertions, 967 deletions
diff --git a/src/library/align.rs b/src/library/align.rs
index 18920369..c16277f6 100644
--- a/src/library/align.rs
+++ b/src/library/align.rs
@@ -1,29 +1,19 @@
use super::prelude::*;
+use super::ParNode;
/// `align`: Configure the alignment along the layouting axes.
pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- castable! {
- Spec<Option<Align>>,
- Expected: "1d or 2d alignment",
- @align: Align => {
- let mut aligns = Spec::default();
- aligns.set(align.axis(), Some(*align));
- aligns
- },
- @aligns: Spec<Align> => aligns.map(Some),
+ let aligns: Spec<_> = args.expect("alignment")?;
+ let body: Node = args.expect("body")?;
+ let mut styles = Styles::new();
+ if let Some(align) = aligns.x {
+ styles.set(ParNode::ALIGN, align);
}
- let aligns = args.expect::<Spec<_>>("alignment")?;
- let body = args.expect::<Template>("body")?;
- Ok(Value::Template(Template::from_block(move |style| {
- let mut style = style.clone();
- if let Some(x) = aligns.x {
- style.par_mut().align = x;
- }
-
- body.pack(&style).aligned(aligns)
- })))
+ Ok(Value::block(
+ body.into_block().styled(styles).aligned(aligns),
+ ))
}
/// A node that aligns its child.
@@ -67,3 +57,19 @@ impl Layout for AlignNode {
frames
}
}
+
+dynamic! {
+ Align: "alignment",
+}
+
+castable! {
+ Spec<Option<Align>>,
+ Expected: "1d or 2d alignment",
+ @align: Align => {
+ let mut aligns = Spec::default();
+ aligns.set(align.axis(), Some(*align));
+ aligns
+ },
+ @aligns: Spec<Align> => aligns.map(Some),
+
+}
diff --git a/src/library/deco.rs b/src/library/deco.rs
deleted file mode 100644
index cb065689..00000000
--- a/src/library/deco.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-use super::prelude::*;
-use crate::util::EcoString;
-
-/// `strike`: Typeset striken-through text.
-pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Strikethrough)
-}
-
-/// `underline`: Typeset underlined text.
-pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Underline)
-}
-
-/// `overline`: Typeset text with an overline.
-pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Overline)
-}
-
-fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
- let stroke = args.named("stroke")?.or_else(|| args.find());
- let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
- let offset = args.named("offset")?;
- let extent = args.named("extent")?.unwrap_or_default();
- let body: Template = args.expect("body")?;
- Ok(Value::Template(body.decorate(Decoration::Line(
- LineDecoration { kind, stroke, thickness, offset, extent },
- ))))
-}
-
-/// `link`: Typeset text as a link.
-pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let url = args.expect::<EcoString>("url")?;
- let body = args.find().unwrap_or_else(|| {
- let mut template = Template::new();
- template.text(url.trim_start_matches("mailto:").trim_start_matches("tel:"));
- template
- });
-
- Ok(Value::Template(body.decorate(Decoration::Link(url))))
-}
-
-/// A decoration for a frame.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub enum Decoration {
- /// A link to an external resource.
- Link(EcoString),
- /// An underline/strikethrough/overline decoration.
- Line(LineDecoration),
-}
-
-impl Decoration {
- /// Apply a decoration to a child's frame.
- pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
- match self {
- Decoration::Link(href) => {
- let link = Element::Link(href.to_string(), frame.size);
- frame.push(Point::zero(), link);
- }
- Decoration::Line(line) => {
- line.apply(ctx, frame);
- }
- }
- }
-}
-
-/// Defines a line that is positioned over, under or on top of text.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct LineDecoration {
- /// The kind of line.
- pub kind: LineKind,
- /// Stroke color of the line, defaults to the text color if `None`.
- pub stroke: Option<Paint>,
- /// Thickness of the line's strokes (dependent on scaled font size), read
- /// from the font tables if `None`.
- pub thickness: Option<Linear>,
- /// Position of the line relative to the baseline (dependent on scaled font
- /// size), read from the font tables if `None`.
- pub offset: Option<Linear>,
- /// Amount that the line will be longer or shorter than its associated text
- /// (dependent on scaled font size).
- pub extent: Linear,
-}
-
-/// The kind of line decoration.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum LineKind {
- /// A line under text.
- Underline,
- /// A line through text.
- Strikethrough,
- /// A line over text.
- Overline,
-}
-
-impl LineDecoration {
- /// Apply a line decoration to a all text elements in a frame.
- pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
- for i in 0 .. frame.elements.len() {
- let (pos, child) = &frame.elements[i];
- if let Element::Text(text) = child {
- let face = ctx.fonts.get(text.face_id);
- let metrics = match self.kind {
- LineKind::Underline => face.underline,
- LineKind::Strikethrough => face.strikethrough,
- LineKind::Overline => face.overline,
- };
-
- let thickness = self
- .thickness
- .map(|s| s.resolve(text.size))
- .unwrap_or(metrics.thickness.to_length(text.size));
-
- let stroke = Stroke {
- paint: self.stroke.unwrap_or(text.fill),
- thickness,
- };
-
- let offset = self
- .offset
- .map(|s| s.resolve(text.size))
- .unwrap_or(-metrics.position.to_length(text.size));
-
- let extent = self.extent.resolve(text.size);
-
- let subpos = Point::new(pos.x - extent, pos.y + offset);
- let target = Point::new(text.width + 2.0 * extent, Length::zero());
- let shape = Shape::stroked(Geometry::Line(target), stroke);
- frame.push(subpos, Element::Shape(shape));
- }
- }
- }
-}
diff --git a/src/library/document.rs b/src/library/document.rs
deleted file mode 100644
index fe01d2df..00000000
--- a/src/library/document.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-use super::prelude::*;
-use super::PageNode;
-
-/// The root layout node, a document consisting of top-level page runs.
-#[derive(Debug, Hash)]
-pub struct DocumentNode {
- /// The page runs.
- pub pages: Vec<PageNode>,
-}
-
-impl DocumentNode {
- /// Layout the document into a sequence of frames, one per page.
- pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
- self.pages.iter().flat_map(|node| node.layout(ctx)).collect()
- }
-}
diff --git a/src/library/flow.rs b/src/library/flow.rs
index 98b518b7..cfa761b6 100644
--- a/src/library/flow.rs
+++ b/src/library/flow.rs
@@ -1,50 +1,14 @@
use std::fmt::{self, Debug, Formatter};
use super::prelude::*;
-use super::{AlignNode, ParNode, PlacedNode, Spacing};
-
-/// `flow`: A vertical flow of paragraphs and other layout nodes.
-pub fn flow(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- enum Child {
- Spacing(Spacing),
- Any(Template),
- }
-
- castable! {
- Child,
- Expected: "linear, fractional or template",
- Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
- Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Template(v) => Self::Any(v),
- }
-
- let children: Vec<Child> = args.all().collect();
-
- Ok(Value::Template(Template::from_block(move |style| {
- let children = children
- .iter()
- .map(|child| match child {
- Child::Spacing(spacing) => FlowChild::Spacing(*spacing),
- Child::Any(node) => FlowChild::Node(node.pack(style)),
- })
- .collect();
-
- FlowNode { children }
- })))
-}
+use super::{AlignNode, ParNode, PlacedNode, SpacingKind, SpacingNode, TextNode};
/// A vertical flow of content consisting of paragraphs and other layout nodes.
///
/// This node is reponsible for layouting both the top-level content flow and
/// the contents of boxes.
-#[derive(Debug, Hash)]
-pub struct FlowNode {
- /// The children that compose the flow. There are different kinds of
- /// children for different purposes.
- pub children: Vec<FlowChild>,
-}
+#[derive(Hash)]
+pub struct FlowNode(pub Vec<FlowChild>);
impl Layout for FlowNode {
fn layout(
@@ -56,19 +20,54 @@ impl Layout for FlowNode {
}
}
+impl Debug for FlowNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Flow ")?;
+ f.debug_list().entries(&self.0).finish()
+ }
+}
+
/// A child of a flow node.
#[derive(Hash)]
pub enum FlowChild {
+ /// A paragraph/block break.
+ Break(Styles),
/// Vertical spacing between other children.
- Spacing(Spacing),
+ Spacing(SpacingNode),
/// An arbitrary node.
Node(PackedNode),
}
+impl FlowChild {
+ /// A reference to the child's styles.
+ pub fn styles(&self) -> &Styles {
+ match self {
+ Self::Break(styles) => styles,
+ Self::Spacing(node) => &node.styles,
+ Self::Node(node) => &node.styles,
+ }
+ }
+
+ /// A mutable reference to the child's styles.
+ pub fn styles_mut(&mut self) -> &mut Styles {
+ match self {
+ Self::Break(styles) => styles,
+ Self::Spacing(node) => &mut node.styles,
+ Self::Node(node) => &mut node.styles,
+ }
+ }
+}
+
impl Debug for FlowChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
- Self::Spacing(spacing) => spacing.fmt(f),
+ Self::Break(styles) => {
+ if f.alternate() {
+ styles.fmt(f)?;
+ }
+ write!(f, "Break")
+ }
+ Self::Spacing(node) => node.fmt(f),
Self::Node(node) => node.fmt(f),
}
}
@@ -118,7 +117,7 @@ impl<'a> FlowLayouter<'a> {
regions.expand.y = false;
Self {
- children: &flow.children,
+ children: &flow.0,
expand,
full,
regions,
@@ -132,15 +131,21 @@ impl<'a> FlowLayouter<'a> {
/// Layout all children.
fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
for child in self.children {
- match *child {
- FlowChild::Spacing(Spacing::Linear(v)) => {
- self.layout_absolute(v);
- }
- FlowChild::Spacing(Spacing::Fractional(v)) => {
- self.items.push(FlowItem::Fractional(v));
- self.fr += v;
+ match child {
+ FlowChild::Break(styles) => {
+ let chain = styles.chain(&ctx.styles);
+ let em = chain.get(TextNode::SIZE).abs;
+ let amount = chain.get(ParNode::SPACING).resolve(em);
+ self.layout_absolute(amount.into());
}
- FlowChild::Node(ref node) => {
+ FlowChild::Spacing(node) => match node.kind {
+ SpacingKind::Linear(v) => self.layout_absolute(v),
+ SpacingKind::Fractional(v) => {
+ self.items.push(FlowItem::Fractional(v));
+ self.fr += v;
+ }
+ },
+ FlowChild::Node(node) => {
if self.regions.is_full() {
self.finish_region();
}
@@ -166,18 +171,25 @@ impl<'a> FlowLayouter<'a> {
/// Layout a node.
fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode) {
+ // Placed nodes that are out of flow produce placed items which aren't
+ // aligned later.
if let Some(placed) = node.downcast::<PlacedNode>() {
- let frame = node.layout(ctx, &self.regions).remove(0);
if placed.out_of_flow() {
+ let frame = node.layout(ctx, &self.regions).remove(0);
self.items.push(FlowItem::Placed(frame.item));
return;
}
}
+ // How to align the node.
let aligns = Spec::new(
// For non-expanding paragraphs it is crucial that we align the
// whole paragraph according to its internal alignment.
- node.downcast::<ParNode>().map_or(Align::Left, |par| par.align),
+ if node.is::<ParNode>() {
+ node.styles.chain(&ctx.styles).get(ParNode::ALIGN)
+ } else {
+ Align::Left
+ },
// Vertical align node alignment is respected by the flow node.
node.downcast::<AlignNode>()
.and_then(|aligned| aligned.aligns.y)
@@ -235,7 +247,7 @@ impl<'a> FlowLayouter<'a> {
output.push_frame(pos, frame);
}
FlowItem::Placed(frame) => {
- output.push_frame(Point::with_y(offset), frame);
+ output.push_frame(Point::zero(), frame);
}
}
}
diff --git a/src/library/grid.rs b/src/library/grid.rs
index 7a9d88c3..d341cf5b 100644
--- a/src/library/grid.rs
+++ b/src/library/grid.rs
@@ -10,7 +10,7 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Value::Relative(v) => vec![TrackSizing::Linear(v.into())],
Value::Linear(v) => vec![TrackSizing::Linear(v)],
Value::Fractional(v) => vec![TrackSizing::Fractional(v)],
- Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize],
+ Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?],
Value::Array(values) => values
.into_iter()
.filter_map(|v| v.cast().ok())
@@ -39,15 +39,11 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
row_gutter.unwrap_or(base_gutter),
);
- let children: Vec<Template> = args.all().collect();
-
- Ok(Value::Template(Template::from_block(move |style| {
- GridNode {
- tracks: tracks.clone(),
- gutter: gutter.clone(),
- children: children.iter().map(|child| child.pack(style)).collect(),
- }
- })))
+ Ok(Value::block(GridNode {
+ tracks,
+ gutter,
+ children: args.all().map(Node::into_block).collect(),
+ }))
}
/// A node that arranges its children in a grid.
diff --git a/src/library/heading.rs b/src/library/heading.rs
new file mode 100644
index 00000000..96ff2688
--- /dev/null
+++ b/src/library/heading.rs
@@ -0,0 +1,63 @@
+use super::prelude::*;
+use super::{FontFamily, TextNode};
+
+/// A section heading.
+#[derive(Debug, Hash)]
+pub struct HeadingNode {
+ /// The node that produces the heading's contents.
+ pub child: PackedNode,
+ /// The logical nesting depth of the section, starting from one. In the
+ /// default style, this controls the text size of the heading.
+ pub level: usize,
+}
+
+#[properties]
+impl HeadingNode {
+ /// The heading's font family.
+ pub const FAMILY: Smart<String> = Smart::Auto;
+ /// The fill color of heading in the text. Just the surrounding text color
+ /// if `auto`.
+ pub const FILL: Smart<Paint> = Smart::Auto;
+}
+
+impl Construct for HeadingNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::block(Self {
+ child: args.expect::<Node>("body")?.into_block(),
+ level: args.named("level")?.unwrap_or(1),
+ }))
+ }
+}
+
+impl Set for HeadingNode {
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ styles.set_opt(Self::FAMILY, args.named("family")?);
+ styles.set_opt(Self::FILL, args.named("fill")?);
+ Ok(())
+ }
+}
+
+impl Layout for HeadingNode {
+ fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ let upscale = (1.6 - 0.1 * self.level as f64).max(0.75);
+ ctx.styles.set(TextNode::STRONG, true);
+ ctx.styles.set(TextNode::SIZE, Relative::new(upscale).into());
+
+ if let Smart::Custom(family) = ctx.styles.get_ref(Self::FAMILY) {
+ let list: Vec<_> = std::iter::once(FontFamily::named(family))
+ .chain(ctx.styles.get_ref(TextNode::FAMILY_LIST).iter().cloned())
+ .collect();
+ ctx.styles.set(TextNode::FAMILY_LIST, list);
+ }
+
+ if let Smart::Custom(fill) = ctx.styles.get(Self::FILL) {
+ ctx.styles.set(TextNode::FILL, fill);
+ }
+
+ self.child.layout(ctx, regions)
+ }
+}
diff --git a/src/library/image.rs b/src/library/image.rs
index 08ed5069..efb246a1 100644
--- a/src/library/image.rs
+++ b/src/library/image.rs
@@ -1,6 +1,7 @@
use std::io;
use super::prelude::*;
+use super::LinkNode;
use crate::diag::Error;
use crate::image::ImageId;
@@ -20,9 +21,9 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})
})?;
- Ok(Value::Template(Template::from_inline(move |_| {
- ImageNode { id, fit }.pack().sized(Spec::new(width, height))
- })))
+ Ok(Value::inline(
+ ImageNode { id, fit }.pack().sized(Spec::new(width, height)),
+ ))
}
/// An image node.
@@ -85,6 +86,11 @@ impl Layout for ImageNode {
frame.clip();
}
+ // Apply link if it exists.
+ if let Some(url) = ctx.styles.get_ref(LinkNode::URL) {
+ frame.link(url);
+ }
+
vec![frame.constrain(Constraints::tight(regions))]
}
}
diff --git a/src/library/link.rs b/src/library/link.rs
new file mode 100644
index 00000000..40604a62
--- /dev/null
+++ b/src/library/link.rs
@@ -0,0 +1,28 @@
+use super::prelude::*;
+use crate::util::EcoString;
+
+/// `link`: Link text or other elements.
+pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let url: String = args.expect::<EcoString>("url")?.into();
+ let body = args.find().unwrap_or_else(|| {
+ let mut text = url.as_str();
+ for prefix in ["mailto:", "tel:"] {
+ text = text.trim_start_matches(prefix);
+ }
+ Node::Text(text.into())
+ });
+
+ Ok(Value::Node(
+ body.styled(Styles::one(LinkNode::URL, Some(url))),
+ ))
+}
+
+/// Host for link styles.
+#[derive(Debug, Hash)]
+pub struct LinkNode;
+
+#[properties]
+impl LinkNode {
+ /// An URL to link to.
+ pub const URL: Option<String> = None;
+}
diff --git a/src/library/list.rs b/src/library/list.rs
new file mode 100644
index 00000000..25eb3600
--- /dev/null
+++ b/src/library/list.rs
@@ -0,0 +1,102 @@
+use std::hash::Hash;
+
+use super::prelude::*;
+use super::{GridNode, TextNode, TrackSizing};
+
+/// An unordered or ordered list.
+#[derive(Debug, Hash)]
+pub struct ListNode<L> {
+ /// The node that produces the item's body.
+ pub child: PackedNode,
+ /// The list labelling style -- unordered or ordered.
+ pub labelling: L,
+}
+
+#[properties]
+impl<L: Labelling> ListNode<L> {
+ /// The indentation of each item's label.
+ pub const LABEL_INDENT: Linear = Relative::new(0.0).into();
+ /// The space between the label and the body of each item.
+ pub const BODY_INDENT: Linear = Relative::new(0.5).into();
+}
+
+impl<L: Labelling> Construct for ListNode<L> {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(args
+ .all()
+ .map(|node: Node| {
+ Node::block(Self {
+ child: node.into_block(),
+ labelling: L::default(),
+ })
+ })
+ .sum())
+ }
+}
+
+impl<L: Labelling> Set for ListNode<L> {
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?);
+ styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?);
+ Ok(())
+ }
+}
+
+impl<L: Labelling> Layout for ListNode<L> {
+ fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ let em = ctx.styles.get(TextNode::SIZE).abs;
+ let label_indent = ctx.styles.get(Self::LABEL_INDENT).resolve(em);
+ let body_indent = ctx.styles.get(Self::BODY_INDENT).resolve(em);
+
+ let columns = vec![
+ TrackSizing::Linear(label_indent.into()),
+ TrackSizing::Auto,
+ TrackSizing::Linear(body_indent.into()),
+ TrackSizing::Auto,
+ ];
+
+ let children = vec![
+ PackedNode::default(),
+ Node::Text(self.labelling.label()).into_block(),
+ PackedNode::default(),
+ self.child.clone(),
+ ];
+
+ GridNode {
+ tracks: Spec::new(columns, vec![]),
+ gutter: Spec::default(),
+ children,
+ }
+ .layout(ctx, regions)
+ }
+}
+
+/// How to label a list.
+pub trait Labelling: Debug + Default + Hash + 'static {
+ /// Return the item's label.
+ fn label(&self) -> EcoString;
+}
+
+/// Unordered list labelling style.
+#[derive(Debug, Default, Hash)]
+pub struct Unordered;
+
+impl Labelling for Unordered {
+ fn label(&self) -> EcoString {
+ '•'.into()
+ }
+}
+
+/// Ordered list labelling style.
+#[derive(Debug, Default, Hash)]
+pub struct Ordered(pub Option<usize>);
+
+impl Labelling for Ordered {
+ fn label(&self) -> EcoString {
+ format_eco!("{}.", self.0.unwrap_or(1))
+ }
+}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index d60a13ea..b2dd0dbe 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -4,11 +4,12 @@
//! definitions.
mod align;
-mod deco;
-mod document;
mod flow;
mod grid;
+mod heading;
mod image;
+mod link;
+mod list;
mod pad;
mod page;
mod par;
@@ -23,10 +24,15 @@ mod utility;
/// Helpful imports for creating library functionality.
mod prelude {
+ pub use std::fmt::{self, Debug, Formatter};
pub use std::rc::Rc;
+ pub use typst_macros::properties;
+
pub use crate::diag::{At, TypResult};
- pub use crate::eval::{Args, EvalContext, Smart, Template, Value};
+ pub use crate::eval::{
+ Args, Construct, EvalContext, Node, Property, Set, Smart, Styles, Value,
+ };
pub use crate::frame::*;
pub use crate::geom::*;
pub use crate::layout::*;
@@ -36,10 +42,11 @@ mod prelude {
pub use self::image::*;
pub use align::*;
-pub use deco::*;
-pub use document::*;
pub use flow::*;
pub use grid::*;
+pub use heading::*;
+pub use link::*;
+pub use list::*;
pub use pad::*;
pub use page::*;
pub use par::*;
@@ -54,28 +61,38 @@ pub use utility::*;
use crate::eval::{Scope, Value};
use crate::geom::*;
-use crate::style::FontFamily;
/// Construct a scope containing all standard library definitions.
pub fn new() -> Scope {
let mut std = Scope::new();
- // Text.
- std.def_func("font", font);
- std.def_func("par", par);
+ // Classes.
+ std.def_class::<PageNode>("page");
+ std.def_class::<ParNode>("par");
+ std.def_class::<TextNode>("text");
+ std.def_class::<HeadingNode>("heading");
+ std.def_class::<ListNode<Unordered>>("list");
+ std.def_class::<ListNode<Ordered>>("enum");
+
+ // Text functions.
+ // TODO(style): These should be classes, once that works for inline nodes.
std.def_func("strike", strike);
std.def_func("underline", underline);
std.def_func("overline", overline);
std.def_func("link", link);
- // Layout.
- std.def_func("page", page);
+ // Break and spacing functions.
std.def_func("pagebreak", pagebreak);
+ std.def_func("parbreak", parbreak);
+ std.def_func("linebreak", linebreak);
std.def_func("h", h);
std.def_func("v", v);
+
+ // Layout functions.
+ // TODO(style): Decide which of these should be classes
+ // (and which of their properties should be settable).
std.def_func("box", box_);
std.def_func("block", block);
- std.def_func("flow", flow);
std.def_func("stack", stack);
std.def_func("grid", grid);
std.def_func("pad", pad);
@@ -84,15 +101,13 @@ pub fn new() -> Scope {
std.def_func("move", move_);
std.def_func("scale", scale);
std.def_func("rotate", rotate);
-
- // Elements.
std.def_func("image", image);
std.def_func("rect", rect);
std.def_func("square", square);
std.def_func("ellipse", ellipse);
std.def_func("circle", circle);
- // Utility.
+ // Utility functions.
std.def_func("assert", assert);
std.def_func("type", type_);
std.def_func("repr", repr);
@@ -110,14 +125,15 @@ pub fn new() -> Scope {
std.def_func("len", len);
std.def_func("sorted", sorted);
- // Colors.
+ // Predefined colors.
+ // TODO: More colors.
std.def_const("white", RgbaColor::WHITE);
std.def_const("black", RgbaColor::BLACK);
std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF));
std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
- // Arbitrary constants.
+ // Other constants.
std.def_const("ltr", Dir::LTR);
std.def_const("rtl", Dir::RTL);
std.def_const("ttb", Dir::TTB);
@@ -139,17 +155,20 @@ dynamic! {
Dir: "direction",
}
-dynamic! {
- Align: "alignment",
+castable! {
+ Paint,
+ Expected: "color",
+ Value::Color(color) => Paint::Solid(color),
}
-dynamic! {
- FontFamily: "font family",
- Value::Str(string) => Self::Named(string.to_lowercase()),
+castable! {
+ usize,
+ Expected: "non-negative integer",
+ Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?,
}
castable! {
- Paint,
- Expected: "color",
- Value::Color(color) => Paint::Solid(color),
+ String,
+ Expected: "string",
+ Value::Str(string) => string.into(),
}
diff --git a/src/library/pad.rs b/src/library/pad.rs
index 681da73e..75fea2e5 100644
--- a/src/library/pad.rs
+++ b/src/library/pad.rs
@@ -7,7 +7,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let top = args.named("top")?;
let right = args.named("right")?;
let bottom = args.named("bottom")?;
- let body: Template = args.expect("body")?;
+ let body: Node = args.expect("body")?;
let padding = Sides::new(
left.or(all).unwrap_or_default(),
top.or(all).unwrap_or_default(),
@@ -15,9 +15,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
bottom.or(all).unwrap_or_default(),
);
- Ok(Value::Template(Template::from_block(move |style| {
- body.pack(style).padded(padding)
- })))
+ Ok(Value::block(body.into_block().padded(padding)))
}
/// A node that adds padding to its child.
diff --git a/src/library/page.rs b/src/library/page.rs
index 0d29ddb6..0e690770 100644
--- a/src/library/page.rs
+++ b/src/library/page.rs
@@ -1,115 +1,405 @@
-use super::prelude::*;
-use crate::style::{Paper, PaperClass};
-
-/// `page`: Configure pages.
-pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- castable! {
- Paper,
- Expected: "string",
- Value::Str(string) => Paper::from_name(&string).ok_or("unknown paper")?,
- }
-
- let paper = args.named::<Paper>("paper")?.or_else(|| args.find());
- let width = args.named::<Smart<_>>("width")?;
- let height = args.named::<Smart<_>>("height")?;
- let flip = args.named("flip")?;
- let margins = args.named("margins")?;
- let left = args.named("left")?;
- let top = args.named("top")?;
- let right = args.named("right")?;
- let bottom = args.named("bottom")?;
- let fill = args.named("fill")?;
-
- ctx.template.modify(move |style| {
- let page = style.page_mut();
-
- if let Some(paper) = paper {
- page.class = paper.class();
- page.size = paper.size();
- }
+#![allow(unused)]
- if let Some(width) = width {
- page.class = PaperClass::Custom;
- page.size.x = width.unwrap_or(Length::inf());
- }
+use std::fmt::{self, Display, Formatter};
+use std::str::FromStr;
- if let Some(height) = height {
- page.class = PaperClass::Custom;
- page.size.y = height.unwrap_or(Length::inf());
- }
+use super::prelude::*;
+use super::PadNode;
- if flip.unwrap_or(false) {
- std::mem::swap(&mut page.size.x, &mut page.size.y);
- }
+/// `pagebreak`: Start a new page.
+pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
+ Ok(Value::Node(Node::Pagebreak))
+}
- if let Some(margins) = margins {
- page.margins = Sides::splat(margins);
- }
+/// Layouts its child onto one or multiple pages.
+#[derive(Clone, PartialEq, Hash)]
+pub struct PageNode {
+ /// The node producing the content.
+ pub child: PackedNode,
+ /// The page's styles.
+ pub styles: Styles,
+}
- if let Some(left) = left {
- page.margins.left = left;
- }
+#[properties]
+impl PageNode {
+ /// The unflipped width of the page.
+ pub const WIDTH: Smart<Length> = Smart::Custom(Paper::default().width());
+ /// The unflipped height of the page.
+ pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::default().height());
+ /// The class of paper. Defines the default margins.
+ pub const CLASS: PaperClass = Paper::default().class();
+ /// Whether the page is flipped into landscape orientation.
+ pub const FLIPPED: bool = false;
+ /// The left margin.
+ pub const LEFT: Smart<Linear> = Smart::Auto;
+ /// The right margin.
+ pub const RIGHT: Smart<Linear> = Smart::Auto;
+ /// The top margin.
+ pub const TOP: Smart<Linear> = Smart::Auto;
+ /// The bottom margin.
+ pub const BOTTOM: Smart<Linear> = Smart::Auto;
+ /// The page's background color.
+ pub const FILL: Option<Paint> = None;
+}
- if let Some(top) = top {
- page.margins.top = top;
- }
+impl Construct for PageNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::Page(Self {
+ child: args.expect::<Node>("body")?.into_block(),
+ styles: Styles::new(),
+ }))
+ }
+}
- if let Some(right) = right {
- page.margins.right = right;
+impl Set for PageNode {
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
+ styles.set(Self::CLASS, paper.class());
+ styles.set(Self::WIDTH, Smart::Custom(paper.width()));
+ styles.set(Self::HEIGHT, Smart::Custom(paper.height()));
}
- if let Some(bottom) = bottom {
- page.margins.bottom = bottom;
+ if let Some(width) = args.named("width")? {
+ styles.set(Self::CLASS, PaperClass::Custom);
+ styles.set(Self::WIDTH, width);
}
- if let Some(fill) = fill {
- page.fill = fill;
+ if let Some(height) = args.named("height")? {
+ styles.set(Self::CLASS, PaperClass::Custom);
+ styles.set(Self::HEIGHT, height);
}
- });
-
- ctx.template.pagebreak(false);
-
- Ok(Value::None)
-}
-/// `pagebreak`: Start a new page.
-pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.pagebreak(true);
- Ok(Value::Template(template))
-}
+ let margins = args.named("margins")?;
+ styles.set_opt(Self::FLIPPED, args.named("flipped")?);
+ styles.set_opt(Self::LEFT, args.named("left")?.or(margins));
+ styles.set_opt(Self::TOP, args.named("top")?.or(margins));
+ styles.set_opt(Self::RIGHT, args.named("right")?.or(margins));
+ styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(margins));
+ styles.set_opt(Self::FILL, args.named("fill")?);
-/// Layouts its children onto one or multiple pages.
-#[derive(Debug, Hash)]
-pub struct PageNode {
- /// The size of the page.
- pub size: Size,
- /// The background fill.
- pub fill: Option<Paint>,
- /// The node that produces the actual pages.
- pub child: PackedNode,
+ Ok(())
+ }
}
impl PageNode {
+ /// Style the node with styles from a style map.
+ pub fn styled(mut self, styles: Styles) -> Self {
+ self.styles.apply(&styles);
+ self
+ }
+
/// Layout the page run into a sequence of frames, one per page.
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
+ let prev = ctx.styles.clone();
+ ctx.styles = self.styles.chain(&ctx.styles);
+
// When one of the lengths is infinite the page fits its content along
// that axis.
- let expand = self.size.map(Length::is_finite);
- let regions = Regions::repeat(self.size, self.size, expand);
+ let width = ctx.styles.get(Self::WIDTH).unwrap_or(Length::inf());
+ let height = ctx.styles.get(Self::HEIGHT).unwrap_or(Length::inf());
+ let mut size = Size::new(width, height);
+ if ctx.styles.get(Self::FLIPPED) {
+ std::mem::swap(&mut size.x, &mut size.y);
+ }
+
+ // Determine the margins.
+ let class = ctx.styles.get(Self::CLASS);
+ let default = class.default_margins();
+ let padding = Sides {
+ left: ctx.styles.get(Self::LEFT).unwrap_or(default.left),
+ right: ctx.styles.get(Self::RIGHT).unwrap_or(default.right),
+ top: ctx.styles.get(Self::TOP).unwrap_or(default.top),
+ bottom: ctx.styles.get(Self::BOTTOM).unwrap_or(default.bottom),
+ };
+
+ // Pad the child.
+ let padded = PadNode { child: self.child.clone(), padding }.pack();
// Layout the child.
+ let expand = size.map(Length::is_finite);
+ let regions = Regions::repeat(size, size, expand);
let mut frames: Vec<_> =
- self.child.layout(ctx, &regions).into_iter().map(|c| c.item).collect();
+ padded.layout(ctx, &regions).into_iter().map(|c| c.item).collect();
// Add background fill if requested.
- if let Some(fill) = self.fill {
+ if let Some(fill) = ctx.styles.get(Self::FILL) {
for frame in &mut frames {
let shape = Shape::filled(Geometry::Rect(frame.size), fill);
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
}
}
+ ctx.styles = prev;
frames
}
}
+
+impl Debug for PageNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ self.styles.fmt(f)?;
+ }
+ f.write_str("Page(")?;
+ self.child.fmt(f)?;
+ f.write_str(")")
+ }
+}
+
+/// Specification of a paper.
+#[derive(Debug, Copy, Clone)]
+pub struct Paper {
+ /// The broad class this paper belongs to.
+ class: PaperClass,
+ /// The width of the paper in millimeters.
+ width: f64,
+ /// The height of the paper in millimeters.
+ height: f64,
+}
+
+impl Paper {
+ /// The class of the paper.
+ pub fn class(self) -> PaperClass {
+ self.class
+ }
+
+ /// The width of the paper.
+ pub fn width(self) -> Length {
+ Length::mm(self.width)
+ }
+
+ /// The height of the paper.
+ pub fn height(self) -> Length {
+ Length::mm(self.height)
+ }
+}
+
+impl Default for Paper {
+ fn default() -> Self {
+ Paper::A4
+ }
+}
+
+castable! {
+ Paper,
+ Expected: "string",
+ Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?,
+}
+
+/// Defines default margins for a class of related papers.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum PaperClass {
+ Custom,
+ Base,
+ US,
+ Newspaper,
+ Book,
+}
+
+impl PaperClass {
+ /// The default margins for this page class.
+ fn default_margins(self) -> Sides<Linear> {
+ let f = |r| Relative::new(r).into();
+ let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b));
+ match self {
+ Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842),
+ Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842),
+ Self::US => s(0.1760, 0.1092, 0.1760, 0.0910),
+ Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294),
+ Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965),
+ }
+ }
+}
+
+/// Defines paper constants and a paper parsing implementation.
+macro_rules! papers {
+ ($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => {
+ /// Predefined papers.
+ ///
+ /// Each paper is parsable from its name in kebab-case.
+ impl Paper {
+ $(pub const $var: Self = Self {
+ class: PaperClass::$class,
+ width: $width,
+ height: $height,
+ };)*
+ }
+
+ impl FromStr for Paper {
+ type Err = ParsePaperError;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ match name.to_lowercase().as_str() {
+ $($($pats)* => Ok(Self::$var),)*
+ _ => Err(ParsePaperError),
+ }
+ }
+ }
+
+ /// The error when parsing a [`Paper`] from a string fails.
+ #[derive(Debug, Copy, Clone, Eq, PartialEq)]
+ pub struct ParsePaperError;
+
+ impl Display for ParsePaperError {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad("invalid paper name")
+ }
+ }
+
+ impl std::error::Error for ParsePaperError {}
+ };
+}
+
+// All paper sizes in mm.
+//
+// Resources:
+// - https://papersizes.io/
+// - https://en.wikipedia.org/wiki/Paper_size
+// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
+// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
+papers! {
+ // ---------------------------------------------------------------------- //
+ // ISO 216 A Series
+ (A0: Base, 841.0, 1189.0, "a0")
+ (A1: Base, 594.0, 841.0, "a1")
+ (A2: Base, 420.0, 594.0, "a2")
+ (A3: Base, 297.0, 420.0, "a3")
+ (A4: Base, 210.0, 297.0, "a4")
+ (A5: Base, 148.0, 210.0, "a5")
+ (A6: Book, 105.0, 148.0, "a6")
+ (A7: Base, 74.0, 105.0, "a7")
+ (A8: Base, 52.0, 74.0, "a8")
+ (A9: Base, 37.0, 52.0, "a9")
+ (A10: Base, 26.0, 37.0, "a10")
+ (A11: Base, 18.0, 26.0, "a11")
+
+ // ISO 216 B Series
+ (ISO_B1: Base, 707.0, 1000.0, "iso-b1")
+ (ISO_B2: Base, 500.0, 707.0, "iso-b2")
+ (ISO_B3: Base, 353.0, 500.0, "iso-b3")
+ (ISO_B4: Base, 250.0, 353.0, "iso-b4")
+ (ISO_B5: Book, 176.0, 250.0, "iso-b5")
+ (ISO_B6: Book, 125.0, 176.0, "iso-b6")
+ (ISO_B7: Base, 88.0, 125.0, "iso-b7")
+ (ISO_B8: Base, 62.0, 88.0, "iso-b8")
+
+ // ISO 216 C Series
+ (ISO_C3: Base, 324.0, 458.0, "iso-c3")
+ (ISO_C4: Base, 229.0, 324.0, "iso-c4")
+ (ISO_C5: Base, 162.0, 229.0, "iso-c5")
+ (ISO_C6: Base, 114.0, 162.0, "iso-c6")
+ (ISO_C7: Base, 81.0, 114.0, "iso-c7")
+ (ISO_C8: Base, 57.0, 81.0, "iso-c8")
+
+ // DIN D Series (extension to ISO)
+ (DIN_D3: Base, 272.0, 385.0, "din-d3")
+ (DIN_D4: Base, 192.0, 272.0, "din-d4")
+ (DIN_D5: Base, 136.0, 192.0, "din-d5")
+ (DIN_D6: Base, 96.0, 136.0, "din-d6")
+ (DIN_D7: Base, 68.0, 96.0, "din-d7")
+ (DIN_D8: Base, 48.0, 68.0, "din-d8")
+
+ // SIS (used in academia)
+ (SIS_G5: Base, 169.0, 239.0, "sis-g5")
+ (SIS_E5: Base, 115.0, 220.0, "sis-e5")
+
+ // ANSI Extensions
+ (ANSI_A: Base, 216.0, 279.0, "ansi-a")
+ (ANSI_B: Base, 279.0, 432.0, "ansi-b")
+ (ANSI_C: Base, 432.0, 559.0, "ansi-c")
+ (ANSI_D: Base, 559.0, 864.0, "ansi-d")
+ (ANSI_E: Base, 864.0, 1118.0, "ansi-e")
+
+ // ANSI Architectural Paper
+ (ARCH_A: Base, 229.0, 305.0, "arch-a")
+ (ARCH_B: Base, 305.0, 457.0, "arch-b")
+ (ARCH_C: Base, 457.0, 610.0, "arch-c")
+ (ARCH_D: Base, 610.0, 914.0, "arch-d")
+ (ARCH_E1: Base, 762.0, 1067.0, "arch-e1")
+ (ARCH_E: Base, 914.0, 1219.0, "arch-e")
+
+ // JIS B Series
+ (JIS_B0: Base, 1030.0, 1456.0, "jis-b0")
+ (JIS_B1: Base, 728.0, 1030.0, "jis-b1")
+ (JIS_B2: Base, 515.0, 728.0, "jis-b2")
+ (JIS_B3: Base, 364.0, 515.0, "jis-b3")
+ (JIS_B4: Base, 257.0, 364.0, "jis-b4")
+ (JIS_B5: Base, 182.0, 257.0, "jis-b5")
+ (JIS_B6: Base, 128.0, 182.0, "jis-b6")
+ (JIS_B7: Base, 91.0, 128.0, "jis-b7")
+ (JIS_B8: Base, 64.0, 91.0, "jis-b8")
+ (JIS_B9: Base, 45.0, 64.0, "jis-b9")
+ (JIS_B10: Base, 32.0, 45.0, "jis-b10")
+ (JIS_B11: Base, 22.0, 32.0, "jis-b11")
+
+ // SAC D Series
+ (SAC_D0: Base, 764.0, 1064.0, "sac-d0")
+ (SAC_D1: Base, 532.0, 760.0, "sac-d1")
+ (SAC_D2: Base, 380.0, 528.0, "sac-d2")
+ (SAC_D3: Base, 264.0, 376.0, "sac-d3")
+ (SAC_D4: Base, 188.0, 260.0, "sac-d4")
+ (SAC_D5: Base, 130.0, 184.0, "sac-d5")
+ (SAC_D6: Base, 92.0, 126.0, "sac-d6")
+
+ // ISO 7810 ID
+ (ISO_ID_1: Base, 85.6, 53.98, "iso-id-1")
+ (ISO_ID_2: Base, 74.0, 105.0, "iso-id-2")
+ (ISO_ID_3: Base, 88.0, 125.0, "iso-id-3")
+
+ // ---------------------------------------------------------------------- //
+ // Asia
+ (ASIA_F4: Base, 210.0, 330.0, "asia-f4")
+
+ // Japan
+ (JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4")
+ (JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5")
+ (JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6")
+ (JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4")
+ (JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5")
+ (JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card")
+
+ // China
+ (CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card")
+
+ // Europe
+ (EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card")
+
+ // French Traditional (AFNOR)
+ (FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière")
+ (FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture")
+ (FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition")
+ (FR_RAISIN: Base, 500.0, 650.0, "fr-raisin")
+ (FR_CARRE: Base, 450.0, 560.0, "fr-carré")
+ (FR_JESUS: Base, 560.0, 760.0, "fr-jésus")
+
+ // United Kingdom Imperial
+ (UK_BRIEF: Base, 406.4, 342.9, "uk-brief")
+ (UK_DRAFT: Base, 254.0, 406.4, "uk-draft")
+ (UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap")
+ (UK_QUARTO: Base, 203.2, 254.0, "uk-quarto")
+ (UK_CROWN: Base, 508.0, 381.0, "uk-crown")
+ (UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a")
+ (UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b")
+
+ // Unites States
+ (US_LETTER: US, 215.9, 279.4, "us-letter")
+ (US_LEGAL: US, 215.9, 355.6, "us-legal")
+ (US_TABLOID: US, 279.4, 431.8, "us-tabloid")
+ (US_EXECUTIVE: US, 184.15, 266.7, "us-executive")
+ (US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio")
+ (US_STATEMENT: US, 139.7, 215.9, "us-statement")
+ (US_LEDGER: US, 431.8, 279.4, "us-ledger")
+ (US_OFICIO: US, 215.9, 340.36, "us-oficio")
+ (US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter")
+ (US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal")
+ (US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card")
+ (US_DIGEST: Book, 139.7, 215.9, "us-digest")
+ (US_TRADE: Book, 152.4, 228.6, "us-trade")
+
+ // ---------------------------------------------------------------------- //
+ // Other
+ (NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact")
+ (NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner")
+ (NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet")
+ (PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9")
+ (PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3")
+}
diff --git a/src/library/par.rs b/src/library/par.rs
index 6abfa7af..26280d8e 100644
--- a/src/library/par.rs
+++ b/src/library/par.rs
@@ -6,75 +6,81 @@ use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
use super::prelude::*;
-use super::{shape, Decoration, ShapedText, Spacing};
-use crate::style::TextStyle;
+use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode};
use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
-/// `par`: Configure paragraphs.
-pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let spacing = args.named("spacing")?;
- let leading = args.named("leading")?;
-
- let mut dir =
- args.named("lang")?
- .map(|iso: EcoString| match iso.to_lowercase().as_str() {
- "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
- "en" | "fr" | "de" => Dir::LTR,
- _ => Dir::LTR,
- });
-
- if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
- if v.axis() != SpecAxis::Horizontal {
- bail!(span, "must be horizontal");
- }
- dir = Some(v);
- }
+/// `parbreak`: Start a new paragraph.
+pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
+ Ok(Value::Node(Node::Parbreak))
+}
- let mut align = None;
- if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? {
- if v.axis() != SpecAxis::Horizontal {
- bail!(span, "must be horizontal");
- }
- align = Some(v);
- }
+/// `linebreak`: Start a new line.
+pub fn linebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
+ Ok(Value::Node(Node::Linebreak))
+}
- ctx.template.modify(move |style| {
- let par = style.par_mut();
+/// A node that arranges its children into a paragraph.
+#[derive(Hash)]
+pub struct ParNode(pub Vec<ParChild>);
- if let Some(dir) = dir {
- par.dir = dir;
- par.align = if dir == Dir::LTR { Align::Left } else { Align::Right };
- }
+#[properties]
+impl ParNode {
+ /// The direction for text and inline objects.
+ pub const DIR: Dir = Dir::LTR;
+ /// How to align text and inline objects in their line.
+ pub const ALIGN: Align = Align::Left;
+ /// The spacing between lines (dependent on scaled font size).
+ pub const LEADING: Linear = Relative::new(0.65).into();
+ /// The spacing between paragraphs (dependent on scaled font size).
+ pub const SPACING: Linear = Relative::new(1.2).into();
+}
- if let Some(align) = align {
- par.align = align;
- }
+impl Construct for ParNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ // Lift to a block so that it doesn't merge with adjacent stuff.
+ Ok(Node::Block(args.expect::<Node>("body")?.into_block()))
+ }
+}
- if let Some(leading) = leading {
- par.leading = leading;
+impl Set for ParNode {
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ let spacing = args.named("spacing")?;
+ let leading = args.named("leading")?;
+
+ let mut dir =
+ args.named("lang")?
+ .map(|iso: EcoString| match iso.to_lowercase().as_str() {
+ "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
+ "en" | "fr" | "de" => Dir::LTR,
+ _ => Dir::LTR,
+ });
+
+ if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
+ if v.axis() != SpecAxis::Horizontal {
+ bail!(span, "must be horizontal");
+ }
+ dir = Some(v);
}
- if let Some(spacing) = spacing {
- par.spacing = spacing;
+ let mut align = None;
+ if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? {
+ if v.axis() != SpecAxis::Horizontal {
+ bail!(span, "must be horizontal");
+ }
+ align = Some(v);
}
- });
- ctx.template.parbreak();
+ if let (Some(dir), None) = (dir, align) {
+ align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
+ }
- Ok(Value::None)
-}
+ styles.set_opt(Self::DIR, dir);
+ styles.set_opt(Self::ALIGN, align);
+ styles.set_opt(Self::LEADING, leading);
+ styles.set_opt(Self::SPACING, spacing);
-/// A node that arranges its children into a paragraph.
-#[derive(Debug, Hash)]
-pub struct ParNode {
- /// The text direction (either LTR or RTL).
- pub dir: Dir,
- /// How to align text in its line.
- pub align: Align,
- /// The spacing to insert between each line.
- pub leading: Length,
- /// The children to be arranged in a paragraph.
- pub children: Vec<ParChild>,
+ Ok(())
+ }
}
impl Layout for ParNode {
@@ -87,7 +93,7 @@ impl Layout for ParNode {
let text = self.collect_text();
// Find out the BiDi embedding levels.
- let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
+ let bidi = BidiInfo::new(&text, Level::from_dir(ctx.styles.get(Self::DIR)));
// Prepare paragraph layout by building a representation on which we can
// do line breaking without layouting each and every line from scratch.
@@ -123,38 +129,63 @@ impl ParNode {
/// The string representation of each child.
fn strings(&self) -> impl Iterator<Item = &str> {
- self.children.iter().map(|child| match child {
+ self.0.iter().map(|child| match child {
ParChild::Spacing(_) => " ",
- ParChild::Text(ref piece, ..) => piece,
- ParChild::Node(..) => "\u{FFFC}",
- ParChild::Decorate(_) | ParChild::Undecorate => "",
+ ParChild::Text(ref node) => &node.text,
+ ParChild::Node(_) => "\u{FFFC}",
})
}
}
+impl Debug for ParNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Par ")?;
+ f.debug_list().entries(&self.0).finish()
+ }
+}
+
/// A child of a paragraph node.
#[derive(Hash)]
pub enum ParChild {
/// Spacing between other nodes.
- Spacing(Spacing),
+ Spacing(SpacingNode),
/// A run of text and how to align it in its line.
- Text(EcoString, Rc<TextStyle>),
+ Text(TextNode),
/// Any child node and how to align it in its line.
Node(PackedNode),
- /// A decoration that applies until a matching `Undecorate`.
- Decorate(Decoration),
- /// The end of a decoration.
- Undecorate,
+}
+
+impl ParChild {
+ /// Create a text child.
+ pub fn text(text: impl Into<EcoString>, styles: Styles) -> Self {
+ Self::Text(TextNode { text: text.into(), styles })
+ }
+
+ /// A reference to the child's styles.
+ pub fn styles(&self) -> &Styles {
+ match self {
+ Self::Spacing(node) => &node.styles,
+ Self::Text(node) => &node.styles,
+ Self::Node(node) => &node.styles,
+ }
+ }
+
+ /// A mutable reference to the child's styles.
+ pub fn styles_mut(&mut self) -> &mut Styles {
+ match self {
+ Self::Spacing(node) => &mut node.styles,
+ Self::Text(node) => &mut node.styles,
+ Self::Node(node) => &mut node.styles,
+ }
+ }
}
impl Debug for ParChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
- Self::Spacing(v) => write!(f, "Spacing({:?})", v),
- Self::Text(text, _) => write!(f, "Text({:?})", text),
+ Self::Spacing(node) => node.fmt(f),
+ Self::Text(node) => node.fmt(f),
Self::Node(node) => node.fmt(f),
- Self::Decorate(deco) => write!(f, "Decorate({:?})", deco),
- Self::Undecorate => write!(f, "Undecorate"),
}
}
}
@@ -172,8 +203,6 @@ struct ParLayouter<'a> {
items: Vec<ParItem<'a>>,
/// The ranges of the items in `bidi.text`.
ranges: Vec<Range>,
- /// The decorations and the ranges they span.
- decos: Vec<(Range, &'a Decoration)>,
}
/// Range of a substring of text.
@@ -201,22 +230,22 @@ impl<'a> ParLayouter<'a> {
) -> Self {
let mut items = vec![];
let mut ranges = vec![];
- let mut starts = vec![];
- let mut decos = vec![];
// Layout the children and collect them into items.
- for (range, child) in par.ranges().zip(&par.children) {
- match *child {
- ParChild::Spacing(Spacing::Linear(v)) => {
- let resolved = v.resolve(regions.current.x);
- items.push(ParItem::Absolute(resolved));
- ranges.push(range);
- }
- ParChild::Spacing(Spacing::Fractional(v)) => {
- items.push(ParItem::Fractional(v));
- ranges.push(range);
- }
- ParChild::Text(_, ref style) => {
+ for (range, child) in par.ranges().zip(&par.0) {
+ match child {
+ ParChild::Spacing(node) => match node.kind {
+ SpacingKind::Linear(v) => {
+ let resolved = v.resolve(regions.current.x);
+ items.push(ParItem::Absolute(resolved));
+ ranges.push(range);
+ }
+ SpacingKind::Fractional(v) => {
+ items.push(ParItem::Fractional(v));
+ ranges.push(range);
+ }
+ },
+ ParChild::Text(node) => {
// TODO: Also split by language and script.
let mut cursor = range.start;
for (level, group) in bidi.levels[range].group_by_key(|&lvl| lvl) {
@@ -224,41 +253,27 @@ impl<'a> ParLayouter<'a> {
cursor += group.len();
let subrange = start .. cursor;
let text = &bidi.text[subrange.clone()];
- let shaped = shape(ctx, text, style, level.dir());
+ let styles = node.styles.chain(&ctx.styles);
+ let shaped = shape(&mut ctx.fonts, text, styles, level.dir());
items.push(ParItem::Text(shaped));
ranges.push(subrange);
}
}
- ParChild::Node(ref node) => {
+ ParChild::Node(node) => {
let size = Size::new(regions.current.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false));
let frame = node.layout(ctx, &pod).remove(0);
items.push(ParItem::Frame(Rc::take(frame.item)));
ranges.push(range);
}
- ParChild::Decorate(ref deco) => {
- starts.push((range.start, deco));
- }
- ParChild::Undecorate => {
- if let Some((start, deco)) = starts.pop() {
- decos.push((start .. range.end, deco));
- }
- }
}
}
- for (start, deco) in starts {
- decos.push((start .. bidi.text.len(), deco));
- }
+ let em = ctx.styles.get(TextNode::SIZE).abs;
+ let align = ctx.styles.get(ParNode::ALIGN);
+ let leading = ctx.styles.get(ParNode::LEADING).resolve(em);
- Self {
- align: par.align,
- leading: par.leading,
- bidi,
- items,
- ranges,
- decos,
- }
+ Self { align, leading, bidi, items, ranges }
}
/// Find first-fit line breaks and build the paragraph.
@@ -430,7 +445,7 @@ impl<'a> LineLayout<'a> {
// empty string.
if !range.is_empty() || rest.is_empty() {
// Reshape that part.
- let reshaped = shaped.reshape(ctx, range);
+ let reshaped = shaped.reshape(&mut ctx.fonts, range);
last = Some(ParItem::Text(reshaped));
}
@@ -451,7 +466,7 @@ impl<'a> LineLayout<'a> {
// Reshape if necessary.
if range.len() < shaped.text.len() {
if !range.is_empty() {
- let reshaped = shaped.reshape(ctx, range);
+ let reshaped = shaped.reshape(&mut ctx.fonts, range);
first = Some(ParItem::Text(reshaped));
}
@@ -504,28 +519,19 @@ impl<'a> LineLayout<'a> {
let mut output = Frame::new(size);
output.baseline = Some(self.baseline);
- for (range, item) in self.reordered() {
- let mut position = |mut frame: Frame| {
- // Decorate.
- for (deco_range, deco) in &self.par.decos {
- if deco_range.contains(&range.start) {
- deco.apply(ctx, &mut frame);
- }
- }
-
+ for item in self.reordered() {
+ let mut position = |frame: Frame| {
let x = offset + self.par.align.resolve(remaining);
let y = self.baseline - frame.baseline();
offset += frame.size.x;
-
- // Add to the line's frame.
output.merge_frame(Point::new(x, y), frame);
};
- match *item {
- ParItem::Absolute(v) => offset += v,
+ match item {
+ ParItem::Absolute(v) => offset += *v,
ParItem::Fractional(v) => offset += v.resolve(self.fr, remaining),
- ParItem::Text(ref shaped) => position(shaped.build()),
- ParItem::Frame(ref frame) => position(frame.clone()),
+ ParItem::Text(shaped) => position(shaped.build(&ctx.fonts)),
+ ParItem::Frame(frame) => position(frame.clone()),
}
}
@@ -533,7 +539,7 @@ impl<'a> LineLayout<'a> {
}
/// Iterate through the line's items in visual order.
- fn reordered(&self) -> impl Iterator<Item = (Range, &ParItem<'a>)> {
+ fn reordered(&self) -> impl Iterator<Item = &ParItem<'a>> {
// The bidi crate doesn't like empty lines.
let (levels, runs) = if !self.line.is_empty() {
// Find the paragraph that contains the line.
@@ -548,7 +554,7 @@ impl<'a> LineLayout<'a> {
// Compute the reordered ranges in visual order (left to right).
self.par.bidi.visual_runs(para, self.line.clone())
} else {
- <_>::default()
+ (vec![], vec![])
};
runs.into_iter()
@@ -565,7 +571,7 @@ impl<'a> LineLayout<'a> {
Either::Right(range.rev())
}
})
- .map(move |idx| (self.ranges[idx].clone(), self.get(idx).unwrap()))
+ .map(move |idx| self.get(idx).unwrap())
}
/// Find the index of the item whose range contains the `text_offset`.
diff --git a/src/library/placed.rs b/src/library/placed.rs
index 722e0035..589a299b 100644
--- a/src/library/placed.rs
+++ b/src/library/placed.rs
@@ -6,27 +6,22 @@ pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let aligns = args.find().unwrap_or(Spec::new(Some(Align::Left), None));
let tx = args.named("dx")?.unwrap_or_default();
let ty = args.named("dy")?.unwrap_or_default();
- let body: Template = args.expect("body")?;
- Ok(Value::Template(Template::from_block(move |style| {
- PlacedNode {
- child: body.pack(style).moved(Point::new(tx, ty)).aligned(aligns),
- }
- })))
+ let body: Node = args.expect("body")?;
+ Ok(Value::block(PlacedNode(
+ body.into_block().moved(Point::new(tx, ty)).aligned(aligns),
+ )))
}
/// A node that places its child absolutely.
#[derive(Debug, Hash)]
-pub struct PlacedNode {
- /// The node to be placed.
- pub child: PackedNode,
-}
+pub struct PlacedNode(pub PackedNode);
impl PlacedNode {
/// Whether this node wants to be placed relative to its its parent's base
/// origin. instead of relative to the parent's current flow/cursor
/// position.
pub fn out_of_flow(&self) -> bool {
- self.child
+ self.0
.downcast::<AlignNode>()
.map_or(false, |node| node.aligns.y.is_some())
}
@@ -48,7 +43,7 @@ impl Layout for PlacedNode {
Regions::one(regions.base, regions.base, expand)
};
- let mut frames = self.child.layout(ctx, &pod);
+ let mut frames = self.0.layout(ctx, &pod);
let Constrained { item: frame, cts } = &mut frames[0];
// If expansion is off, zero all sizes so that we don't take up any
@@ -56,13 +51,6 @@ impl Layout for PlacedNode {
let target = regions.expand.select(regions.current, Size::zero());
Rc::make_mut(frame).resize(target, Align::LEFT_TOP);
- // Place relative to parent's base origin by offsetting our elements by
- // the negative cursor position.
- if out_of_flow {
- let offset = (regions.current - regions.base).to_point();
- Rc::make_mut(frame).translate(offset);
- }
-
// Set base constraint because our pod size is base and exact
// constraints if we needed to expand or offset.
*cts = Constraints::new(regions.expand);
diff --git a/src/library/shape.rs b/src/library/shape.rs
index 61c0d6e3..a9c9b333 100644
--- a/src/library/shape.rs
+++ b/src/library/shape.rs
@@ -1,6 +1,7 @@
use std::f64::consts::SQRT_2;
use super::prelude::*;
+use super::LinkNode;
/// `rect`: A rectangle with optional content.
pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
@@ -76,20 +77,15 @@ fn shape_impl(
}
// The shape's contents.
- let body = args.find::<Template>();
-
- Ok(Value::Template(Template::from_inline(move |style| {
- ShapeNode {
- kind,
- fill,
- stroke,
- child: body
- .as_ref()
- .map(|body| body.pack(style).padded(Sides::splat(padding))),
- }
- .pack()
- .sized(Spec::new(width, height))
- })))
+ let child = args
+ .find()
+ .map(|body: Node| body.into_block().padded(Sides::splat(padding)));
+
+ Ok(Value::inline(
+ ShapeNode { kind, fill, stroke, child }
+ .pack()
+ .sized(Spec::new(width, height)),
+ ))
}
/// Places its child into a sizable and fillable shape.
@@ -154,9 +150,10 @@ impl Layout for ShapeNode {
frames = vec![Frame::new(size).constrain(Constraints::tight(regions))];
}
+ let frame = Rc::make_mut(&mut frames[0].item);
+
// Add fill and/or stroke.
if self.fill.is_some() || self.stroke.is_some() {
- let frame = Rc::make_mut(&mut frames[0].item);
let geometry = match self.kind {
ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size),
ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size),
@@ -171,6 +168,11 @@ impl Layout for ShapeNode {
frame.prepend(Point::zero(), Element::Shape(shape));
}
+ // Apply link if it exists.
+ if let Some(url) = ctx.styles.get_ref(LinkNode::URL) {
+ frame.link(url);
+ }
+
frames
}
}
diff --git a/src/library/sized.rs b/src/library/sized.rs
index dfdc721d..6d677ca8 100644
--- a/src/library/sized.rs
+++ b/src/library/sized.rs
@@ -4,18 +4,16 @@ use super::prelude::*;
pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let width = args.named("width")?;
let height = args.named("height")?;
- let body: Template = args.find().unwrap_or_default();
- Ok(Value::Template(Template::from_inline(move |style| {
- body.pack(style).sized(Spec::new(width, height))
- })))
+ let body: Node = args.find().unwrap_or_default();
+ Ok(Value::inline(
+ body.into_block().sized(Spec::new(width, height)),
+ ))
}
/// `block`: Place content into the flow.
pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let body: Template = args.find().unwrap_or_default();
- Ok(Value::Template(Template::from_block(move |style| {
- body.pack(style)
- })))
+ let body: Node = args.find().unwrap_or_default();
+ Ok(Value::block(body.into_block()))
}
/// A node that sizes its child.
diff --git a/src/library/spacing.rs b/src/library/spacing.rs
index 59911dc7..b5ecce69 100644
--- a/src/library/spacing.rs
+++ b/src/library/spacing.rs
@@ -2,21 +2,41 @@ use super::prelude::*;
/// `h`: Horizontal spacing.
pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.spacing(SpecAxis::Horizontal, args.expect("spacing")?);
- Ok(Value::Template(template))
+ Ok(Value::Node(Node::Spacing(
+ SpecAxis::Horizontal,
+ args.expect("spacing")?,
+ )))
}
/// `v`: Vertical spacing.
pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.spacing(SpecAxis::Vertical, args.expect("spacing")?);
- Ok(Value::Template(template))
+ Ok(Value::Node(Node::Spacing(
+ SpecAxis::Vertical,
+ args.expect("spacing")?,
+ )))
+}
+
+/// Explicit spacing in a flow or paragraph.
+#[derive(Hash)]
+pub struct SpacingNode {
+ /// The kind of spacing.
+ pub kind: SpacingKind,
+ /// The spacing's styles.
+ pub styles: Styles,
+}
+
+impl Debug for SpacingNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ self.styles.fmt(f)?;
+ }
+ write!(f, "{:?}", self.kind)
+ }
}
/// Kinds of spacing.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Spacing {
+pub enum SpacingKind {
/// A length stated in absolute values and/or relative to the parent's size.
Linear(Linear),
/// A length that is the fraction of the remaining free space in the parent.
@@ -24,7 +44,7 @@ pub enum Spacing {
}
castable! {
- Spacing,
+ SpacingKind,
Expected: "linear or fractional",
Value::Length(v) => Self::Linear(v.into()),
Value::Relative(v) => Self::Linear(v.into()),
diff --git a/src/library/stack.rs b/src/library/stack.rs
index 2b1371ab..8a1f0fd5 100644
--- a/src/library/stack.rs
+++ b/src/library/stack.rs
@@ -1,54 +1,31 @@
use std::fmt::{self, Debug, Formatter};
use super::prelude::*;
-use super::{AlignNode, Spacing};
+use super::{AlignNode, SpacingKind, SpacingNode};
/// `stack`: Stack children along an axis.
pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- enum Child {
- Spacing(Spacing),
- Any(Template),
- }
-
- castable! {
- Child,
- Expected: "linear, fractional or template",
- Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
- Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Template(v) => Self::Any(v),
- }
-
let dir = args.named("dir")?.unwrap_or(Dir::TTB);
let spacing = args.named("spacing")?;
- let list: Vec<Child> = args.all().collect();
- Ok(Value::Template(Template::from_block(move |style| {
- let mut children = vec![];
- let mut delayed = None;
+ let mut children = vec![];
+ let mut deferred = None;
- // Build the list of stack children.
- for child in &list {
- match child {
- Child::Spacing(v) => {
- children.push(StackChild::Spacing(*v));
- delayed = None;
- }
- Child::Any(child) => {
- if let Some(v) = delayed {
- children.push(StackChild::Spacing(v));
- }
-
- let node = child.pack(style);
- children.push(StackChild::Node(node));
- delayed = spacing;
+ // Build the list of stack children.
+ for child in args.all() {
+ match child {
+ StackChild::Spacing(_) => deferred = None,
+ StackChild::Node(_) => {
+ if let Some(v) = deferred {
+ children.push(StackChild::spacing(v));
}
+ deferred = spacing;
}
}
+ children.push(child);
+ }
- StackNode { dir, children }
- })))
+ Ok(Value::block(StackNode { dir, children }))
}
/// A node that stacks its children.
@@ -74,20 +51,37 @@ impl Layout for StackNode {
#[derive(Hash)]
pub enum StackChild {
/// Spacing between other nodes.
- Spacing(Spacing),
+ Spacing(SpacingNode),
/// An arbitrary node.
Node(PackedNode),
}
+impl StackChild {
+ /// Create a spacing node from a spacing kind.
+ pub fn spacing(kind: SpacingKind) -> Self {
+ Self::Spacing(SpacingNode { kind, styles: Styles::new() })
+ }
+}
+
impl Debug for StackChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
- Self::Spacing(spacing) => spacing.fmt(f),
+ Self::Spacing(node) => node.fmt(f),
Self::Node(node) => node.fmt(f),
}
}
}
+castable! {
+ StackChild,
+ Expected: "linear, fractional or template",
+ Value::Length(v) => Self::spacing(SpacingKind::Linear(v.into())),
+ Value::Relative(v) => Self::spacing(SpacingKind::Linear(v.into())),
+ Value::Linear(v) => Self::spacing(SpacingKind::Linear(v)),
+ Value::Fractional(v) => Self::spacing(SpacingKind::Fractional(v)),
+ Value::Node(v) => Self::Node(v.into_block()),
+}
+
/// Performs stack layout.
struct StackLayouter<'a> {
/// The stack node to layout.
@@ -148,15 +142,15 @@ impl<'a> StackLayouter<'a> {
/// Layout all children.
fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
for child in &self.stack.children {
- match *child {
- StackChild::Spacing(Spacing::Linear(v)) => {
- self.layout_absolute(v);
- }
- StackChild::Spacing(Spacing::Fractional(v)) => {
- self.items.push(StackItem::Fractional(v));
- self.fr += v;
- }
- StackChild::Node(ref node) => {
+ match child {
+ StackChild::Spacing(node) => match node.kind {
+ SpacingKind::Linear(v) => self.layout_absolute(v),
+ SpacingKind::Fractional(v) => {
+ self.items.push(StackItem::Fractional(v));
+ self.fr += v;
+ }
+ },
+ StackChild::Node(node) => {
if self.regions.is_full() {
self.finish_region();
}
diff --git a/src/library/text.rs b/src/library/text.rs
index 0790196d..99c68f79 100644
--- a/src/library/text.rs
+++ b/src/library/text.rs
@@ -1,426 +1,488 @@
use std::borrow::Cow;
use std::convert::TryInto;
-use std::ops::Range;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::{BitXor, Range};
use rustybuzz::{Feature, UnicodeBuffer};
use ttf_parser::Tag;
use super::prelude::*;
+use super::LinkNode;
use crate::font::{
Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight,
VerticalFontMetric,
};
use crate::geom::{Dir, Em, Length, Point, Size};
-use crate::style::{
- FontFamily, FontFeatures, NumberPosition, NumberType, NumberWidth, Style,
- StylisticSet, TextStyle,
-};
use crate::util::{EcoString, SliceExt};
-/// `font`: Configure the font.
-pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- castable! {
- Vec<FontFamily>,
- Expected: "string, generic family or array thereof",
- Value::Str(string) => vec![FontFamily::Named(string.to_lowercase())],
- Value::Array(values) => {
- values.into_iter().filter_map(|v| v.cast().ok()).collect()
- },
- @family: FontFamily => vec![family.clone()],
- }
-
- castable! {
- Vec<EcoString>,
- Expected: "string or array of strings",
- Value::Str(string) => vec![string.to_lowercase()],
- Value::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .map(|string: EcoString| string.to_lowercase())
- .collect(),
- }
-
- castable! {
- FontStyle,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "normal" => Self::Normal,
- "italic" => Self::Italic,
- "oblique" => Self::Oblique,
- _ => Err(r#"expected "normal", "italic" or "oblique""#)?,
- },
- }
-
- castable! {
- FontWeight,
- Expected: "integer or string",
- Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number),
- Value::Str(string) => match string.as_str() {
- "thin" => Self::THIN,
- "extralight" => Self::EXTRALIGHT,
- "light" => Self::LIGHT,
- "regular" => Self::REGULAR,
- "medium" => Self::MEDIUM,
- "semibold" => Self::SEMIBOLD,
- "bold" => Self::BOLD,
- "extrabold" => Self::EXTRABOLD,
- "black" => Self::BLACK,
- _ => Err("unknown font weight")?,
- },
- }
-
- castable! {
- FontStretch,
- Expected: "relative",
- Value::Relative(v) => Self::from_ratio(v.get() as f32),
- }
-
- castable! {
- VerticalFontMetric,
- Expected: "linear or string",
- Value::Length(v) => Self::Linear(v.into()),
- Value::Relative(v) => Self::Linear(v.into()),
- Value::Linear(v) => Self::Linear(v),
- Value::Str(string) => match string.as_str() {
- "ascender" => Self::Ascender,
- "cap-height" => Self::CapHeight,
- "x-height" => Self::XHeight,
- "baseline" => Self::Baseline,
- "descender" => Self::Descender,
- _ => Err("unknown font metric")?,
- },
- }
-
- castable! {
- StylisticSet,
- Expected: "integer",
- Value::Int(v) => match v {
- 1 ..= 20 => Self::new(v as u8),
- _ => Err("must be between 1 and 20")?,
- },
- }
-
- castable! {
- NumberType,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "lining" => Self::Lining,
- "old-style" => Self::OldStyle,
- _ => Err(r#"expected "lining" or "old-style""#)?,
- },
- }
-
- castable! {
- NumberWidth,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "proportional" => Self::Proportional,
- "tabular" => Self::Tabular,
- _ => Err(r#"expected "proportional" or "tabular""#)?,
- },
- }
-
- castable! {
- NumberPosition,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "normal" => Self::Normal,
- "subscript" => Self::Subscript,
- "superscript" => Self::Superscript,
- _ => Err(r#"expected "normal", "subscript" or "superscript""#)?,
- },
- }
-
- castable! {
- Vec<(Tag, u32)>,
- Expected: "array of strings or dictionary mapping tags to integers",
- Value::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1))
- .collect(),
- Value::Dict(values) => values
- .into_iter()
- .filter_map(|(k, v)| {
- let tag = Tag::from_bytes_lossy(k.as_bytes());
- let num = v.cast::<i64>().ok()?.try_into().ok()?;
- Some((tag, num))
- })
- .collect(),
+/// `strike`: Typeset striken-through text.
+pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Strikethrough)
+}
+
+/// `underline`: Typeset underlined text.
+pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Underline)
+}
+
+/// `overline`: Typeset text with an overline.
+pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Overline)
+}
+
+fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
+ let stroke = args.named("stroke")?.or_else(|| args.find());
+ let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
+ let offset = args.named("offset")?;
+ let extent = args.named("extent")?.unwrap_or_default();
+ let body: Node = args.expect("body")?;
+ let deco = LineDecoration { kind, stroke, thickness, offset, extent };
+ Ok(Value::Node(
+ body.styled(Styles::one(TextNode::LINES, vec![deco])),
+ ))
+}
+
+/// A single run of text with the same style.
+#[derive(Hash)]
+pub struct TextNode {
+ /// The run's text.
+ pub text: EcoString,
+ /// The run's styles.
+ pub styles: Styles,
+}
+
+#[properties]
+impl TextNode {
+ /// A prioritized sequence of font families.
+ pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif];
+ /// The serif font family/families.
+ pub const SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")];
+ /// The sans-serif font family/families.
+ pub const SANS_SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")];
+ /// The monospace font family/families.
+ pub const MONOSPACE_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Mono")];
+ /// Whether to allow font fallback when the primary font list contains no
+ /// match.
+ pub const FALLBACK: bool = true;
+
+ /// How the font is styled.
+ pub const STYLE: FontStyle = FontStyle::Normal;
+ /// The boldness / thickness of the font's glyphs.
+ pub const WEIGHT: FontWeight = FontWeight::REGULAR;
+ /// The width of the glyphs.
+ pub const STRETCH: FontStretch = FontStretch::NORMAL;
+ /// Whether the font weight should be increased by 300.
+ #[fold(bool::bitxor)]
+ pub const STRONG: bool = false;
+ /// Whether the the font style should be inverted.
+ #[fold(bool::bitxor)]
+ pub const EMPH: bool = false;
+ /// Whether a monospace font should be preferred.
+ pub const MONOSPACE: bool = false;
+ /// The glyph fill color.
+ pub const FILL: Paint = RgbaColor::BLACK.into();
+ /// Decorative lines.
+ #[fold(|a, b| a.into_iter().chain(b).collect())]
+ pub const LINES: Vec<LineDecoration> = vec![];
+
+ /// The size of the glyphs.
+ #[fold(Linear::compose)]
+ pub const SIZE: Linear = Length::pt(11.0).into();
+ /// The amount of space that should be added between characters.
+ pub const TRACKING: Em = Em::zero();
+ /// The top end of the text bounding box.
+ pub const TOP_EDGE: VerticalFontMetric = VerticalFontMetric::CapHeight;
+ /// The bottom end of the text bounding box.
+ pub const BOTTOM_EDGE: VerticalFontMetric = VerticalFontMetric::Baseline;
+
+ /// Whether to apply kerning ("kern").
+ pub const KERNING: bool = true;
+ /// Whether small capital glyphs should be used. ("smcp")
+ pub const SMALLCAPS: bool = false;
+ /// Whether to apply stylistic alternates. ("salt")
+ pub const ALTERNATES: bool = false;
+ /// Which stylistic set to apply. ("ss01" - "ss20")
+ pub const STYLISTIC_SET: Option<StylisticSet> = None;
+ /// Whether standard ligatures are active. ("liga", "clig")
+ pub const LIGATURES: bool = true;
+ /// Whether ligatures that should be used sparingly are active. ("dlig")
+ pub const DISCRETIONARY_LIGATURES: bool = false;
+ /// Whether historical ligatures are active. ("hlig")
+ pub const HISTORICAL_LIGATURES: bool = false;
+ /// Which kind of numbers / figures to select.
+ pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto;
+ /// The width of numbers / figures.
+ pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto;
+ /// How to position numbers.
+ pub const NUMBER_POSITION: NumberPosition = NumberPosition::Normal;
+ /// Whether to have a slash through the zero glyph. ("zero")
+ pub const SLASHED_ZERO: bool = false;
+ /// Whether to convert fractions. ("frac")
+ pub const FRACTIONS: bool = false;
+ /// Raw OpenType features to apply.
+ pub const FEATURES: Vec<(Tag, u32)> = vec![];
+}
+
+impl Construct for TextNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ // We don't need to do anything more here because the whole point of the
+ // text constructor is to apply the styles and that happens
+ // automatically during class construction.
+ args.expect::<Node>("body")
}
+}
- let list = args.named("family")?.or_else(|| {
- let families: Vec<_> = args.all().collect();
- (!families.is_empty()).then(|| families)
- });
+impl Set for TextNode {
+ fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> {
+ let list = args.named("family")?.or_else(|| {
+ let families: Vec<_> = args.all().collect();
+ (!families.is_empty()).then(|| families)
+ });
+
+ styles.set_opt(Self::FAMILY_LIST, list);
+ styles.set_opt(Self::SERIF_LIST, args.named("serif")?);
+ styles.set_opt(Self::SANS_SERIF_LIST, args.named("sans-serif")?);
+ styles.set_opt(Self::MONOSPACE_LIST, args.named("monospace")?);
+ styles.set_opt(Self::FALLBACK, args.named("fallback")?);
+ styles.set_opt(Self::STYLE, args.named("style")?);
+ styles.set_opt(Self::WEIGHT, args.named("weight")?);
+ styles.set_opt(Self::STRETCH, args.named("stretch")?);
+ styles.set_opt(Self::FILL, args.named("fill")?.or_else(|| args.find()));
+ styles.set_opt(Self::SIZE, args.named("size")?.or_else(|| args.find()));
+ styles.set_opt(Self::TRACKING, args.named("tracking")?.map(Em::new));
+ styles.set_opt(Self::TOP_EDGE, args.named("top-edge")?);
+ styles.set_opt(Self::BOTTOM_EDGE, args.named("bottom-edge")?);
+ styles.set_opt(Self::KERNING, args.named("kerning")?);
+ styles.set_opt(Self::SMALLCAPS, args.named("smallcaps")?);
+ styles.set_opt(Self::ALTERNATES, args.named("alternates")?);
+ styles.set_opt(Self::STYLISTIC_SET, args.named("stylistic-set")?);
+ styles.set_opt(Self::LIGATURES, args.named("ligatures")?);
+ styles.set_opt(
+ Self::DISCRETIONARY_LIGATURES,
+ args.named("discretionary-ligatures")?,
+ );
+ styles.set_opt(
+ Self::HISTORICAL_LIGATURES,
+ args.named("historical-ligatures")?,
+ );
+ styles.set_opt(Self::NUMBER_TYPE, args.named("number-type")?);
+ styles.set_opt(Self::NUMBER_WIDTH, args.named("number-width")?);
+ styles.set_opt(Self::NUMBER_POSITION, args.named("number-position")?);
+ styles.set_opt(Self::SLASHED_ZERO, args.named("slashed-zero")?);
+ styles.set_opt(Self::FRACTIONS, args.named("fractions")?);
+ styles.set_opt(Self::FEATURES, args.named("features")?);
- let serif = args.named("serif")?;
- let sans_serif = args.named("sans-serif")?;
- let monospace = args.named("monospace")?;
- let fallback = args.named("fallback")?;
- let style = args.named("style")?;
- let weight = args.named("weight")?;
- let stretch = args.named("stretch")?;
- let size = args.named::<Linear>("size")?.or_else(|| args.find());
- let tracking = args.named("tracking")?.map(Em::new);
- let top_edge = args.named("top-edge")?;
- let bottom_edge = args.named("bottom-edge")?;
- let fill = args.named("fill")?.or_else(|| args.find());
- let kerning = args.named("kerning")?;
- let smallcaps = args.named("smallcaps")?;
- let alternates = args.named("alternates")?;
- let stylistic_set = args.named("stylistic-set")?;
- let ligatures = args.named("ligatures")?;
- let discretionary_ligatures = args.named("discretionary-ligatures")?;
- let historical_ligatures = args.named("historical-ligatures")?;
- let number_type = args.named("number-type")?;
- let number_width = args.named("number-width")?;
- let number_position = args.named("number-position")?;
- let slashed_zero = args.named("slashed-zero")?;
- let fractions = args.named("fractions")?;
- let features = args.named("features")?;
- let body = args.find::<Template>();
-
- macro_rules! set {
- ($target:expr => $source:expr) => {
- if let Some(v) = $source {
- $target = v;
- }
- };
+ Ok(())
}
+}
- let f = move |style_: &mut Style| {
- let text = style_.text_mut();
- set!(text.families_mut().list => list.clone());
- set!(text.families_mut().serif => serif.clone());
- set!(text.families_mut().sans_serif => sans_serif.clone());
- set!(text.families_mut().monospace => monospace.clone());
- set!(text.fallback => fallback);
- set!(text.variant.style => style);
- set!(text.variant.weight => weight);
- set!(text.variant.stretch => stretch);
- set!(text.size => size.map(|v| v.resolve(text.size)));
- set!(text.tracking => tracking);
- set!(text.top_edge => top_edge);
- set!(text.bottom_edge => bottom_edge);
- set!(text.fill => fill);
- set!(text.features_mut().kerning => kerning);
- set!(text.features_mut().smallcaps => smallcaps);
- set!(text.features_mut().alternates => alternates);
- set!(text.features_mut().stylistic_set => stylistic_set);
- set!(text.features_mut().ligatures.standard => ligatures);
- set!(text.features_mut().ligatures.discretionary => discretionary_ligatures);
- set!(text.features_mut().ligatures.historical => historical_ligatures);
- set!(text.features_mut().numbers.type_ => number_type);
- set!(text.features_mut().numbers.width => number_width);
- set!(text.features_mut().numbers.position => number_position);
- set!(text.features_mut().numbers.slashed_zero => slashed_zero);
- set!(text.features_mut().numbers.fractions => fractions);
- set!(text.features_mut().raw => features.clone());
- };
+impl Debug for TextNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if f.alternate() {
+ self.styles.fmt(f)?;
+ }
+ write!(f, "Text({:?})", self.text)
+ }
+}
- Ok(if let Some(body) = body {
- Value::Template(body.modified(f))
- } else {
- ctx.template.modify(f);
- Value::None
- })
+/// A generic or named font family.
+#[derive(Clone, Eq, PartialEq, Hash)]
+pub enum FontFamily {
+ /// A family that has "serifs", small strokes attached to letters.
+ Serif,
+ /// A family in which glyphs do not have "serifs", small attached strokes.
+ SansSerif,
+ /// A family in which (almost) all glyphs are of equal width.
+ Monospace,
+ /// A specific font family like "Arial".
+ Named(NamedFamily),
}
-/// Shape text into [`ShapedText`].
-pub fn shape<'a>(
- ctx: &mut LayoutContext,
- text: &'a str,
- style: &'a TextStyle,
- dir: Dir,
-) -> ShapedText<'a> {
- let mut glyphs = vec![];
- if !text.is_empty() {
- shape_segment(
- ctx.fonts,
- &mut glyphs,
- 0,
- text,
- style.variant(),
- style.families(),
- None,
- dir,
- &tags(&style.features),
- );
+impl FontFamily {
+ /// Create a named font family variant, directly from a string.
+ pub fn named(string: &str) -> Self {
+ Self::Named(NamedFamily::new(string))
}
+}
- track(&mut glyphs, style.tracking);
+impl Debug for FontFamily {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Serif => f.pad("serif"),
+ Self::SansSerif => f.pad("sans-serif"),
+ Self::Monospace => f.pad("monospace"),
+ Self::Named(s) => s.fmt(f),
+ }
+ }
+}
- let (size, baseline) = measure(ctx, &glyphs, style);
- ShapedText {
- text,
- dir,
- style,
- size,
- baseline,
- glyphs: Cow::Owned(glyphs),
+/// A specific font family like "Arial".
+#[derive(Clone, Eq, PartialEq, Hash)]
+pub struct NamedFamily(String);
+
+impl NamedFamily {
+ /// Create a named font family variant.
+ pub fn new(string: &str) -> Self {
+ Self(string.to_lowercase())
+ }
+
+ /// The lowercased family name.
+ pub fn as_str(&self) -> &str {
+ &self.0
}
}
-/// The result of shaping text.
-///
-/// This type contains owned or borrowed shaped text runs, which can be
-/// measured, used to reshape substrings more quickly and converted into a
-/// frame.
-#[derive(Debug, Clone)]
-pub struct ShapedText<'a> {
- /// The text that was shaped.
- pub text: &'a str,
- /// The text direction.
- pub dir: Dir,
- /// The properties used for font selection.
- pub style: &'a TextStyle,
- /// The font size.
- pub size: Size,
- /// The baseline from the top of the frame.
- pub baseline: Length,
- /// The shaped glyphs.
- pub glyphs: Cow<'a, [ShapedGlyph]>,
+impl Debug for NamedFamily {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.0.fmt(f)
+ }
}
-/// A single glyph resulting from shaping.
-#[derive(Debug, Copy, Clone)]
-pub struct ShapedGlyph {
- /// The font face the glyph is contained in.
- pub face_id: FaceId,
- /// The glyph's index in the face.
- pub glyph_id: u16,
- /// The advance width of the glyph.
- pub x_advance: Em,
- /// The horizontal offset of the glyph.
- pub x_offset: Em,
- /// The start index of the glyph in the source text.
- pub text_index: usize,
- /// Whether splitting the shaping result before this glyph would yield the
- /// same results as shaping the parts to both sides of `text_index`
- /// separately.
- pub safe_to_break: bool,
+dynamic! {
+ FontFamily: "font family",
+ Value::Str(string) => Self::named(&string),
}
-impl<'a> ShapedText<'a> {
- /// Build the shaped text's frame.
- pub fn build(&self) -> Frame {
- let mut offset = Length::zero();
- let mut frame = Frame::new(self.size);
- frame.baseline = Some(self.baseline);
+castable! {
+ Vec<FontFamily>,
+ Expected: "string, generic family or array thereof",
+ Value::Str(string) => vec![FontFamily::named(&string)],
+ Value::Array(values) => {
+ values.into_iter().filter_map(|v| v.cast().ok()).collect()
+ },
+ @family: FontFamily => vec![family.clone()],
+}
- for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
- let pos = Point::new(offset, self.baseline);
+castable! {
+ Vec<NamedFamily>,
+ Expected: "string or array of strings",
+ Value::Str(string) => vec![NamedFamily::new(&string)],
+ Value::Array(values) => values
+ .into_iter()
+ .filter_map(|v| v.cast().ok())
+ .map(|string: EcoString| NamedFamily::new(&string))
+ .collect(),
+}
- let mut text = Text {
- face_id,
- size: self.style.size,
- width: Length::zero(),
- fill: self.style.fill,
- glyphs: vec![],
- };
+castable! {
+ FontStyle,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "normal" => Self::Normal,
+ "italic" => Self::Italic,
+ "oblique" => Self::Oblique,
+ _ => Err(r#"expected "normal", "italic" or "oblique""#)?,
+ },
+}
- for glyph in group {
- text.glyphs.push(Glyph {
- id: glyph.glyph_id,
- x_advance: glyph.x_advance,
- x_offset: glyph.x_offset,
- });
- text.width += glyph.x_advance.to_length(text.size);
- }
+castable! {
+ FontWeight,
+ Expected: "integer or string",
+ Value::Int(v) => Value::Int(v)
+ .cast::<usize>()?
+ .try_into()
+ .map_or(Self::BLACK, Self::from_number),
+ Value::Str(string) => match string.as_str() {
+ "thin" => Self::THIN,
+ "extralight" => Self::EXTRALIGHT,
+ "light" => Self::LIGHT,
+ "regular" => Self::REGULAR,
+ "medium" => Self::MEDIUM,
+ "semibold" => Self::SEMIBOLD,
+ "bold" => Self::BOLD,
+ "extrabold" => Self::EXTRABOLD,
+ "black" => Self::BLACK,
+ _ => Err("unknown font weight")?,
+ },
+}
- offset += text.width;
- frame.push(pos, Element::Text(text));
- }
+castable! {
+ FontStretch,
+ Expected: "relative",
+ Value::Relative(v) => Self::from_ratio(v.get() as f32),
+}
- frame
+castable! {
+ VerticalFontMetric,
+ Expected: "linear or string",
+ Value::Length(v) => Self::Linear(v.into()),
+ Value::Relative(v) => Self::Linear(v.into()),
+ Value::Linear(v) => Self::Linear(v),
+ Value::Str(string) => match string.as_str() {
+ "ascender" => Self::Ascender,
+ "cap-height" => Self::CapHeight,
+ "x-height" => Self::XHeight,
+ "baseline" => Self::Baseline,
+ "descender" => Self::Descender,
+ _ => Err("unknown font metric")?,
+ },
+}
+
+/// A stylistic set in a font face.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct StylisticSet(u8);
+
+impl StylisticSet {
+ /// Creates a new set, clamping to 1-20.
+ pub fn new(index: u8) -> Self {
+ Self(index.clamp(1, 20))
}
- /// Reshape a range of the shaped text, reusing information from this
- /// shaping process if possible.
- pub fn reshape(
- &'a self,
- ctx: &mut LayoutContext,
- text_range: Range<usize>,
- ) -> ShapedText<'a> {
- if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
- let (size, baseline) = measure(ctx, glyphs, self.style);
- Self {
- text: &self.text[text_range],
- dir: self.dir,
- style: self.style,
- size,
- baseline,
- glyphs: Cow::Borrowed(glyphs),
- }
- } else {
- shape(ctx, &self.text[text_range], self.style, self.dir)
- }
+ /// Get the value, guaranteed to be 1-20.
+ pub fn get(self) -> u8 {
+ self.0
}
+}
- /// Find the subslice of glyphs that represent the given text range if both
- /// sides are safe to break.
- fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
- let Range { mut start, mut end } = text_range;
- if !self.dir.is_positive() {
- std::mem::swap(&mut start, &mut end);
- }
+castable! {
+ StylisticSet,
+ Expected: "integer",
+ Value::Int(v) => match v {
+ 1 ..= 20 => Self::new(v as u8),
+ _ => Err("must be between 1 and 20")?,
+ },
+}
- let left = self.find_safe_to_break(start, Side::Left)?;
- let right = self.find_safe_to_break(end, Side::Right)?;
- Some(&self.glyphs[left .. right])
- }
+/// Which kind of numbers / figures to select.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum NumberType {
+ /// Numbers that fit well with capital text. ("lnum")
+ Lining,
+ /// Numbers that fit well into flow of upper- and lowercase text. ("onum")
+ OldStyle,
+}
- /// Find the glyph offset matching the text index that is most towards the
- /// given side and safe-to-break.
- fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
- let ltr = self.dir.is_positive();
+castable! {
+ NumberType,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "lining" => Self::Lining,
+ "old-style" => Self::OldStyle,
+ _ => Err(r#"expected "lining" or "old-style""#)?,
+ },
+}
- // Handle edge cases.
- let len = self.glyphs.len();
- if text_index == 0 {
- return Some(if ltr { 0 } else { len });
- } else if text_index == self.text.len() {
- return Some(if ltr { len } else { 0 });
- }
+/// The width of numbers / figures.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum NumberWidth {
+ /// Number widths are glyph specific. ("pnum")
+ Proportional,
+ /// All numbers are of equal width / monospaced. ("tnum")
+ Tabular,
+}
- // Find any glyph with the text index.
- let mut idx = self
- .glyphs
- .binary_search_by(|g| {
- let ordering = g.text_index.cmp(&text_index);
- if ltr { ordering } else { ordering.reverse() }
- })
- .ok()?;
+castable! {
+ NumberWidth,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "proportional" => Self::Proportional,
+ "tabular" => Self::Tabular,
+ _ => Err(r#"expected "proportional" or "tabular""#)?,
+ },
+}
- let next = match towards {
- Side::Left => usize::checked_sub,
- Side::Right => usize::checked_add,
- };
+/// How to position numbers.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum NumberPosition {
+ /// Numbers are positioned on the same baseline as text.
+ Normal,
+ /// Numbers are smaller and placed at the bottom. ("subs")
+ Subscript,
+ /// Numbers are smaller and placed at the top. ("sups")
+ Superscript,
+}
- // Search for the outermost glyph with the text index.
- while let Some(next) = next(idx, 1) {
- if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
- break;
- }
- idx = next;
- }
+castable! {
+ NumberPosition,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "normal" => Self::Normal,
+ "subscript" => Self::Subscript,
+ "superscript" => Self::Superscript,
+ _ => Err(r#"expected "normal", "subscript" or "superscript""#)?,
+ },
+}
- // RTL needs offset one because the left side of the range should be
- // exclusive and the right side inclusive, contrary to the normal
- // behaviour of ranges.
- if !ltr {
- idx += 1;
- }
+castable! {
+ Vec<(Tag, u32)>,
+ Expected: "array of strings or dictionary mapping tags to integers",
+ Value::Array(values) => values
+ .into_iter()
+ .filter_map(|v| v.cast().ok())
+ .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1))
+ .collect(),
+ Value::Dict(values) => values
+ .into_iter()
+ .filter_map(|(k, v)| {
+ let tag = Tag::from_bytes_lossy(k.as_bytes());
+ let num = v.cast::<i64>().ok()?.try_into().ok()?;
+ Some((tag, num))
+ })
+ .collect(),
+}
- self.glyphs[idx].safe_to_break.then(|| idx)
- }
+/// Defines a line that is positioned over, under or on top of text.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct LineDecoration {
+ /// The kind of line.
+ pub kind: LineKind,
+ /// Stroke color of the line, defaults to the text color if `None`.
+ pub stroke: Option<Paint>,
+ /// Thickness of the line's strokes (dependent on scaled font size), read
+ /// from the font tables if `None`.
+ pub thickness: Option<Linear>,
+ /// Position of the line relative to the baseline (dependent on scaled font
+ /// size), read from the font tables if `None`.
+ pub offset: Option<Linear>,
+ /// Amount that the line will be longer or shorter than its associated text
+ /// (dependent on scaled font size).
+ pub extent: Linear,
}
-/// A visual side.
-enum Side {
- Left,
- Right,
+/// The kind of line decoration.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum LineKind {
+ /// A line under text.
+ Underline,
+ /// A line through text.
+ Strikethrough,
+ /// A line over text.
+ Overline,
+}
+
+/// Shape text into [`ShapedText`].
+pub fn shape<'a>(
+ fonts: &mut FontStore,
+ text: &'a str,
+ styles: Styles,
+ dir: Dir,
+) -> ShapedText<'a> {
+ let mut glyphs = vec![];
+ if !text.is_empty() {
+ shape_segment(
+ fonts,
+ &mut glyphs,
+ 0,
+ text,
+ variant(&styles),
+ families(&styles),
+ None,
+ dir,
+ &tags(&styles),
+ );
+ }
+
+ track(&mut glyphs, styles.get(TextNode::TRACKING));
+ let (size, baseline) = measure(fonts, &glyphs, &styles);
+
+ ShapedText {
+ text,
+ dir,
+ styles,
+ size,
+ baseline,
+ glyphs: Cow::Owned(glyphs),
+ }
}
/// Shape text with font fallback using the `families` iterator.
@@ -568,36 +630,40 @@ fn track(glyphs: &mut [ShapedGlyph], tracking: Em) {
/// Measure the size and baseline of a run of shaped glyphs with the given
/// properties.
fn measure(
- ctx: &mut LayoutContext,
+ fonts: &mut FontStore,
glyphs: &[ShapedGlyph],
- style: &TextStyle,
+ styles: &Styles,
) -> (Size, Length) {
let mut width = Length::zero();
let mut top = Length::zero();
let mut bottom = Length::zero();
+ let size = styles.get(TextNode::SIZE).abs;
+ let top_edge = styles.get(TextNode::TOP_EDGE);
+ let bottom_edge = styles.get(TextNode::BOTTOM_EDGE);
+
// Expand top and bottom by reading the face's vertical metrics.
let mut expand = |face: &Face| {
- top.set_max(face.vertical_metric(style.top_edge, style.size));
- bottom.set_max(-face.vertical_metric(style.bottom_edge, style.size));
+ top.set_max(face.vertical_metric(top_edge, size));
+ bottom.set_max(-face.vertical_metric(bottom_edge, size));
};
if glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
- for family in style.families() {
- if let Some(face_id) = ctx.fonts.select(family, style.variant) {
- expand(ctx.fonts.get(face_id));
+ for family in families(styles) {
+ if let Some(face_id) = fonts.select(family, variant(styles)) {
+ expand(fonts.get(face_id));
break;
}
}
} else {
for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
- let face = ctx.fonts.get(face_id);
+ let face = fonts.get(face_id);
expand(face);
for glyph in group {
- width += glyph.x_advance.to_length(style.size);
+ width += glyph.x_advance.resolve(size);
}
}
}
@@ -605,75 +671,320 @@ fn measure(
(Size::new(width, top + bottom), top)
}
+/// Resolve the font variant with `STRONG` and `EMPH` factored in.
+fn variant(styles: &Styles) -> FontVariant {
+ let mut variant = FontVariant::new(
+ styles.get(TextNode::STYLE),
+ styles.get(TextNode::WEIGHT),
+ styles.get(TextNode::STRETCH),
+ );
+
+ if styles.get(TextNode::STRONG) {
+ variant.weight = variant.weight.thicken(300);
+ }
+
+ if styles.get(TextNode::EMPH) {
+ variant.style = match variant.style {
+ FontStyle::Normal => FontStyle::Italic,
+ FontStyle::Italic => FontStyle::Normal,
+ FontStyle::Oblique => FontStyle::Normal,
+ }
+ }
+
+ variant
+}
+
+/// Resolve a prioritized iterator over the font families.
+fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone {
+ let head = if styles.get(TextNode::MONOSPACE) {
+ styles.get_ref(TextNode::MONOSPACE_LIST).as_slice()
+ } else {
+ &[]
+ };
+
+ let core = styles.get_ref(TextNode::FAMILY_LIST).iter().flat_map(move |family| {
+ match family {
+ FontFamily::Named(name) => std::slice::from_ref(name),
+ FontFamily::Serif => styles.get_ref(TextNode::SERIF_LIST),
+ FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF_LIST),
+ FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE_LIST),
+ }
+ });
+
+ let tail: &[&str] = if styles.get(TextNode::FALLBACK) {
+ &["ibm plex sans", "latin modern math", "twitter color emoji"]
+ } else {
+ &[]
+ };
+
+ head.iter()
+ .chain(core)
+ .map(|named| named.as_str())
+ .chain(tail.iter().copied())
+}
+
/// Collect the tags of the OpenType features to apply.
-fn tags(features: &FontFeatures) -> Vec<Feature> {
+fn tags(styles: &Styles) -> Vec<Feature> {
let mut tags = vec![];
let mut feat = |tag, value| {
tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
};
// Features that are on by default in Harfbuzz are only added if disabled.
- if !features.kerning {
+ if !styles.get(TextNode::KERNING) {
feat(b"kern", 0);
}
// Features that are off by default in Harfbuzz are only added if enabled.
- if features.smallcaps {
+ if styles.get(TextNode::SMALLCAPS) {
feat(b"smcp", 1);
}
- if features.alternates {
+ if styles.get(TextNode::ALTERNATES) {
feat(b"salt", 1);
}
let storage;
- if let Some(set) = features.stylistic_set {
+ if let Some(set) = styles.get(TextNode::STYLISTIC_SET) {
storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
feat(&storage, 1);
}
- if !features.ligatures.standard {
+ if !styles.get(TextNode::LIGATURES) {
feat(b"liga", 0);
feat(b"clig", 0);
}
- if features.ligatures.discretionary {
+ if styles.get(TextNode::DISCRETIONARY_LIGATURES) {
feat(b"dlig", 1);
}
- if features.ligatures.historical {
+ if styles.get(TextNode::HISTORICAL_LIGATURES) {
feat(b"hilg", 1);
}
- match features.numbers.type_ {
+ match styles.get(TextNode::NUMBER_TYPE) {
Smart::Auto => {}
Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
}
- match features.numbers.width {
+ match styles.get(TextNode::NUMBER_WIDTH) {
Smart::Auto => {}
Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
}
- match features.numbers.position {
+ match styles.get(TextNode::NUMBER_POSITION) {
NumberPosition::Normal => {}
NumberPosition::Subscript => feat(b"subs", 1),
NumberPosition::Superscript => feat(b"sups", 1),
}
- if features.numbers.slashed_zero {
+ if styles.get(TextNode::SLASHED_ZERO) {
feat(b"zero", 1);
}
- if features.numbers.fractions {
+ if styles.get(TextNode::FRACTIONS) {
feat(b"frac", 1);
}
- for &(tag, value) in features.raw.iter() {
+ for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() {
tags.push(Feature::new(tag, value, ..))
}
tags
}
+
+/// The result of shaping text.
+///
+/// This type contains owned or borrowed shaped text runs, which can be
+/// measured, used to reshape substrings more quickly and converted into a
+/// frame.
+#[derive(Debug, Clone)]
+pub struct ShapedText<'a> {
+ /// The text that was shaped.
+ pub text: &'a str,
+ /// The text direction.
+ pub dir: Dir,
+ /// The text's style properties.
+ // TODO(style): Go back to reference.
+ pub styles: Styles,
+ /// The font size.
+ pub size: Size,
+ /// The baseline from the top of the frame.
+ pub baseline: Length,
+ /// The shaped glyphs.
+ pub glyphs: Cow<'a, [ShapedGlyph]>,
+}
+
+/// A single glyph resulting from shaping.
+#[derive(Debug, Copy, Clone)]
+pub struct ShapedGlyph {
+ /// The font face the glyph is contained in.
+ pub face_id: FaceId,
+ /// The glyph's index in the face.
+ pub glyph_id: u16,
+ /// The advance width of the glyph.
+ pub x_advance: Em,
+ /// The horizontal offset of the glyph.
+ pub x_offset: Em,
+ /// The start index of the glyph in the source text.
+ pub text_index: usize,
+ /// Whether splitting the shaping result before this glyph would yield the
+ /// same results as shaping the parts to both sides of `text_index`
+ /// separately.
+ pub safe_to_break: bool,
+}
+
+impl<'a> ShapedText<'a> {
+ /// Build the shaped text's frame.
+ pub fn build(&self, fonts: &FontStore) -> Frame {
+ let mut offset = Length::zero();
+ let mut frame = Frame::new(self.size);
+ frame.baseline = Some(self.baseline);
+
+ for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
+ let pos = Point::new(offset, self.baseline);
+
+ let size = self.styles.get(TextNode::SIZE).abs;
+ let fill = self.styles.get(TextNode::FILL);
+ let glyphs = group
+ .iter()
+ .map(|glyph| Glyph {
+ id: glyph.glyph_id,
+ x_advance: glyph.x_advance,
+ x_offset: glyph.x_offset,
+ })
+ .collect();
+
+ let text = Text { face_id, size, fill, glyphs };
+ let width = text.width();
+ frame.push(pos, Element::Text(text));
+
+ // Apply line decorations.
+ for line in self.styles.get_ref(TextNode::LINES) {
+ let face = fonts.get(face_id);
+ let metrics = match line.kind {
+ LineKind::Underline => face.underline,
+ LineKind::Strikethrough => face.strikethrough,
+ LineKind::Overline => face.overline,
+ };
+
+ let extent = line.extent.resolve(size);
+ let offset = line
+ .offset
+ .map(|s| s.resolve(size))
+ .unwrap_or(-metrics.position.resolve(size));
+
+ let stroke = Stroke {
+ paint: line.stroke.unwrap_or(fill),
+ thickness: line
+ .thickness
+ .map(|s| s.resolve(size))
+ .unwrap_or(metrics.thickness.resolve(size)),
+ };
+
+ let subpos = Point::new(pos.x - extent, pos.y + offset);
+ let target = Point::new(width + 2.0 * extent, Length::zero());
+ let shape = Shape::stroked(Geometry::Line(target), stroke);
+ frame.push(subpos, Element::Shape(shape));
+ }
+
+ offset += width;
+ }
+
+ // Apply link if it exists.
+ if let Some(url) = self.styles.get_ref(LinkNode::URL) {
+ frame.link(url);
+ }
+
+ frame
+ }
+
+ /// Reshape a range of the shaped text, reusing information from this
+ /// shaping process if possible.
+ pub fn reshape(
+ &'a self,
+ fonts: &mut FontStore,
+ text_range: Range<usize>,
+ ) -> ShapedText<'a> {
+ if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
+ let (size, baseline) = measure(fonts, glyphs, &self.styles);
+ Self {
+ text: &self.text[text_range],
+ dir: self.dir,
+ styles: self.styles.clone(),
+ size,
+ baseline,
+ glyphs: Cow::Borrowed(glyphs),
+ }
+ } else {
+ shape(fonts, &self.text[text_range], self.styles.clone(), self.dir)
+ }
+ }
+
+ /// Find the subslice of glyphs that represent the given text range if both
+ /// sides are safe to break.
+ fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
+ let Range { mut start, mut end } = text_range;
+ if !self.dir.is_positive() {
+ std::mem::swap(&mut start, &mut end);
+ }
+
+ let left = self.find_safe_to_break(start, Side::Left)?;
+ let right = self.find_safe_to_break(end, Side::Right)?;
+ Some(&self.glyphs[left .. right])
+ }
+
+ /// Find the glyph offset matching the text index that is most towards the
+ /// given side and safe-to-break.
+ fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
+ let ltr = self.dir.is_positive();
+
+ // Handle edge cases.
+ let len = self.glyphs.len();
+ if text_index == 0 {
+ return Some(if ltr { 0 } else { len });
+ } else if text_index == self.text.len() {
+ return Some(if ltr { len } else { 0 });
+ }
+
+ // Find any glyph with the text index.
+ let mut idx = self
+ .glyphs
+ .binary_search_by(|g| {
+ let ordering = g.text_index.cmp(&text_index);
+ if ltr { ordering } else { ordering.reverse() }
+ })
+ .ok()?;
+
+ let next = match towards {
+ Side::Left => usize::checked_sub,
+ Side::Right => usize::checked_add,
+ };
+
+ // Search for the outermost glyph with the text index.
+ while let Some(next) = next(idx, 1) {
+ if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
+ break;
+ }
+ idx = next;
+ }
+
+ // RTL needs offset one because the left side of the range should be
+ // exclusive and the right side inclusive, contrary to the normal
+ // behaviour of ranges.
+ if !ltr {
+ idx += 1;
+ }
+
+ self.glyphs[idx].safe_to_break.then(|| idx)
+ }
+}
+
+/// A visual side.
+enum Side {
+ Left,
+ Right,
+}
diff --git a/src/library/transform.rs b/src/library/transform.rs
index 9ba71ecf..6a6b4034 100644
--- a/src/library/transform.rs
+++ b/src/library/transform.rs
@@ -26,15 +26,15 @@ pub fn rotate(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
}
fn transform_impl(args: &mut Args, transform: Transform) -> TypResult<Value> {
- let body: Template = args.expect("body")?;
+ let body: Node = args.expect("body")?;
let origin = args
.named("origin")?
.unwrap_or(Spec::splat(None))
.unwrap_or(Align::CENTER_HORIZON);
- Ok(Value::Template(Template::from_inline(move |style| {
- body.pack(style).transformed(transform, origin)
- })))
+ Ok(Value::inline(
+ body.into_block().transformed(transform, origin),
+ ))
}
/// A node that transforms its child without affecting layout.