diff options
Diffstat (limited to 'src/library')
| -rw-r--r-- | src/library/deco.rs | 74 | ||||
| -rw-r--r-- | src/library/elements/heading.rs (renamed from src/library/heading.rs) | 6 | ||||
| -rw-r--r-- | src/library/elements/image.rs (renamed from src/library/image.rs) | 6 | ||||
| -rw-r--r-- | src/library/elements/list.rs (renamed from src/library/list.rs) | 16 | ||||
| -rw-r--r-- | src/library/elements/math.rs (renamed from src/library/math.rs) | 4 | ||||
| -rw-r--r-- | src/library/elements/mod.rs | 15 | ||||
| -rw-r--r-- | src/library/elements/shape.rs (renamed from src/library/shape.rs) | 26 | ||||
| -rw-r--r-- | src/library/elements/table.rs (renamed from src/library/table.rs) | 6 | ||||
| -rw-r--r-- | src/library/layout/align.rs (renamed from src/library/align.rs) | 6 | ||||
| -rw-r--r-- | src/library/layout/columns.rs (renamed from src/library/columns.rs) | 6 | ||||
| -rw-r--r-- | src/library/layout/container.rs (renamed from src/library/container.rs) | 8 | ||||
| -rw-r--r-- | src/library/layout/flow.rs (renamed from src/library/flow.rs) | 7 | ||||
| -rw-r--r-- | src/library/layout/grid.rs (renamed from src/library/grid.rs) | 4 | ||||
| -rw-r--r-- | src/library/layout/hide.rs (renamed from src/library/hide.rs) | 4 | ||||
| -rw-r--r-- | src/library/layout/mod.rs | 27 | ||||
| -rw-r--r-- | src/library/layout/pad.rs (renamed from src/library/pad.rs) | 4 | ||||
| -rw-r--r-- | src/library/layout/page.rs (renamed from src/library/page.rs) | 4 | ||||
| -rw-r--r-- | src/library/layout/place.rs (renamed from src/library/place.rs) | 4 | ||||
| -rw-r--r-- | src/library/layout/spacing.rs (renamed from src/library/spacing.rs) | 4 | ||||
| -rw-r--r-- | src/library/layout/stack.rs (renamed from src/library/stack.rs) | 4 | ||||
| -rw-r--r-- | src/library/layout/transform.rs (renamed from src/library/transform.rs) | 19 | ||||
| -rw-r--r-- | src/library/mod.rs | 206 | ||||
| -rw-r--r-- | src/library/prelude.rs | 20 | ||||
| -rw-r--r-- | src/library/text/deco.rs | 250 | ||||
| -rw-r--r-- | src/library/text/link.rs (renamed from src/library/link.rs) | 4 | ||||
| -rw-r--r-- | src/library/text/mod.rs | 409 | ||||
| -rw-r--r-- | src/library/text/par.rs (renamed from src/library/par.rs) | 219 | ||||
| -rw-r--r-- | src/library/text/raw.rs (renamed from src/library/raw.rs) | 6 | ||||
| -rw-r--r-- | src/library/text/shaping.rs (renamed from src/library/text.rs) | 578 | ||||
| -rw-r--r-- | src/library/utility/math.rs | 114 | ||||
| -rw-r--r-- | src/library/utility/mod.rs (renamed from src/library/utility.rs) | 149 | ||||
| -rw-r--r-- | src/library/utility/numbering.rs (renamed from src/library/numbering.rs) | 86 |
32 files changed, 1144 insertions, 1151 deletions
diff --git a/src/library/deco.rs b/src/library/deco.rs deleted file mode 100644 index 8c2b5a9a..00000000 --- a/src/library/deco.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Text decorations. - -use super::prelude::*; -use super::TextNode; - -/// Typeset underline, striken-through or overlined text. -#[derive(Debug, Hash)] -pub struct DecoNode<const L: DecoLine>(pub Template); - -#[class] -impl<const L: DecoLine> DecoNode<L> { - /// Stroke color of the line, defaults to the text color if `None`. - #[shorthand] - pub const STROKE: Option<Paint> = None; - /// Thickness of the line's strokes (dependent on scaled font size), read - /// from the font tables if `None`. - #[shorthand] - pub const THICKNESS: Option<Linear> = None; - /// Position of the line relative to the baseline (dependent on scaled font - /// size), read from the font tables if `None`. - pub const OFFSET: Option<Linear> = None; - /// Amount that the line will be longer or shorter than its associated text - /// (dependent on scaled font size). - pub const EXTENT: Linear = Linear::zero(); - /// Whether the line skips sections in which it would collide - /// with the glyphs. Does not apply to strikethrough. - pub const EVADE: bool = true; - - fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { - Ok(Template::show(Self(args.expect::<Template>("body")?))) - } -} - -impl<const L: DecoLine> Show for DecoNode<L> { - fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { - Ok(styles - .show(self, ctx, [Value::Template(self.0.clone())])? - .unwrap_or_else(|| { - self.0.clone().styled(TextNode::LINES, vec![Decoration { - line: L, - stroke: styles.get(Self::STROKE), - thickness: styles.get(Self::THICKNESS), - offset: styles.get(Self::OFFSET), - extent: styles.get(Self::EXTENT), - evade: styles.get(Self::EVADE), - }]) - })) - } -} - -/// Defines a line that is positioned over, under or on top of text. -/// -/// For more details, see [`DecoNode`]. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Decoration { - pub line: DecoLine, - pub stroke: Option<Paint>, - pub thickness: Option<Linear>, - pub offset: Option<Linear>, - pub extent: Linear, - pub evade: bool, -} - -/// A kind of decorative line. -pub type DecoLine = usize; - -/// A line under text. -pub const UNDERLINE: DecoLine = 0; - -/// A line through text. -pub const STRIKETHROUGH: DecoLine = 1; - -/// A line over text. -pub const OVERLINE: DecoLine = 2; diff --git a/src/library/heading.rs b/src/library/elements/heading.rs index 0c9c8e27..a67f4f24 100644 --- a/src/library/heading.rs +++ b/src/library/elements/heading.rs @@ -1,7 +1,5 @@ -//! Document-structuring section headings. - -use super::prelude::*; -use super::{FontFamily, TextNode}; +use crate::library::prelude::*; +use crate::library::text::{FontFamily, TextNode}; /// A section heading. #[derive(Debug, Hash)] diff --git a/src/library/image.rs b/src/library/elements/image.rs index c1220734..66fb8f4b 100644 --- a/src/library/image.rs +++ b/src/library/elements/image.rs @@ -1,9 +1,7 @@ -//! Raster and vector graphics. - -use super::prelude::*; -use super::TextNode; use crate::diag::Error; use crate::image::ImageId; +use crate::library::prelude::*; +use crate::library::text::TextNode; /// Show a raster or vector graphic. #[derive(Debug, Hash)] diff --git a/src/library/list.rs b/src/library/elements/list.rs index baa8a0c9..726a2834 100644 --- a/src/library/list.rs +++ b/src/library/elements/list.rs @@ -1,13 +1,12 @@ -//! Unordered (bulleted) and ordered (numbered) lists. - -use super::prelude::*; -use super::{GridNode, Numbering, ParNode, TextNode, TrackSizing}; - +use crate::library::layout::{GridNode, TrackSizing}; +use crate::library::prelude::*; +use crate::library::text::{ParNode, TextNode}; +use crate::library::utility::Numbering; use crate::parse::Scanner; -/// An unordered or ordered list. +/// An unordered (bulleted) or ordered (numbered) list. #[derive(Debug, Hash)] -pub struct ListNode<const L: ListKind> { +pub struct ListNode<const L: ListKind = UNORDERED> { /// Where the list starts. pub start: usize, /// If true, there is paragraph spacing between the items, if false @@ -26,6 +25,9 @@ pub struct ListItem { pub body: Box<Template>, } +/// An ordered list. +pub type EnumNode = ListNode<ORDERED>; + #[class] impl<const L: ListKind> ListNode<L> { /// How the list is labelled. diff --git a/src/library/math.rs b/src/library/elements/math.rs index 5d0ae41b..761b4480 100644 --- a/src/library/math.rs +++ b/src/library/elements/math.rs @@ -1,6 +1,4 @@ -//! Mathematical formulas. - -use super::prelude::*; +use crate::library::prelude::*; /// A mathematical formula. #[derive(Debug, Hash)] diff --git a/src/library/elements/mod.rs b/src/library/elements/mod.rs new file mode 100644 index 00000000..572010bb --- /dev/null +++ b/src/library/elements/mod.rs @@ -0,0 +1,15 @@ +//! Primitive and semantic elements. + +mod heading; +mod image; +mod list; +mod math; +mod shape; +mod table; + +pub use self::image::*; +pub use heading::*; +pub use list::*; +pub use math::*; +pub use shape::*; +pub use table::*; diff --git a/src/library/shape.rs b/src/library/elements/shape.rs index bf159c52..8b967412 100644 --- a/src/library/shape.rs +++ b/src/library/elements/shape.rs @@ -1,14 +1,24 @@ -//! Colorable geometrical shapes. - use std::f64::consts::SQRT_2; -use super::prelude::*; -use super::TextNode; +use crate::library::prelude::*; +use crate::library::text::TextNode; /// Place a node into a sizable and fillable shape. #[derive(Debug, Hash)] pub struct ShapeNode<const S: ShapeKind>(pub Option<LayoutNode>); +/// Place a node into a square. +pub type SquareNode = ShapeNode<SQUARE>; + +/// Place a node into a rectangle. +pub type RectNode = ShapeNode<RECT>; + +/// Place a node into a circle. +pub type CircleNode = ShapeNode<CIRCLE>; + +/// Place a node into an ellipse. +pub type EllipseNode = ShapeNode<ELLIPSE>; + #[class] impl<const S: ShapeKind> ShapeNode<S> { /// How to fill the shape. @@ -134,16 +144,16 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> { pub type ShapeKind = usize; /// A rectangle with equal side lengths. -pub const SQUARE: ShapeKind = 0; +const SQUARE: ShapeKind = 0; /// A quadrilateral with four right angles. -pub const RECT: ShapeKind = 1; +const RECT: ShapeKind = 1; /// An ellipse with coinciding foci. -pub const CIRCLE: ShapeKind = 2; +const CIRCLE: ShapeKind = 2; /// A curve around two focal points. -pub const ELLIPSE: ShapeKind = 3; +const ELLIPSE: ShapeKind = 3; /// Whether a shape kind is curvy. fn is_round(kind: ShapeKind) -> bool { diff --git a/src/library/table.rs b/src/library/elements/table.rs index 07f28737..555dcc44 100644 --- a/src/library/table.rs +++ b/src/library/elements/table.rs @@ -1,7 +1,5 @@ -//! Tabular container. - -use super::prelude::*; -use super::{GridNode, TrackSizing}; +use crate::library::layout::{GridNode, TrackSizing}; +use crate::library::prelude::*; /// A table of items. #[derive(Debug, Hash)] diff --git a/src/library/align.rs b/src/library/layout/align.rs index 2fd734d3..7fbe0d01 100644 --- a/src/library/align.rs +++ b/src/library/layout/align.rs @@ -1,7 +1,5 @@ -//! Aligning nodes in their parent container. - -use super::prelude::*; -use super::ParNode; +use crate::library::prelude::*; +use crate::library::text::ParNode; /// Align a node along the layouting axes. #[derive(Debug, Hash)] diff --git a/src/library/columns.rs b/src/library/layout/columns.rs index 3ce9b6e5..167e7068 100644 --- a/src/library/columns.rs +++ b/src/library/layout/columns.rs @@ -1,7 +1,5 @@ -//! Multi-column layouts. - -use super::prelude::*; -use super::ParNode; +use crate::library::prelude::*; +use crate::library::text::ParNode; /// Separate a region into multiple equally sized columns. #[derive(Debug, Hash)] diff --git a/src/library/container.rs b/src/library/layout/container.rs index 5517d927..55579878 100644 --- a/src/library/container.rs +++ b/src/library/layout/container.rs @@ -1,8 +1,6 @@ -//! Inline- and block-level containers. +use crate::library::prelude::*; -use super::prelude::*; - -/// Size content and place it into a paragraph. +/// An inline-level container that sizes content and places it into a paragraph. pub struct BoxNode; #[class] @@ -15,7 +13,7 @@ impl BoxNode { } } -/// Place content into a separate flow. +/// A block-level container that places content into a separate flow. pub struct BlockNode; #[class] diff --git a/src/library/flow.rs b/src/library/layout/flow.rs index 9e039d3a..f4b885b1 100644 --- a/src/library/flow.rs +++ b/src/library/layout/flow.rs @@ -1,7 +1,6 @@ -//! A flow of paragraphs and other block-level nodes. - -use super::prelude::*; -use super::{AlignNode, ParNode, PlaceNode, SpacingKind, TextNode}; +use super::{AlignNode, PlaceNode, SpacingKind}; +use crate::library::prelude::*; +use crate::library::text::{ParNode, TextNode}; /// Arrange spacing, paragraphs and other block-level nodes into a flow. /// diff --git a/src/library/grid.rs b/src/library/layout/grid.rs index 2d8cb462..63cd83b1 100644 --- a/src/library/grid.rs +++ b/src/library/layout/grid.rs @@ -1,6 +1,4 @@ -//! Layout along a row and column raster. - -use super::prelude::*; +use crate::library::prelude::*; /// Arrange nodes in a grid. #[derive(Debug, Hash)] diff --git a/src/library/hide.rs b/src/library/layout/hide.rs index 89aea6d3..861a1208 100644 --- a/src/library/hide.rs +++ b/src/library/layout/hide.rs @@ -1,6 +1,4 @@ -//! Hiding of nodes without affecting layout. - -use super::prelude::*; +use crate::library::prelude::*; /// Hide a node without affecting layout. #[derive(Debug, Hash)] diff --git a/src/library/layout/mod.rs b/src/library/layout/mod.rs new file mode 100644 index 00000000..944548ab --- /dev/null +++ b/src/library/layout/mod.rs @@ -0,0 +1,27 @@ +//! Composable layouts. + +mod align; +mod columns; +mod container; +mod flow; +mod grid; +mod hide; +mod pad; +mod page; +mod place; +mod spacing; +mod stack; +mod transform; + +pub use align::*; +pub use columns::*; +pub use container::*; +pub use flow::*; +pub use grid::*; +pub use hide::*; +pub use pad::*; +pub use page::*; +pub use place::*; +pub use spacing::*; +pub use stack::*; +pub use transform::*; diff --git a/src/library/pad.rs b/src/library/layout/pad.rs index 174322e4..175a54f0 100644 --- a/src/library/pad.rs +++ b/src/library/layout/pad.rs @@ -1,6 +1,4 @@ -//! Surrounding nodes with extra space. - -use super::prelude::*; +use crate::library::prelude::*; /// Pad a node at the sides. #[derive(Debug, Hash)] diff --git a/src/library/page.rs b/src/library/layout/page.rs index 5281c501..f5d766a5 100644 --- a/src/library/page.rs +++ b/src/library/layout/page.rs @@ -1,10 +1,8 @@ -//! Pages of paper. - use std::fmt::{self, Display, Formatter}; use std::str::FromStr; -use super::prelude::*; use super::ColumnsNode; +use crate::library::prelude::*; /// Layouts its child onto one or multiple pages. #[derive(Clone, PartialEq, Hash)] diff --git a/src/library/place.rs b/src/library/layout/place.rs index 00e72dfb..d65b3836 100644 --- a/src/library/place.rs +++ b/src/library/layout/place.rs @@ -1,7 +1,5 @@ -//! Absolute placement of nodes. - -use super::prelude::*; use super::AlignNode; +use crate::library::prelude::*; /// Place a node at an absolute position. #[derive(Debug, Hash)] diff --git a/src/library/spacing.rs b/src/library/layout/spacing.rs index 6f57c71c..3bebfb14 100644 --- a/src/library/spacing.rs +++ b/src/library/layout/spacing.rs @@ -1,6 +1,4 @@ -//! Horizontal and vertical spacing between nodes. - -use super::prelude::*; +use crate::library::prelude::*; /// Horizontal spacing. pub struct HNode; diff --git a/src/library/stack.rs b/src/library/layout/stack.rs index 8e463f6a..414490ef 100644 --- a/src/library/stack.rs +++ b/src/library/layout/stack.rs @@ -1,7 +1,5 @@ -//! Side-by-side layout of nodes along an axis. - -use super::prelude::*; use super::{AlignNode, SpacingKind}; +use crate::library::prelude::*; /// Arrange nodes and spacing along an axis. #[derive(Debug, Hash)] diff --git a/src/library/transform.rs b/src/library/layout/transform.rs index 897f3144..fafb37a4 100644 --- a/src/library/transform.rs +++ b/src/library/layout/transform.rs @@ -1,7 +1,5 @@ -//! Affine transformations on nodes. - -use super::prelude::*; use crate::geom::Transform; +use crate::library::prelude::*; /// Transform a node without affecting layout. #[derive(Debug, Hash)] @@ -12,6 +10,15 @@ pub struct TransformNode<const T: TransformKind> { pub child: LayoutNode, } +/// Transform a node by translating it without affecting layout. +pub type MoveNode = TransformNode<MOVE>; + +/// Transform a node by rotating it without affecting layout. +pub type RotateNode = TransformNode<ROTATE>; + +/// Transform a node by scaling it without affecting layout. +pub type ScaleNode = TransformNode<SCALE>; + #[class] impl<const T: TransformKind> TransformNode<T> { /// The origin of the transformation. @@ -70,10 +77,10 @@ impl<const T: TransformKind> Layout for TransformNode<T> { pub type TransformKind = usize; /// A translation on the X and Y axes. -pub const MOVE: TransformKind = 0; +const MOVE: TransformKind = 0; /// A rotational transformation. -pub const ROTATE: TransformKind = 1; +const ROTATE: TransformKind = 1; /// A scale transformation. -pub const SCALE: TransformKind = 2; +const SCALE: TransformKind = 2; diff --git a/src/library/mod.rs b/src/library/mod.rs index 590dd331..4a80ef43 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -3,79 +3,11 @@ //! Call [`new`] to obtain a [`Scope`] containing all standard library //! definitions. -pub mod align; -pub mod columns; -pub mod container; -pub mod deco; -pub mod flow; -pub mod grid; -pub mod heading; -pub mod hide; -pub mod image; -pub mod link; -pub mod list; -pub mod math; -pub mod numbering; -pub mod pad; -pub mod page; -pub mod par; -pub mod place; -pub mod raw; -pub mod shape; -pub mod spacing; -pub mod stack; -pub mod table; +pub mod elements; +pub mod layout; +pub mod prelude; pub mod text; -pub mod transform; - pub mod utility; -pub use self::image::*; -pub use align::*; -pub use columns::*; -pub use container::*; -pub use deco::*; -pub use flow::*; -pub use grid::*; -pub use heading::*; -pub use hide::*; -pub use link::*; -pub use list::*; -pub use math::*; -pub use numbering::*; -pub use pad::*; -pub use page::*; -pub use par::*; -pub use place::*; -pub use raw::*; -pub use shape::*; -pub use spacing::*; -pub use stack::*; -pub use table::*; -pub use text::*; -pub use transform::*; -pub use utility::*; - -/// Helpful imports for creating library functionality. -pub mod prelude { - pub use std::fmt::{self, Debug, Formatter}; - pub use std::hash::Hash; - pub use std::num::NonZeroUsize; - pub use std::sync::Arc; - - pub use typst_macros::class; - - pub use crate::diag::{with_alternative, At, StrResult, TypResult}; - pub use crate::eval::{ - Arg, Args, Cast, Construct, Func, Layout, LayoutNode, Merge, Property, Regions, - Scope, Set, Show, ShowNode, Smart, StyleChain, StyleMap, StyleVec, Template, - Value, - }; - pub use crate::frame::*; - pub use crate::geom::*; - pub use crate::syntax::{Span, Spanned}; - pub use crate::util::{EcoString, OptionExt}; - pub use crate::Context; -} use prelude::*; @@ -83,72 +15,74 @@ use prelude::*; pub fn new() -> Scope { let mut std = Scope::new(); - // Structure and semantics. - std.def_class::<PageNode>("page"); - std.def_class::<PagebreakNode>("pagebreak"); - std.def_class::<ParNode>("par"); - std.def_class::<ParbreakNode>("parbreak"); - std.def_class::<LinebreakNode>("linebreak"); - std.def_class::<TextNode>("text"); - std.def_class::<StrongNode>("strong"); - std.def_class::<EmphNode>("emph"); - std.def_class::<RawNode>("raw"); - std.def_class::<MathNode>("math"); - std.def_class::<DecoNode<UNDERLINE>>("underline"); - std.def_class::<DecoNode<STRIKETHROUGH>>("strike"); - std.def_class::<DecoNode<OVERLINE>>("overline"); - std.def_class::<LinkNode>("link"); - std.def_class::<HeadingNode>("heading"); - std.def_class::<ListNode<UNORDERED>>("list"); - std.def_class::<ListNode<ORDERED>>("enum"); - std.def_class::<TableNode>("table"); - std.def_class::<ImageNode>("image"); - std.def_class::<ShapeNode<RECT>>("rect"); - std.def_class::<ShapeNode<SQUARE>>("square"); - std.def_class::<ShapeNode<ELLIPSE>>("ellipse"); - std.def_class::<ShapeNode<CIRCLE>>("circle"); + // Text. + std.def_class::<text::TextNode>("text"); + std.def_class::<text::ParNode>("par"); + std.def_class::<text::ParbreakNode>("parbreak"); + std.def_class::<text::LinebreakNode>("linebreak"); + std.def_class::<text::StrongNode>("strong"); + std.def_class::<text::EmphNode>("emph"); + std.def_class::<text::RawNode>("raw"); + std.def_class::<text::UnderlineNode>("underline"); + std.def_class::<text::StrikethroughNode>("strike"); + std.def_class::<text::OverlineNode>("overline"); + std.def_class::<text::LinkNode>("link"); + + // Elements. + std.def_class::<elements::MathNode>("math"); + std.def_class::<elements::HeadingNode>("heading"); + std.def_class::<elements::ListNode>("list"); + std.def_class::<elements::EnumNode>("enum"); + std.def_class::<elements::TableNode>("table"); + std.def_class::<elements::ImageNode>("image"); + std.def_class::<elements::RectNode>("rect"); + std.def_class::<elements::SquareNode>("square"); + std.def_class::<elements::EllipseNode>("ellipse"); + std.def_class::<elements::CircleNode>("circle"); // Layout. - std.def_class::<HNode>("h"); - std.def_class::<VNode>("v"); - std.def_class::<BoxNode>("box"); - std.def_class::<BlockNode>("block"); - std.def_class::<AlignNode>("align"); - std.def_class::<PadNode>("pad"); - std.def_class::<PlaceNode>("place"); - std.def_class::<TransformNode<MOVE>>("move"); - std.def_class::<TransformNode<SCALE>>("scale"); - std.def_class::<TransformNode<ROTATE>>("rotate"); - std.def_class::<HideNode>("hide"); - std.def_class::<StackNode>("stack"); - std.def_class::<GridNode>("grid"); - std.def_class::<ColumnsNode>("columns"); - std.def_class::<ColbreakNode>("colbreak"); + std.def_class::<layout::PageNode>("page"); + std.def_class::<layout::PagebreakNode>("pagebreak"); + std.def_class::<layout::HNode>("h"); + std.def_class::<layout::VNode>("v"); + std.def_class::<layout::BoxNode>("box"); + std.def_class::<layout::BlockNode>("block"); + std.def_class::<layout::AlignNode>("align"); + std.def_class::<layout::PadNode>("pad"); + std.def_class::<layout::StackNode>("stack"); + std.def_class::<layout::GridNode>("grid"); + std.def_class::<layout::ColumnsNode>("columns"); + std.def_class::<layout::ColbreakNode>("colbreak"); + std.def_class::<layout::PlaceNode>("place"); + std.def_class::<layout::MoveNode>("move"); + std.def_class::<layout::ScaleNode>("scale"); + std.def_class::<layout::RotateNode>("rotate"); + std.def_class::<layout::HideNode>("hide"); // Utility functions. - std.def_func("assert", assert); - std.def_func("type", type_); - std.def_func("repr", repr); - std.def_func("join", join); - std.def_func("int", int); - std.def_func("float", float); - std.def_func("str", str); - std.def_func("abs", abs); - std.def_func("min", min); - std.def_func("max", max); - std.def_func("even", even); - std.def_func("odd", odd); - std.def_func("mod", modulo); - std.def_func("range", range); - std.def_func("rgb", rgb); - std.def_func("cmyk", cmyk); - std.def_func("lower", lower); - std.def_func("upper", upper); - std.def_func("letter", letter); - std.def_func("roman", roman); - std.def_func("symbol", symbol); - std.def_func("len", len); - std.def_func("sorted", sorted); + std.def_func("assert", utility::assert); + std.def_func("type", utility::type_); + std.def_func("repr", utility::repr); + std.def_func("join", utility::join); + std.def_func("int", utility::int); + std.def_func("float", utility::float); + std.def_func("str", utility::str); + std.def_func("abs", utility::abs); + std.def_func("min", utility::min); + std.def_func("max", utility::max); + std.def_func("even", utility::even); + std.def_func("odd", utility::odd); + std.def_func("mod", utility::modulo); + std.def_func("range", utility::range); + std.def_func("rgb", utility::rgb); + std.def_func("cmyk", utility::cmyk); + std.def_func("lower", utility::lower); + std.def_func("upper", utility::upper); + std.def_func("letter", utility::letter); + std.def_func("roman", utility::roman); + std.def_func("symbol", utility::symbol); + std.def_func("len", utility::len); + std.def_func("sorted", utility::sorted); // Predefined colors. std.def_const("black", Color::BLACK); @@ -181,9 +115,9 @@ pub fn new() -> Scope { std.def_const("top", Align::Top); std.def_const("horizon", Align::Horizon); std.def_const("bottom", Align::Bottom); - std.def_const("serif", FontFamily::Serif); - std.def_const("sans-serif", FontFamily::SansSerif); - std.def_const("monospace", FontFamily::Monospace); + std.def_const("serif", text::FontFamily::Serif); + std.def_const("sans-serif", text::FontFamily::SansSerif); + std.def_const("monospace", text::FontFamily::Monospace); std } diff --git a/src/library/prelude.rs b/src/library/prelude.rs new file mode 100644 index 00000000..4d13a655 --- /dev/null +++ b/src/library/prelude.rs @@ -0,0 +1,20 @@ +//! Helpful imports for creating library functionality. + +pub use std::fmt::{self, Debug, Formatter}; +pub use std::hash::Hash; +pub use std::num::NonZeroUsize; +pub use std::sync::Arc; + +pub use typst_macros::class; + +pub use crate::diag::{with_alternative, At, StrResult, TypResult}; +pub use crate::eval::{ + Arg, Args, Array, Cast, Construct, Dict, Func, Layout, LayoutNode, Merge, Property, + Regions, Scope, Set, Show, ShowNode, Smart, StyleChain, StyleMap, StyleVec, Template, + Value, +}; +pub use crate::frame::*; +pub use crate::geom::*; +pub use crate::syntax::{Span, Spanned}; +pub use crate::util::{EcoString, OptionExt}; +pub use crate::Context; diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs new file mode 100644 index 00000000..a288c995 --- /dev/null +++ b/src/library/text/deco.rs @@ -0,0 +1,250 @@ +use kurbo::{BezPath, Line, ParamCurve}; +use ttf_parser::{GlyphId, OutlineBuilder}; + +use super::TextNode; +use crate::font::FontStore; +use crate::library::prelude::*; + +/// Typeset underline, stricken-through or overlined text. +#[derive(Debug, Hash)] +pub struct DecoNode<const L: DecoLine>(pub Template); + +/// Typeset underlined text. +pub type UnderlineNode = DecoNode<UNDERLINE>; + +/// Typeset stricken-through text. +pub type StrikethroughNode = DecoNode<STRIKETHROUGH>; + +/// Typeset overlined text. +pub type OverlineNode = DecoNode<OVERLINE>; + +#[class] +impl<const L: DecoLine> DecoNode<L> { + /// Stroke color of the line, defaults to the text color if `None`. + #[shorthand] + pub const STROKE: Option<Paint> = None; + /// Thickness of the line's strokes (dependent on scaled font size), read + /// from the font tables if `None`. + #[shorthand] + pub const THICKNESS: Option<Linear> = None; + /// Position of the line relative to the baseline (dependent on scaled font + /// size), read from the font tables if `None`. + pub const OFFSET: Option<Linear> = None; + /// Amount that the line will be longer or shorter than its associated text + /// (dependent on scaled font size). + pub const EXTENT: Linear = Linear::zero(); + /// Whether the line skips sections in which it would collide + /// with the glyphs. Does not apply to strikethrough. + pub const EVADE: bool = true; + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::show(Self(args.expect::<Template>("body")?))) + } +} + +impl<const L: DecoLine> Show for DecoNode<L> { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + Ok(styles + .show(self, ctx, [Value::Template(self.0.clone())])? + .unwrap_or_else(|| { + self.0.clone().styled(TextNode::LINES, vec![Decoration { + line: L, + stroke: styles.get(Self::STROKE), + thickness: styles.get(Self::THICKNESS), + offset: styles.get(Self::OFFSET), + extent: styles.get(Self::EXTENT), + evade: styles.get(Self::EVADE), + }]) + })) + } +} + +/// Defines a line that is positioned over, under or on top of text. +/// +/// For more details, see [`DecoNode`]. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Decoration { + pub line: DecoLine, + pub stroke: Option<Paint>, + pub thickness: Option<Linear>, + pub offset: Option<Linear>, + pub extent: Linear, + pub evade: bool, +} + +/// A kind of decorative line. +pub type DecoLine = usize; + +/// A line under text. +pub const UNDERLINE: DecoLine = 0; + +/// A line through text. +pub const STRIKETHROUGH: DecoLine = 1; + +/// A line over text. +pub const OVERLINE: DecoLine = 2; + +/// Add line decorations to a single run of shaped text. +pub fn decorate( + frame: &mut Frame, + deco: &Decoration, + fonts: &FontStore, + text: &Text, + pos: Point, + width: Length, +) { + let face = fonts.get(text.face_id); + let metrics = match deco.line { + STRIKETHROUGH => face.strikethrough, + OVERLINE => face.overline, + UNDERLINE | _ => face.underline, + }; + + let evade = deco.evade && deco.line != STRIKETHROUGH; + let extent = deco.extent.resolve(text.size); + let offset = deco + .offset + .map(|s| s.resolve(text.size)) + .unwrap_or(-metrics.position.resolve(text.size)); + + let stroke = Stroke { + paint: deco.stroke.unwrap_or(text.fill), + thickness: deco + .thickness + .map(|s| s.resolve(text.size)) + .unwrap_or(metrics.thickness.resolve(text.size)), + }; + + let gap_padding = 0.08 * text.size; + let min_width = 0.162 * text.size; + + let mut start = pos.x - extent; + let end = pos.x + (width + 2.0 * extent); + + let mut push_segment = |from: Length, to: Length| { + let origin = Point::new(from, pos.y + offset); + let target = Point::new(to - from, Length::zero()); + + if target.x >= min_width || !evade { + let shape = Shape::stroked(Geometry::Line(target), stroke); + frame.push(origin, Element::Shape(shape)); + } + }; + + if !evade { + push_segment(start, end); + return; + } + + let line = Line::new( + kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), + kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), + ); + + let mut x = pos.x; + let mut intersections = vec![]; + + for glyph in text.glyphs.iter() { + let dx = glyph.x_offset.resolve(text.size) + x; + let mut builder = BezPathBuilder::new(face.units_per_em, text.size, dx.to_raw()); + + let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); + let path = builder.finish(); + + x += glyph.x_advance.resolve(text.size); + + // Only do the costly segments intersection test if the line + // intersects the bounding box. + if bbox.map_or(false, |bbox| { + let y_min = -face.to_em(bbox.y_max).resolve(text.size); + let y_max = -face.to_em(bbox.y_min).resolve(text.size); + + offset >= y_min && offset <= y_max + }) { + // Find all intersections of segments with the line. + intersections.extend( + path.segments() + .flat_map(|seg| seg.intersect_line(line)) + .map(|is| Length::raw(line.eval(is.line_t).x)), + ); + } + } + + // When emitting the decorative line segments, we move from left to + // right. The intersections are not necessarily in this order, yet. + intersections.sort(); + + for gap in intersections.chunks_exact(2) { + let l = gap[0] - gap_padding; + let r = gap[1] + gap_padding; + + if start >= end { + break; + } + + if start >= l { + start = r; + continue; + } + + push_segment(start, l); + start = r; + } + + if start < end { + push_segment(start, end); + } +} + +/// Builds a kurbo [`BezPath`] for a glyph. +struct BezPathBuilder { + path: BezPath, + units_per_em: f64, + font_size: Length, + x_offset: f64, +} + +impl BezPathBuilder { + fn new(units_per_em: f64, font_size: Length, x_offset: f64) -> Self { + Self { + path: BezPath::new(), + units_per_em, + font_size, + x_offset, + } + } + + fn finish(self) -> BezPath { + self.path + } + + fn p(&self, x: f32, y: f32) -> kurbo::Point { + kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) + } + + fn s(&self, v: f32) -> f64 { + Em::from_units(v, self.units_per_em).resolve(self.font_size).to_raw() + } +} + +impl OutlineBuilder for BezPathBuilder { + fn move_to(&mut self, x: f32, y: f32) { + self.path.move_to(self.p(x, y)); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.path.line_to(self.p(x, y)); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.path.quad_to(self.p(x1, y1), self.p(x, y)); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); + } + + fn close(&mut self) { + self.path.close_path(); + } +} diff --git a/src/library/link.rs b/src/library/text/link.rs index 4a153d46..29f41927 100644 --- a/src/library/link.rs +++ b/src/library/text/link.rs @@ -1,7 +1,5 @@ -//! Hyperlinking. - -use super::prelude::*; use super::TextNode; +use crate::library::prelude::*; use crate::util::EcoString; /// Link text and other elements to an URL. diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs new file mode 100644 index 00000000..0df59007 --- /dev/null +++ b/src/library/text/mod.rs @@ -0,0 +1,409 @@ +mod deco; +mod link; +mod par; +mod raw; +mod shaping; + +pub use deco::*; +pub use link::*; +pub use par::*; +pub use raw::*; +pub use shaping::*; + +use std::borrow::Cow; +use std::ops::BitXor; + +use ttf_parser::Tag; + +use crate::font::{Face, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; +use crate::library::prelude::*; +use crate::util::EcoString; + +/// A single run of text with the same style. +#[derive(Hash)] +pub struct TextNode; + +#[class] +impl TextNode { + /// A prioritized sequence of font families. + #[variadic] + pub const FAMILY: Vec<FontFamily> = vec![FontFamily::SansSerif]; + /// The serif font family/families. + pub const SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")]; + /// The sans-serif font family/families. + pub const SANS_SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")]; + /// The monospace font family/families. + pub const MONOSPACE: 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; + /// The glyph fill color. + #[shorthand] + pub const FILL: Paint = Color::BLACK.into(); + + /// The size of the glyphs. + #[shorthand] + #[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![]; + + /// Whether the font weight should be increased by 300. + #[skip] + #[fold(bool::bitxor)] + pub const STRONG: bool = false; + /// Whether the the font style should be inverted. + #[skip] + #[fold(bool::bitxor)] + pub const EMPH: bool = false; + /// Whether a monospace font should be preferred. + #[skip] + pub const MONOSPACED: bool = false; + /// The case transformation that should be applied to the next. + #[skip] + pub const CASE: Option<Case> = None; + /// Decorative lines. + #[skip] + #[fold(|a, b| a.into_iter().chain(b).collect())] + pub const LINES: Vec<Decoration> = vec![]; + /// An URL the text should link to. + #[skip] + pub const LINK: Option<EcoString> = None; + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + // The text constructor is special: It doesn't create a text node. + // Instead, it leaves the passed argument structurally unchanged, but + // styles all text in it. + args.expect("body") + } +} + +/// Strong text, rendered in boldface. +#[derive(Debug, Hash)] +pub struct StrongNode(pub Template); + +#[class] +impl StrongNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::show(Self(args.expect("body")?))) + } +} + +impl Show for StrongNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + Ok(styles + .show(self, ctx, [Value::Template(self.0.clone())])? + .unwrap_or_else(|| self.0.clone().styled(TextNode::STRONG, true))) + } +} + +/// Emphasized text, rendered with an italic face. +#[derive(Debug, Hash)] +pub struct EmphNode(pub Template); + +#[class] +impl EmphNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::show(Self(args.expect("body")?))) + } +} + +impl Show for EmphNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + Ok(styles + .show(self, ctx, [Value::Template(self.0.clone())])? + .unwrap_or_else(|| self.0.clone().styled(TextNode::EMPH, true))) + } +} + +/// 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), +} + +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), + } + } +} + +dynamic! { + FontFamily: "font family", + Value::Str(string) => Self::Named(NamedFamily::new(&string)), +} + +castable! { + Vec<FontFamily>, + Expected: "string, generic family or array thereof", + Value::Str(string) => vec![FontFamily::Named(NamedFamily::new(&string))], + Value::Array(values) => { + values.into_iter().filter_map(|v| v.cast().ok()).collect() + }, + @family: FontFamily => vec![family.clone()], +} + +/// A specific font family like "Arial". +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct NamedFamily(EcoString); + +impl NamedFamily { + /// Create a named font family variant. + pub fn new(string: &str) -> Self { + Self(string.to_lowercase().into()) + } + + /// The lowercased family name. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Debug for NamedFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +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(), +} + +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) => 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")?, + }, +} + +castable! { + FontStretch, + Expected: "relative", + Value::Relative(v) => Self::from_ratio(v.get() as f32), +} + +castable! { + Em, + Expected: "float", + Value::Float(v) => Self::new(v), +} + +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)) + } + + /// Get the value, guaranteed to be 1-20. + pub fn get(self) -> u8 { + self.0 + } +} + +castable! { + StylisticSet, + Expected: "integer", + Value::Int(v) => match v { + 1 ..= 20 => Self::new(v as u8), + _ => Err("must be between 1 and 20")?, + }, +} + +/// 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, +} + +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""#)?, + }, +} + +/// 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, +} + +castable! { + NumberWidth, + Expected: "string", + Value::Str(string) => match string.as_str() { + "proportional" => Self::Proportional, + "tabular" => Self::Tabular, + _ => Err(r#"expected "proportional" or "tabular""#)?, + }, +} + +/// How to position numbers. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberPosition { + /// Numbers are positioned on the same baseline as text. + Normal, + /// Numbers are smaller and placed at the bottom. ("subs") + Subscript, + /// Numbers are smaller and placed at the top. ("sups") + Superscript, +} + +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(), +} + +/// A case transformation on text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Case { + /// Everything is uppercased. + Upper, + /// Everything is lowercased. + Lower, +} + +impl Case { + /// Apply the case to a string of text. + pub fn apply(self, text: &str) -> String { + match self { + Self::Upper => text.to_uppercase(), + Self::Lower => text.to_lowercase(), + } + } +} diff --git a/src/library/par.rs b/src/library/text/par.rs index 6d5a0597..812231c2 100644 --- a/src/library/par.rs +++ b/src/library/text/par.rs @@ -1,14 +1,13 @@ -//! Paragraph layout. - use std::sync::Arc; use either::Either; use unicode_bidi::{BidiInfo, Level}; use xi_unicode::LineBreakIterator; -use super::prelude::*; -use super::{shape, ShapedText, SpacingKind, TextNode}; +use super::{shape, ShapedText, TextNode}; use crate::font::FontStore; +use crate::library::layout::SpacingKind; +use crate::library::prelude::*; use crate::util::{ArcExt, EcoString, RangeExt, SliceExt}; /// Arrange text, spacing and inline-level nodes into a paragraph. @@ -127,118 +126,24 @@ impl Layout for ParNode { ) -> TypResult<Vec<Arc<Frame>>> { // Collect all text into one string and perform BiDi analysis. let text = self.collect_text(); - let level = Level::from_dir(styles.get(Self::DIR)); - let bidi = BidiInfo::new(&text, level); + let bidi = BidiInfo::new(&text, match styles.get(Self::DIR) { + Dir::LTR => Some(Level::ltr()), + Dir::RTL => Some(Level::rtl()), + _ => None, + }); // Prepare paragraph layout by building a representation on which we can // do line breaking without layouting each and every line from scratch. let par = ParLayout::new(ctx, self, bidi, regions, &styles)?; // Break the paragraph into lines. - let lines = break_lines(&mut ctx.fonts, &par, regions.first.x); + let lines = break_into_lines(&mut ctx.fonts, &par, regions.first.x); // Stack the lines into one frame per region. Ok(stack_lines(&ctx.fonts, lines, regions, styles)) } } -/// Perform line breaking. -fn break_lines<'a>( - fonts: &mut FontStore, - par: &'a ParLayout<'a>, - width: Length, -) -> Vec<LineLayout<'a>> { - // The already determined lines and the current line attempt. - let mut lines = vec![]; - let mut start = 0; - let mut last = None; - - // Find suitable line breaks. - for (end, mandatory) in LineBreakIterator::new(&par.bidi.text) { - // Compute the line and its size. - let mut line = par.line(fonts, start .. end, mandatory); - - // If the line doesn't fit anymore, we push the last fitting attempt - // into the stack and rebuild the line from its end. The resulting - // line cannot be broken up further. - if !width.fits(line.size.x) { - if let Some((last_line, last_end)) = last.take() { - lines.push(last_line); - start = last_end; - line = par.line(fonts, start .. end, mandatory); - } - } - - // Finish the current line if there is a mandatory line break (i.e. - // due to "\n") or if the line doesn't fit horizontally already - // since then no shorter line will be possible. - if mandatory || !width.fits(line.size.x) { - lines.push(line); - start = end; - last = None; - } else { - last = Some((line, end)); - } - } - - if let Some((line, _)) = last { - lines.push(line); - } - - lines -} - -/// Combine the lines into one frame per region. -fn stack_lines( - fonts: &FontStore, - lines: Vec<LineLayout>, - regions: &Regions, - styles: StyleChain, -) -> Vec<Arc<Frame>> { - let em = styles.get(TextNode::SIZE).abs; - let leading = styles.get(ParNode::LEADING).resolve(em); - let align = styles.get(ParNode::ALIGN); - let justify = styles.get(ParNode::JUSTIFY); - - // Determine the paragraph's width: Full width of the region if we - // should expand or there's fractional spacing, fit-to-width otherwise. - let mut width = regions.first.x; - if !regions.expand.x && lines.iter().all(|line| line.fr.is_zero()) { - width = lines.iter().map(|line| line.size.x).max().unwrap_or_default(); - } - - // State for final frame building. - let mut regions = regions.clone(); - let mut finished = vec![]; - let mut first = true; - let mut output = Frame::new(Size::with_x(width)); - - // Stack the lines into one frame per region. - for line in lines { - while !regions.first.y.fits(line.size.y) && !regions.in_last() { - finished.push(Arc::new(output)); - output = Frame::new(Size::with_x(width)); - regions.next(); - first = true; - } - - if !first { - output.size.y += leading; - } - - let frame = line.build(fonts, width, align, justify); - let pos = Point::with_y(output.size.y); - output.size.y += frame.size.y; - output.merge_frame(pos, frame); - - regions.first.y -= line.size.y + leading; - first = false; - } - - finished.push(Arc::new(output)); - finished -} - impl Debug for ParNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("Par ")?; @@ -337,7 +242,8 @@ impl<'a> ParLayout<'a> { cursor += count; let subrange = start .. cursor; let text = &bidi.text[subrange.clone()]; - let shaped = shape(&mut ctx.fonts, text, styles, level.dir()); + let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; + let shaped = shape(&mut ctx.fonts, text, styles, dir); items.push(ParItem::Text(shaped)); ranges.push(subrange); } @@ -613,22 +519,99 @@ impl<'a> LineLayout<'a> { } } -/// Additional methods for BiDi levels. -trait LevelExt: Sized { - fn from_dir(dir: Dir) -> Option<Self>; - fn dir(self) -> Dir; -} +/// Perform line breaking. +fn break_into_lines<'a>( + fonts: &mut FontStore, + par: &'a ParLayout<'a>, + width: Length, +) -> Vec<LineLayout<'a>> { + // The already determined lines and the current line attempt. + let mut lines = vec![]; + let mut start = 0; + let mut last = None; -impl LevelExt for Level { - fn from_dir(dir: Dir) -> Option<Self> { - match dir { - Dir::LTR => Some(Level::ltr()), - Dir::RTL => Some(Level::rtl()), - _ => None, + // Find suitable line breaks. + for (end, mandatory) in LineBreakIterator::new(&par.bidi.text) { + // Compute the line and its size. + let mut line = par.line(fonts, start .. end, mandatory); + + // If the line doesn't fit anymore, we push the last fitting attempt + // into the stack and rebuild the line from its end. The resulting + // line cannot be broken up further. + if !width.fits(line.size.x) { + if let Some((last_line, last_end)) = last.take() { + lines.push(last_line); + start = last_end; + line = par.line(fonts, start .. end, mandatory); + } + } + + // Finish the current line if there is a mandatory line break (i.e. + // due to "\n") or if the line doesn't fit horizontally already + // since then no shorter line will be possible. + if mandatory || !width.fits(line.size.x) { + lines.push(line); + start = end; + last = None; + } else { + last = Some((line, end)); } } - fn dir(self) -> Dir { - if self.is_ltr() { Dir::LTR } else { Dir::RTL } + if let Some((line, _)) = last { + lines.push(line); + } + + lines +} + +/// Combine the lines into one frame per region. +fn stack_lines( + fonts: &FontStore, + lines: Vec<LineLayout>, + regions: &Regions, + styles: StyleChain, +) -> Vec<Arc<Frame>> { + let em = styles.get(TextNode::SIZE).abs; + let leading = styles.get(ParNode::LEADING).resolve(em); + let align = styles.get(ParNode::ALIGN); + let justify = styles.get(ParNode::JUSTIFY); + + // Determine the paragraph's width: Full width of the region if we + // should expand or there's fractional spacing, fit-to-width otherwise. + let mut width = regions.first.x; + if !regions.expand.x && lines.iter().all(|line| line.fr.is_zero()) { + width = lines.iter().map(|line| line.size.x).max().unwrap_or_default(); + } + + // State for final frame building. + let mut regions = regions.clone(); + let mut finished = vec![]; + let mut first = true; + let mut output = Frame::new(Size::with_x(width)); + + // Stack the lines into one frame per region. + for line in lines { + while !regions.first.y.fits(line.size.y) && !regions.in_last() { + finished.push(Arc::new(output)); + output = Frame::new(Size::with_x(width)); + regions.next(); + first = true; + } + + if !first { + output.size.y += leading; + } + + let frame = line.build(fonts, width, align, justify); + let pos = Point::with_y(output.size.y); + output.size.y += frame.size.y; + output.merge_frame(pos, frame); + + regions.first.y -= line.size.y + leading; + first = false; } + + finished.push(Arc::new(output)); + finished } diff --git a/src/library/raw.rs b/src/library/text/raw.rs index 785eeac5..97857f11 100644 --- a/src/library/raw.rs +++ b/src/library/text/raw.rs @@ -1,12 +1,10 @@ -//! Monospaced text and code. - use once_cell::sync::Lazy; use syntect::easy::HighlightLines; use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet}; use syntect::parsing::SyntaxSet; -use super::prelude::*; -use crate::library::TextNode; +use crate::library::prelude::*; +use crate::library::text::TextNode; use crate::source::SourceId; use crate::syntax::{self, RedNode}; diff --git a/src/library/text.rs b/src/library/text/shaping.rs index b76b60ee..26c8daf3 100644 --- a/src/library/text.rs +++ b/src/library/text/shaping.rs @@ -1,410 +1,11 @@ -//! Text shaping and styling. +use std::ops::Range; -use std::borrow::Cow; -use std::fmt::{self, Debug, Formatter}; -use std::ops::{BitXor, Range}; - -use kurbo::{BezPath, Line, ParamCurve}; use rustybuzz::{Feature, UnicodeBuffer}; -use ttf_parser::{GlyphId, OutlineBuilder, Tag}; - -use super::prelude::*; -use super::Decoration; -use crate::font::{ - Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight, - VerticalFontMetric, -}; -use crate::geom::{Dir, Em, Length, Point, Size}; -use crate::util::{EcoString, SliceExt}; - -/// A single run of text with the same style. -#[derive(Hash)] -pub struct TextNode; - -#[class] -impl TextNode { - /// A prioritized sequence of font families. - #[variadic] - pub const FAMILY: Vec<FontFamily> = vec![FontFamily::SansSerif]; - /// The serif font family/families. - pub const SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")]; - /// The sans-serif font family/families. - pub const SANS_SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")]; - /// The monospace font family/families. - pub const MONOSPACE: 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; - /// The glyph fill color. - #[shorthand] - pub const FILL: Paint = Color::BLACK.into(); - - /// The size of the glyphs. - #[shorthand] - #[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![]; - - /// Whether the font weight should be increased by 300. - #[skip] - #[fold(bool::bitxor)] - pub const STRONG: bool = false; - /// Whether the the font style should be inverted. - #[skip] - #[fold(bool::bitxor)] - pub const EMPH: bool = false; - /// Whether a monospace font should be preferred. - #[skip] - pub const MONOSPACED: bool = false; - /// The case transformation that should be applied to the next. - #[skip] - pub const CASE: Option<Case> = None; - /// Decorative lines. - #[skip] - #[fold(|a, b| a.into_iter().chain(b).collect())] - pub const LINES: Vec<Decoration> = vec![]; - /// An URL the text should link to. - #[skip] - pub const LINK: Option<EcoString> = None; - - fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { - // The text constructor is special: It doesn't create a text node. - // Instead, it leaves the passed argument structurally unchanged, but - // styles all text in it. - args.expect("body") - } -} - -/// Strong text, rendered in boldface. -#[derive(Debug, Hash)] -pub struct StrongNode(pub Template); - -#[class] -impl StrongNode { - fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { - Ok(Template::show(Self(args.expect("body")?))) - } -} - -impl Show for StrongNode { - fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { - Ok(styles - .show(self, ctx, [Value::Template(self.0.clone())])? - .unwrap_or_else(|| self.0.clone().styled(TextNode::STRONG, true))) - } -} - -/// Emphasized text, rendered with an italic face. -#[derive(Debug, Hash)] -pub struct EmphNode(pub Template); - -#[class] -impl EmphNode { - fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { - Ok(Template::show(Self(args.expect("body")?))) - } -} - -impl Show for EmphNode { - fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { - Ok(styles - .show(self, ctx, [Value::Template(self.0.clone())])? - .unwrap_or_else(|| self.0.clone().styled(TextNode::EMPH, true))) - } -} - -/// 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), -} - -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), - } - } -} - -dynamic! { - FontFamily: "font family", - Value::Str(string) => Self::Named(NamedFamily::new(&string)), -} - -castable! { - Vec<FontFamily>, - Expected: "string, generic family or array thereof", - Value::Str(string) => vec![FontFamily::Named(NamedFamily::new(&string))], - Value::Array(values) => { - values.into_iter().filter_map(|v| v.cast().ok()).collect() - }, - @family: FontFamily => vec![family.clone()], -} - -/// A specific font family like "Arial". -#[derive(Clone, Eq, PartialEq, Hash)] -pub struct NamedFamily(EcoString); - -impl NamedFamily { - /// Create a named font family variant. - pub fn new(string: &str) -> Self { - Self(string.to_lowercase().into()) - } - - /// The lowercased family name. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl Debug for NamedFamily { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -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(), -} - -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) => 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")?, - }, -} - -castable! { - FontStretch, - Expected: "relative", - Value::Relative(v) => Self::from_ratio(v.get() as f32), -} - -castable! { - Em, - Expected: "float", - Value::Float(v) => Self::new(v), -} - -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)) - } - - /// Get the value, guaranteed to be 1-20. - pub fn get(self) -> u8 { - self.0 - } -} - -castable! { - StylisticSet, - Expected: "integer", - Value::Int(v) => match v { - 1 ..= 20 => Self::new(v as u8), - _ => Err("must be between 1 and 20")?, - }, -} - -/// 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, -} - -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""#)?, - }, -} - -/// 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, -} - -castable! { - NumberWidth, - Expected: "string", - Value::Str(string) => match string.as_str() { - "proportional" => Self::Proportional, - "tabular" => Self::Tabular, - _ => Err(r#"expected "proportional" or "tabular""#)?, - }, -} - -/// How to position numbers. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum NumberPosition { - /// Numbers are positioned on the same baseline as text. - Normal, - /// Numbers are smaller and placed at the bottom. ("subs") - Subscript, - /// Numbers are smaller and placed at the top. ("sups") - Superscript, -} - -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(), -} - -/// A case transformation on text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Case { - /// Everything is uppercased. - Upper, - /// Everything is lowercased. - Lower, -} - -impl Case { - /// Apply the case to a string of text. - pub fn apply(self, text: &str) -> String { - match self { - Self::Upper => text.to_uppercase(), - Self::Lower => text.to_lowercase(), - } - } -} +use super::*; +use crate::font::{FaceId, FontStore, FontVariant}; +use crate::library::prelude::*; +use crate::util::SliceExt; /// The result of shaping text. /// @@ -448,9 +49,11 @@ pub struct ShapedGlyph { pub is_space: bool, } -/// A visual side. +/// A side you can go toward. enum Side { + /// Go toward the west. Left, + /// Go toward the east. Right, } @@ -947,168 +550,3 @@ fn measure( (Size::new(width, top + bottom), top) } - -/// Add line decorations to a single run of shaped text. -fn decorate( - frame: &mut Frame, - deco: &Decoration, - fonts: &FontStore, - text: &Text, - pos: Point, - width: Length, -) { - let face = fonts.get(text.face_id); - let metrics = match deco.line { - super::STRIKETHROUGH => face.strikethrough, - super::OVERLINE => face.overline, - super::UNDERLINE | _ => face.underline, - }; - - let evade = deco.evade && deco.line != super::STRIKETHROUGH; - let extent = deco.extent.resolve(text.size); - let offset = deco - .offset - .map(|s| s.resolve(text.size)) - .unwrap_or(-metrics.position.resolve(text.size)); - - let stroke = Stroke { - paint: deco.stroke.unwrap_or(text.fill), - thickness: deco - .thickness - .map(|s| s.resolve(text.size)) - .unwrap_or(metrics.thickness.resolve(text.size)), - }; - - let gap_padding = 0.08 * text.size; - let min_width = 0.162 * text.size; - - let mut start = pos.x - extent; - let end = pos.x + (width + 2.0 * extent); - - let mut push_segment = |from: Length, to: Length| { - let origin = Point::new(from, pos.y + offset); - let target = Point::new(to - from, Length::zero()); - - if target.x >= min_width || !evade { - let shape = Shape::stroked(Geometry::Line(target), stroke); - frame.push(origin, Element::Shape(shape)); - } - }; - - if !evade { - push_segment(start, end); - return; - } - - let line = Line::new( - kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), - kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), - ); - - let mut x = pos.x; - let mut intersections = vec![]; - - for glyph in text.glyphs.iter() { - let dx = glyph.x_offset.resolve(text.size) + x; - let mut builder = BezPathBuilder::new(face.units_per_em, text.size, dx.to_raw()); - - let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); - let path = builder.finish(); - - x += glyph.x_advance.resolve(text.size); - - // Only do the costly segments intersection test if the line - // intersects the bounding box. - if bbox.map_or(false, |bbox| { - let y_min = -face.to_em(bbox.y_max).resolve(text.size); - let y_max = -face.to_em(bbox.y_min).resolve(text.size); - - offset >= y_min && offset <= y_max - }) { - // Find all intersections of segments with the line. - intersections.extend( - path.segments() - .flat_map(|seg| seg.intersect_line(line)) - .map(|is| Length::raw(line.eval(is.line_t).x)), - ); - } - } - - // When emitting the decorative line segments, we move from left to - // right. The intersections are not necessarily in this order, yet. - intersections.sort(); - - for gap in intersections.chunks_exact(2) { - let l = gap[0] - gap_padding; - let r = gap[1] + gap_padding; - - if start >= end { - break; - } - - if start >= l { - start = r; - continue; - } - - push_segment(start, l); - start = r; - } - - if start < end { - push_segment(start, end); - } -} - -/// Builds a kurbo [`BezPath`] for a glyph. -struct BezPathBuilder { - path: BezPath, - units_per_em: f64, - font_size: Length, - x_offset: f64, -} - -impl BezPathBuilder { - fn new(units_per_em: f64, font_size: Length, x_offset: f64) -> Self { - Self { - path: BezPath::new(), - units_per_em, - font_size, - x_offset, - } - } - - fn finish(self) -> BezPath { - self.path - } - - fn p(&self, x: f32, y: f32) -> kurbo::Point { - kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) - } - - fn s(&self, v: f32) -> f64 { - Em::from_units(v, self.units_per_em).resolve(self.font_size).to_raw() - } -} - -impl OutlineBuilder for BezPathBuilder { - fn move_to(&mut self, x: f32, y: f32) { - self.path.move_to(self.p(x, y)); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.path.line_to(self.p(x, y)); - } - - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - self.path.quad_to(self.p(x1, y1), self.p(x, y)); - } - - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); - } - - fn close(&mut self) { - self.path.close_path(); - } -} diff --git a/src/library/utility/math.rs b/src/library/utility/math.rs new file mode 100644 index 00000000..795f39fd --- /dev/null +++ b/src/library/utility/math.rs @@ -0,0 +1,114 @@ +use std::cmp::Ordering; + +use crate::library::prelude::*; + +/// The absolute value of a numeric value. +pub fn abs(_: &mut Context, args: &mut Args) -> TypResult<Value> { + let Spanned { v, span } = args.expect("numeric value")?; + Ok(match v { + Value::Int(v) => Value::Int(v.abs()), + Value::Float(v) => Value::Float(v.abs()), + Value::Length(v) => Value::Length(v.abs()), + Value::Angle(v) => Value::Angle(v.abs()), + Value::Relative(v) => Value::Relative(v.abs()), + Value::Fractional(v) => Value::Fractional(v.abs()), + Value::Linear(_) => bail!(span, "cannot take absolute value of a linear"), + v => bail!(span, "expected numeric value, found {}", v.type_name()), + }) +} + +/// The minimum of a sequence of values. +pub fn min(_: &mut Context, args: &mut Args) -> TypResult<Value> { + minmax(args, Ordering::Less) +} + +/// The maximum of a sequence of values. +pub fn max(_: &mut Context, args: &mut Args) -> TypResult<Value> { + minmax(args, Ordering::Greater) +} + +/// Find the minimum or maximum of a sequence of values. +fn minmax(args: &mut Args, goal: Ordering) -> TypResult<Value> { + let mut extremum = args.expect::<Value>("value")?; + for Spanned { v, span } in args.all::<Spanned<Value>>()? { + match v.partial_cmp(&extremum) { + Some(ordering) => { + if ordering == goal { + extremum = v; + } + } + None => bail!( + span, + "cannot compare {} with {}", + extremum.type_name(), + v.type_name(), + ), + } + } + Ok(extremum) +} + +/// Whether an integer is even. +pub fn even(_: &mut Context, args: &mut Args) -> TypResult<Value> { + Ok(Value::Bool(args.expect::<i64>("integer")? % 2 == 0)) +} + +/// Whether an integer is odd. +pub fn odd(_: &mut Context, args: &mut Args) -> TypResult<Value> { + Ok(Value::Bool(args.expect::<i64>("integer")? % 2 != 0)) +} + +/// The modulo of two numbers. +pub fn modulo(_: &mut Context, args: &mut Args) -> TypResult<Value> { + let Spanned { v: v1, span: span1 } = args.expect("integer or float")?; + let Spanned { v: v2, span: span2 } = args.expect("integer or float")?; + + let (a, b) = match (v1, v2) { + (Value::Int(a), Value::Int(b)) => match a.checked_rem(b) { + Some(res) => return Ok(Value::Int(res)), + None => bail!(span2, "divisor must not be zero"), + }, + (Value::Int(a), Value::Float(b)) => (a as f64, b), + (Value::Float(a), Value::Int(b)) => (a, b as f64), + (Value::Float(a), Value::Float(b)) => (a, b), + (Value::Int(_), b) | (Value::Float(_), b) => bail!( + span2, + format!("expected integer or float, found {}", b.type_name()) + ), + (a, _) => bail!( + span1, + format!("expected integer or float, found {}", a.type_name()) + ), + }; + + if b == 0.0 { + bail!(span2, "divisor must not be zero"); + } + + Ok(Value::Float(a % b)) +} + +/// Create a sequence of numbers. +pub fn range(_: &mut Context, args: &mut Args) -> TypResult<Value> { + let first = args.expect::<i64>("end")?; + let (start, end) = match args.eat::<i64>()? { + Some(second) => (first, second), + None => (0, first), + }; + + let step: i64 = match args.named("step")? { + Some(Spanned { v: 0, span }) => bail!(span, "step must not be zero"), + Some(Spanned { v, .. }) => v, + None => 1, + }; + + let mut x = start; + let mut seq = vec![]; + + while x.cmp(&end) == 0.cmp(&step) { + seq.push(Value::Int(x)); + x += step; + } + + Ok(Value::Array(Array::from_vec(seq))) +} diff --git a/src/library/utility.rs b/src/library/utility/mod.rs index ceae20bf..886cdc13 100644 --- a/src/library/utility.rs +++ b/src/library/utility/mod.rs @@ -1,11 +1,15 @@ //! Computational utility functions. -use std::cmp::Ordering; +mod math; +mod numbering; + +pub use math::*; +pub use numbering::*; + use std::str::FromStr; -use super::prelude::*; -use super::{Case, TextNode}; -use crate::eval::Array; +use crate::library::prelude::*; +use crate::library::text::{Case, TextNode}; /// Ensure that a condition is fulfilled. pub fn assert(_: &mut Context, args: &mut Args) -> TypResult<Value> { @@ -75,7 +79,7 @@ pub fn float(_: &mut Context, args: &mut Args) -> TypResult<Value> { })) } -/// Try to convert a value to a string. +/// Cconvert a value to a string. pub fn str(_: &mut Context, args: &mut Args) -> TypResult<Value> { let Spanned { v, span } = args.expect("value")?; Ok(Value::Str(match v { @@ -141,115 +145,19 @@ pub fn cmyk(_: &mut Context, args: &mut Args) -> TypResult<Value> { Ok(Value::Color(CmykColor::new(c, m, y, k).into())) } -/// The absolute value of a numeric value. -pub fn abs(_: &mut Context, args: &mut Args) -> TypResult<Value> { - let Spanned { v, span } = args.expect("numeric value")?; - Ok(match v { - Value::Int(v) => Value::Int(v.abs()), - Value::Float(v) => Value::Float(v.abs()), - Value::Length(v) => Value::Length(v.abs()), - Value::Angle(v) => Value::Angle(v.abs()), - Value::Relative(v) => Value::Relative(v.abs()), - Value::Fractional(v) => Value::Fractional(v.abs()), - Value::Linear(_) => bail!(span, "cannot take absolute value of a linear"), - v => bail!(span, "expected numeric value, found {}", v.type_name()), - }) -} - -/// The minimum of a sequence of values. -pub fn min(_: &mut Context, args: &mut Args) -> TypResult<Value> { - minmax(args, Ordering::Less) -} - -/// The maximum of a sequence of values. -pub fn max(_: &mut Context, args: &mut Args) -> TypResult<Value> { - minmax(args, Ordering::Greater) -} - -/// Whether an integer is even. -pub fn even(_: &mut Context, args: &mut Args) -> TypResult<Value> { - Ok(Value::Bool(args.expect::<i64>("integer")? % 2 == 0)) -} - -/// Whether an integer is odd. -pub fn odd(_: &mut Context, args: &mut Args) -> TypResult<Value> { - Ok(Value::Bool(args.expect::<i64>("integer")? % 2 != 0)) -} - -/// The modulo of two numbers. -pub fn modulo(_: &mut Context, args: &mut Args) -> TypResult<Value> { - let Spanned { v: v1, span: span1 } = args.expect("integer or float")?; - let Spanned { v: v2, span: span2 } = args.expect("integer or float")?; - - let (a, b) = match (v1, v2) { - (Value::Int(a), Value::Int(b)) => match a.checked_rem(b) { - Some(res) => return Ok(Value::Int(res)), - None => bail!(span2, "divisor must not be zero"), - }, - (Value::Int(a), Value::Float(b)) => (a as f64, b), - (Value::Float(a), Value::Int(b)) => (a, b as f64), - (Value::Float(a), Value::Float(b)) => (a, b), - (Value::Int(_), b) | (Value::Float(_), b) => bail!( - span2, - format!("expected integer or float, found {}", b.type_name()) - ), - (a, _) => bail!( - span1, - format!("expected integer or float, found {}", a.type_name()) +/// The length of a string, an array or a dictionary. +pub fn len(_: &mut Context, args: &mut Args) -> TypResult<Value> { + let Spanned { v, span } = args.expect("collection")?; + Ok(Value::Int(match v { + Value::Str(v) => v.len() as i64, + Value::Array(v) => v.len(), + Value::Dict(v) => v.len(), + v => bail!( + span, + "expected string, array or dictionary, found {}", + v.type_name(), ), - }; - - if b == 0.0 { - bail!(span2, "divisor must not be zero"); - } - - Ok(Value::Float(a % b)) -} - -/// Find the minimum or maximum of a sequence of values. -fn minmax(args: &mut Args, goal: Ordering) -> TypResult<Value> { - let mut extremum = args.expect::<Value>("value")?; - for Spanned { v, span } in args.all::<Spanned<Value>>()? { - match v.partial_cmp(&extremum) { - Some(ordering) => { - if ordering == goal { - extremum = v; - } - } - None => bail!( - span, - "cannot compare {} with {}", - extremum.type_name(), - v.type_name(), - ), - } - } - Ok(extremum) -} - -/// Create a sequence of numbers. -pub fn range(_: &mut Context, args: &mut Args) -> TypResult<Value> { - let first = args.expect::<i64>("end")?; - let (start, end) = match args.eat::<i64>()? { - Some(second) => (first, second), - None => (0, first), - }; - - let step: i64 = match args.named("step")? { - Some(Spanned { v: 0, span }) => bail!(span, "step must not be zero"), - Some(Spanned { v, .. }) => v, - None => 1, - }; - - let mut x = start; - let mut seq = vec![]; - - while x.cmp(&end) == 0.cmp(&step) { - seq.push(Value::Int(x)); - x += step; - } - - Ok(Value::Array(Array::from_vec(seq))) + })) } /// Convert a string to lowercase. @@ -272,21 +180,6 @@ fn case(case: Case, args: &mut Args) -> TypResult<Value> { }) } -/// The length of a string, an array or a dictionary. -pub fn len(_: &mut Context, args: &mut Args) -> TypResult<Value> { - let Spanned { v, span } = args.expect("collection")?; - Ok(Value::Int(match v { - Value::Str(v) => v.len() as i64, - Value::Array(v) => v.len(), - Value::Dict(v) => v.len(), - v => bail!( - span, - "expected string, array or dictionary, found {}", - v.type_name(), - ), - })) -} - /// The sorted version of an array. pub fn sorted(_: &mut Context, args: &mut Args) -> TypResult<Value> { let Spanned { v, span } = args.expect::<Spanned<Array>>("array")?; diff --git a/src/library/numbering.rs b/src/library/utility/numbering.rs index 05a39e36..0070873f 100644 --- a/src/library/numbering.rs +++ b/src/library/utility/numbering.rs @@ -1,33 +1,26 @@ -//! Conversion of numbers into letters, roman numerals and symbols. +use crate::library::prelude::*; -use super::prelude::*; +/// Converts an integer into one or multiple letters. +pub fn letter(_: &mut Context, args: &mut Args) -> TypResult<Value> { + convert(Numbering::Letter, args) +} -static ROMANS: &'static [(&'static str, usize)] = &[ - ("M̅", 1000000), - ("D̅", 500000), - ("C̅", 100000), - ("L̅", 50000), - ("X̅", 10000), - ("V̅", 5000), - ("I̅V̅", 4000), - ("M", 1000), - ("CM", 900), - ("D", 500), - ("CD", 400), - ("C", 100), - ("XC", 90), - ("L", 50), - ("XL", 40), - ("X", 10), - ("IX", 9), - ("V", 5), - ("IV", 4), - ("I", 1), -]; +/// Converts an integer into a roman numeral. +pub fn roman(_: &mut Context, args: &mut Args) -> TypResult<Value> { + convert(Numbering::Roman, args) +} -static SYMBOLS: &'static [char] = &['*', '†', '‡', '§', '‖', '¶']; +/// Convert a number into a symbol. +pub fn symbol(_: &mut Context, args: &mut Args) -> TypResult<Value> { + convert(Numbering::Symbol, args) +} + +fn convert(numbering: Numbering, args: &mut Args) -> TypResult<Value> { + let n = args.expect::<usize>("non-negative integer")?; + Ok(Value::Str(numbering.apply(n))) +} -/// The different kinds of numberings. +/// Allows to convert a number into letters, roman numerals and symbols. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Numbering { Arabic, @@ -92,22 +85,27 @@ impl Numbering { } } -/// Converts an integer into one or multiple letters. -pub fn letter(_: &mut Context, args: &mut Args) -> TypResult<Value> { - convert(Numbering::Letter, args) -} - -/// Converts an integer into a roman numeral. -pub fn roman(_: &mut Context, args: &mut Args) -> TypResult<Value> { - convert(Numbering::Roman, args) -} - -/// Convert a number into a symbol. -pub fn symbol(_: &mut Context, args: &mut Args) -> TypResult<Value> { - convert(Numbering::Symbol, args) -} +static ROMANS: &'static [(&'static str, usize)] = &[ + ("M̅", 1000000), + ("D̅", 500000), + ("C̅", 100000), + ("L̅", 50000), + ("X̅", 10000), + ("V̅", 5000), + ("I̅V̅", 4000), + ("M", 1000), + ("CM", 900), + ("D", 500), + ("CD", 400), + ("C", 100), + ("XC", 90), + ("L", 50), + ("XL", 40), + ("X", 10), + ("IX", 9), + ("V", 5), + ("IV", 4), + ("I", 1), +]; -fn convert(numbering: Numbering, args: &mut Args) -> TypResult<Value> { - let n = args.expect::<usize>("non-negative integer")?; - Ok(Value::Str(numbering.apply(n))) -} +static SYMBOLS: &'static [char] = &['*', '†', '‡', '§', '‖', '¶']; |
