Reorganize library

This commit is contained in:
Laurenz 2022-02-28 15:50:48 +01:00
parent b63c21c91d
commit 3ca5b23823
82 changed files with 1174 additions and 1180 deletions

View File

@ -33,7 +33,7 @@ use crate::Context;
/// ```
///
/// [construct]: Self::construct
/// [`TextNode`]: crate::library::TextNode
/// [`TextNode`]: crate::library::text::TextNode
/// [`set`]: Self::set
#[derive(Clone)]
pub struct Class {

View File

@ -9,7 +9,7 @@ use crate::diag::TypResult;
use crate::eval::StyleChain;
use crate::frame::{Element, Frame, Geometry, Shape, Stroke};
use crate::geom::{Align, Length, Linear, Paint, Point, Sides, Size, Spec, Transform};
use crate::library::{AlignNode, PadNode, TransformNode, MOVE};
use crate::library::layout::{AlignNode, MoveNode, PadNode};
use crate::util::Prehashed;
use crate::Context;
@ -203,7 +203,7 @@ impl LayoutNode {
/// Transform this node's contents without affecting layout.
pub fn moved(self, offset: Point) -> Self {
if !offset.is_zero() {
TransformNode::<MOVE> {
MoveNode {
transform: Transform::translation(offset.x, offset.y),
child: self,
}

View File

@ -120,7 +120,7 @@ impl Eval for StrongNode {
type Output = Template;
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
Ok(Template::show(library::StrongNode(
Ok(Template::show(library::text::StrongNode(
self.body().eval(ctx, scp)?,
)))
}
@ -130,7 +130,7 @@ impl Eval for EmphNode {
type Output = Template;
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
Ok(Template::show(library::EmphNode(
Ok(Template::show(library::text::EmphNode(
self.body().eval(ctx, scp)?,
)))
}
@ -140,12 +140,12 @@ impl Eval for RawNode {
type Output = Template;
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
let template = Template::show(library::RawNode {
let template = Template::show(library::text::RawNode {
text: self.text.clone(),
block: self.block,
});
Ok(match self.lang {
Some(_) => template.styled(library::RawNode::LANG, self.lang.clone()),
Some(_) => template.styled(library::text::RawNode::LANG, self.lang.clone()),
None => template,
})
}
@ -155,7 +155,7 @@ impl Eval for MathNode {
type Output = Template;
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
Ok(Template::show(library::MathNode {
Ok(Template::show(library::elements::MathNode {
formula: self.formula.clone(),
display: self.display,
}))
@ -166,7 +166,7 @@ impl Eval for HeadingNode {
type Output = Template;
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
Ok(Template::show(library::HeadingNode {
Ok(Template::show(library::elements::HeadingNode {
body: self.body().eval(ctx, scp)?,
level: self.level(),
}))
@ -177,7 +177,7 @@ impl Eval for ListNode {
type Output = Template;
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
Ok(Template::List(library::ListItem {
Ok(Template::List(library::elements::ListItem {
number: None,
body: Box::new(self.body().eval(ctx, scp)?),
}))
@ -188,7 +188,7 @@ impl Eval for EnumNode {
type Output = Template;
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
Ok(Template::Enum(library::ListItem {
Ok(Template::Enum(library::elements::ListItem {
number: self.number(),
body: Box::new(self.body().eval(ctx, scp)?),
}))

View File

@ -5,7 +5,8 @@ use std::sync::Arc;
use super::{Args, Func, Span, Template, Value};
use crate::diag::{At, TypResult};
use crate::library::{PageNode, ParNode};
use crate::library::layout::PageNode;
use crate::library::text::ParNode;
use crate::Context;
/// A map of style properties.

View File

@ -10,11 +10,10 @@ use super::{
StyleMap, StyleVecBuilder,
};
use crate::diag::StrResult;
use crate::library::elements::{ListItem, ListKind, ListNode, ORDERED, UNORDERED};
use crate::library::layout::{FlowChild, FlowNode, PageNode, PlaceNode, SpacingKind};
use crate::library::prelude::*;
use crate::library::{
DecoNode, FlowChild, FlowNode, ListItem, ListKind, ListNode, PageNode, ParChild,
ParNode, PlaceNode, SpacingKind, TextNode, ORDERED, UNDERLINE, UNORDERED,
};
use crate::library::text::{DecoNode, ParChild, ParNode, TextNode, UNDERLINE};
use crate::util::EcoString;
/// Composable representation of styled content.

View File

@ -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;

View File

@ -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)]

View File

@ -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)]

View File

@ -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.

View File

@ -1,6 +1,4 @@
//! Mathematical formulas.
use super::prelude::*;
use crate::library::prelude::*;
/// A mathematical formula.
#[derive(Debug, Hash)]

View File

@ -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::*;

View File

@ -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 {

View File

@ -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)]

View File

@ -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)]

View File

@ -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)]

View File

@ -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]

View File

@ -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.
///

View File

@ -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)]

View File

@ -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)]

27
src/library/layout/mod.rs Normal file
View File

@ -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::*;

View File

@ -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)]

View File

@ -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)]

View File

@ -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)]

View File

@ -1,6 +1,4 @@
//! Horizontal and vertical spacing between nodes.
use super::prelude::*;
use crate::library::prelude::*;
/// Horizontal spacing.
pub struct HNode;

View File

@ -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)]

View File

@ -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;

View File

@ -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
}

20
src/library/prelude.rs Normal file
View File

@ -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;

250
src/library/text/deco.rs Normal file
View File

@ -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();
}
}

View File

@ -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.

409
src/library/text/mod.rs Normal file
View File

@ -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(),
}
}
}

View File

@ -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
}

View File

@ -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};

View File

@ -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();
}
}

114
src/library/utility/math.rs Normal file
View File

@ -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)))
}

View File

@ -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())
/// 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(),
),
(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))
}
/// 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")?;

View File

@ -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)] = &[
("", 1000000),
("", 500000),
("", 100000),
("", 50000),
("", 10000),
("", 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)
}
/// The different kinds of numberings.
fn convert(numbering: Numbering, args: &mut Args) -> TypResult<Value> {
let n = args.expect::<usize>("non-negative integer")?;
Ok(Value::Str(numbering.apply(n)))
}
/// 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)
}
static ROMANS: &'static [(&'static str, usize)] = &[
("", 1000000),
("", 500000),
("", 100000),
("", 50000),
("", 10000),
("", 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)
}
/// 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)))
}
static SYMBOLS: &'static [char] = &['*', '†', '‡', '§', '‖', '¶'];

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 179 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 B

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,4 +0,0 @@
// Test line breaks.
---
A \ B \ C

View File

@ -1,4 +1,4 @@
// Test line breaking special cases.
// Test line breaks.
---
// Test overlong word that is not directly after a hard break.

View File

@ -1,12 +1,6 @@
// Test collection functions.
// Ref: false
---
#let memes = "ArE mEmEs gReAt?";
#test(lower(memes), "are memes great?")
#test(upper(memes), "ARE MEMES GREAT?")
#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ")
---
// Test the `len` function.
#test(len(()), 0)

View File

@ -1,7 +1,17 @@
// Test string functions.
// Test string handling functions.
// Ref: false
---
// Test the `upper`, `lower`, and number formatting functions.
#let memes = "ArE mEmEs gReAt?";
#test(lower(memes), "are memes great?")
#test(upper(memes), "ARE MEMES GREAT?")
#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ")
---
// Test numbering formatting functions.
// Ref: true
#upper("Abc 8")
#upper[def]

View File

@ -12,7 +12,8 @@ use typst::diag::Error;
use typst::eval::{Smart, StyleMap, Value};
use typst::frame::{Element, Frame};
use typst::geom::{Length, RgbaColor};
use typst::library::{PageNode, TextNode};
use typst::library::layout::PageNode;
use typst::library::text::TextNode;
use typst::loading::FsLoader;
use typst::parse::Scanner;
use typst::source::SourceFile;