Reorganize library
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)?),
|
||||
}))
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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;
|
@ -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)]
|
@ -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)]
|
@ -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.
|
@ -1,6 +1,4 @@
|
||||
//! Mathematical formulas.
|
||||
|
||||
use super::prelude::*;
|
||||
use crate::library::prelude::*;
|
||||
|
||||
/// A mathematical formula.
|
||||
#[derive(Debug, Hash)]
|
15
src/library/elements/mod.rs
Normal 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::*;
|
@ -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 {
|
@ -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)]
|
@ -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)]
|
@ -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)]
|
@ -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]
|
@ -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.
|
||||
///
|
@ -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)]
|
@ -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
@ -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::*;
|
@ -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)]
|
@ -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)]
|
@ -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)]
|
@ -1,6 +1,4 @@
|
||||
//! Horizontal and vertical spacing between nodes.
|
||||
|
||||
use super::prelude::*;
|
||||
use crate::library::prelude::*;
|
||||
|
||||
/// Horizontal spacing.
|
||||
pub struct HNode;
|
@ -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)]
|
@ -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;
|
@ -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
@ -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
@ -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();
|
||||
}
|
||||
}
|
@ -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
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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};
|
||||
|
@ -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
@ -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)))
|
||||
}
|
@ -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")?;
|
@ -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)
|
||||
}
|
||||
|
||||
/// 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)] = &[
|
||||
("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)
|
||||
}
|
||||
|
||||
/// 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] = &['*', '†', '‡', '§', '‖', '¶'];
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 179 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
@ -1,4 +0,0 @@
|
||||
// Test line breaks.
|
||||
|
||||
---
|
||||
A \ B \ C
|
@ -1,4 +1,4 @@
|
||||
// Test line breaking special cases.
|
||||
// Test line breaks.
|
||||
|
||||
---
|
||||
// Test overlong word that is not directly after a hard break.
|
@ -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)
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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;
|
||||
|