Reorganize library
@ -33,7 +33,7 @@ use crate::Context;
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// [construct]: Self::construct
|
/// [construct]: Self::construct
|
||||||
/// [`TextNode`]: crate::library::TextNode
|
/// [`TextNode`]: crate::library::text::TextNode
|
||||||
/// [`set`]: Self::set
|
/// [`set`]: Self::set
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Class {
|
pub struct Class {
|
||||||
|
@ -9,7 +9,7 @@ use crate::diag::TypResult;
|
|||||||
use crate::eval::StyleChain;
|
use crate::eval::StyleChain;
|
||||||
use crate::frame::{Element, Frame, Geometry, Shape, Stroke};
|
use crate::frame::{Element, Frame, Geometry, Shape, Stroke};
|
||||||
use crate::geom::{Align, Length, Linear, Paint, Point, Sides, Size, Spec, Transform};
|
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::util::Prehashed;
|
||||||
use crate::Context;
|
use crate::Context;
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ impl LayoutNode {
|
|||||||
/// Transform this node's contents without affecting layout.
|
/// Transform this node's contents without affecting layout.
|
||||||
pub fn moved(self, offset: Point) -> Self {
|
pub fn moved(self, offset: Point) -> Self {
|
||||||
if !offset.is_zero() {
|
if !offset.is_zero() {
|
||||||
TransformNode::<MOVE> {
|
MoveNode {
|
||||||
transform: Transform::translation(offset.x, offset.y),
|
transform: Transform::translation(offset.x, offset.y),
|
||||||
child: self,
|
child: self,
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ impl Eval for StrongNode {
|
|||||||
type Output = Template;
|
type Output = Template;
|
||||||
|
|
||||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
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)?,
|
self.body().eval(ctx, scp)?,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
@ -130,7 +130,7 @@ impl Eval for EmphNode {
|
|||||||
type Output = Template;
|
type Output = Template;
|
||||||
|
|
||||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
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)?,
|
self.body().eval(ctx, scp)?,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
@ -140,12 +140,12 @@ impl Eval for RawNode {
|
|||||||
type Output = Template;
|
type Output = Template;
|
||||||
|
|
||||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
|
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(),
|
text: self.text.clone(),
|
||||||
block: self.block,
|
block: self.block,
|
||||||
});
|
});
|
||||||
Ok(match self.lang {
|
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,
|
None => template,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -155,7 +155,7 @@ impl Eval for MathNode {
|
|||||||
type Output = Template;
|
type Output = Template;
|
||||||
|
|
||||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
|
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(),
|
formula: self.formula.clone(),
|
||||||
display: self.display,
|
display: self.display,
|
||||||
}))
|
}))
|
||||||
@ -166,7 +166,7 @@ impl Eval for HeadingNode {
|
|||||||
type Output = Template;
|
type Output = Template;
|
||||||
|
|
||||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
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)?,
|
body: self.body().eval(ctx, scp)?,
|
||||||
level: self.level(),
|
level: self.level(),
|
||||||
}))
|
}))
|
||||||
@ -177,7 +177,7 @@ impl Eval for ListNode {
|
|||||||
type Output = Template;
|
type Output = Template;
|
||||||
|
|
||||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
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,
|
number: None,
|
||||||
body: Box::new(self.body().eval(ctx, scp)?),
|
body: Box::new(self.body().eval(ctx, scp)?),
|
||||||
}))
|
}))
|
||||||
@ -188,7 +188,7 @@ impl Eval for EnumNode {
|
|||||||
type Output = Template;
|
type Output = Template;
|
||||||
|
|
||||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
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(),
|
number: self.number(),
|
||||||
body: Box::new(self.body().eval(ctx, scp)?),
|
body: Box::new(self.body().eval(ctx, scp)?),
|
||||||
}))
|
}))
|
||||||
|
@ -5,7 +5,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use super::{Args, Func, Span, Template, Value};
|
use super::{Args, Func, Span, Template, Value};
|
||||||
use crate::diag::{At, TypResult};
|
use crate::diag::{At, TypResult};
|
||||||
use crate::library::{PageNode, ParNode};
|
use crate::library::layout::PageNode;
|
||||||
|
use crate::library::text::ParNode;
|
||||||
use crate::Context;
|
use crate::Context;
|
||||||
|
|
||||||
/// A map of style properties.
|
/// A map of style properties.
|
||||||
|
@ -10,11 +10,10 @@ use super::{
|
|||||||
StyleMap, StyleVecBuilder,
|
StyleMap, StyleVecBuilder,
|
||||||
};
|
};
|
||||||
use crate::diag::StrResult;
|
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::prelude::*;
|
||||||
use crate::library::{
|
use crate::library::text::{DecoNode, ParChild, ParNode, TextNode, UNDERLINE};
|
||||||
DecoNode, FlowChild, FlowNode, ListItem, ListKind, ListNode, PageNode, ParChild,
|
|
||||||
ParNode, PlaceNode, SpacingKind, TextNode, ORDERED, UNDERLINE, UNORDERED,
|
|
||||||
};
|
|
||||||
use crate::util::EcoString;
|
use crate::util::EcoString;
|
||||||
|
|
||||||
/// Composable representation of styled content.
|
/// 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 crate::library::prelude::*;
|
||||||
|
use crate::library::text::{FontFamily, TextNode};
|
||||||
use super::prelude::*;
|
|
||||||
use super::{FontFamily, TextNode};
|
|
||||||
|
|
||||||
/// A section heading.
|
/// A section heading.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,9 +1,7 @@
|
|||||||
//! Raster and vector graphics.
|
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
use super::TextNode;
|
|
||||||
use crate::diag::Error;
|
use crate::diag::Error;
|
||||||
use crate::image::ImageId;
|
use crate::image::ImageId;
|
||||||
|
use crate::library::prelude::*;
|
||||||
|
use crate::library::text::TextNode;
|
||||||
|
|
||||||
/// Show a raster or vector graphic.
|
/// Show a raster or vector graphic.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,13 +1,12 @@
|
|||||||
//! Unordered (bulleted) and ordered (numbered) lists.
|
use crate::library::layout::{GridNode, TrackSizing};
|
||||||
|
use crate::library::prelude::*;
|
||||||
use super::prelude::*;
|
use crate::library::text::{ParNode, TextNode};
|
||||||
use super::{GridNode, Numbering, ParNode, TextNode, TrackSizing};
|
use crate::library::utility::Numbering;
|
||||||
|
|
||||||
use crate::parse::Scanner;
|
use crate::parse::Scanner;
|
||||||
|
|
||||||
/// An unordered or ordered list.
|
/// An unordered (bulleted) or ordered (numbered) list.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
pub struct ListNode<const L: ListKind> {
|
pub struct ListNode<const L: ListKind = UNORDERED> {
|
||||||
/// Where the list starts.
|
/// Where the list starts.
|
||||||
pub start: usize,
|
pub start: usize,
|
||||||
/// If true, there is paragraph spacing between the items, if false
|
/// If true, there is paragraph spacing between the items, if false
|
||||||
@ -26,6 +25,9 @@ pub struct ListItem {
|
|||||||
pub body: Box<Template>,
|
pub body: Box<Template>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An ordered list.
|
||||||
|
pub type EnumNode = ListNode<ORDERED>;
|
||||||
|
|
||||||
#[class]
|
#[class]
|
||||||
impl<const L: ListKind> ListNode<L> {
|
impl<const L: ListKind> ListNode<L> {
|
||||||
/// How the list is labelled.
|
/// How the list is labelled.
|
@ -1,6 +1,4 @@
|
|||||||
//! Mathematical formulas.
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
|
|
||||||
/// A mathematical formula.
|
/// A mathematical formula.
|
||||||
#[derive(Debug, Hash)]
|
#[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 std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
use super::prelude::*;
|
use crate::library::prelude::*;
|
||||||
use super::TextNode;
|
use crate::library::text::TextNode;
|
||||||
|
|
||||||
/// Place a node into a sizable and fillable shape.
|
/// Place a node into a sizable and fillable shape.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
pub struct ShapeNode<const S: ShapeKind>(pub Option<LayoutNode>);
|
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]
|
#[class]
|
||||||
impl<const S: ShapeKind> ShapeNode<S> {
|
impl<const S: ShapeKind> ShapeNode<S> {
|
||||||
/// How to fill the shape.
|
/// How to fill the shape.
|
||||||
@ -134,16 +144,16 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
|
|||||||
pub type ShapeKind = usize;
|
pub type ShapeKind = usize;
|
||||||
|
|
||||||
/// A rectangle with equal side lengths.
|
/// A rectangle with equal side lengths.
|
||||||
pub const SQUARE: ShapeKind = 0;
|
const SQUARE: ShapeKind = 0;
|
||||||
|
|
||||||
/// A quadrilateral with four right angles.
|
/// A quadrilateral with four right angles.
|
||||||
pub const RECT: ShapeKind = 1;
|
const RECT: ShapeKind = 1;
|
||||||
|
|
||||||
/// An ellipse with coinciding foci.
|
/// An ellipse with coinciding foci.
|
||||||
pub const CIRCLE: ShapeKind = 2;
|
const CIRCLE: ShapeKind = 2;
|
||||||
|
|
||||||
/// A curve around two focal points.
|
/// A curve around two focal points.
|
||||||
pub const ELLIPSE: ShapeKind = 3;
|
const ELLIPSE: ShapeKind = 3;
|
||||||
|
|
||||||
/// Whether a shape kind is curvy.
|
/// Whether a shape kind is curvy.
|
||||||
fn is_round(kind: ShapeKind) -> bool {
|
fn is_round(kind: ShapeKind) -> bool {
|
@ -1,7 +1,5 @@
|
|||||||
//! Tabular container.
|
use crate::library::layout::{GridNode, TrackSizing};
|
||||||
|
use crate::library::prelude::*;
|
||||||
use super::prelude::*;
|
|
||||||
use super::{GridNode, TrackSizing};
|
|
||||||
|
|
||||||
/// A table of items.
|
/// A table of items.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,7 +1,5 @@
|
|||||||
//! Aligning nodes in their parent container.
|
use crate::library::prelude::*;
|
||||||
|
use crate::library::text::ParNode;
|
||||||
use super::prelude::*;
|
|
||||||
use super::ParNode;
|
|
||||||
|
|
||||||
/// Align a node along the layouting axes.
|
/// Align a node along the layouting axes.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,7 +1,5 @@
|
|||||||
//! Multi-column layouts.
|
use crate::library::prelude::*;
|
||||||
|
use crate::library::text::ParNode;
|
||||||
use super::prelude::*;
|
|
||||||
use super::ParNode;
|
|
||||||
|
|
||||||
/// Separate a region into multiple equally sized columns.
|
/// Separate a region into multiple equally sized columns.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,8 +1,6 @@
|
|||||||
//! Inline- and block-level containers.
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
use super::prelude::*;
|
/// An inline-level container that sizes content and places it into a paragraph.
|
||||||
|
|
||||||
/// Size content and place it into a paragraph.
|
|
||||||
pub struct BoxNode;
|
pub struct BoxNode;
|
||||||
|
|
||||||
#[class]
|
#[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;
|
pub struct BlockNode;
|
||||||
|
|
||||||
#[class]
|
#[class]
|
@ -1,7 +1,6 @@
|
|||||||
//! A flow of paragraphs and other block-level nodes.
|
use super::{AlignNode, PlaceNode, SpacingKind};
|
||||||
|
use crate::library::prelude::*;
|
||||||
use super::prelude::*;
|
use crate::library::text::{ParNode, TextNode};
|
||||||
use super::{AlignNode, ParNode, PlaceNode, SpacingKind, TextNode};
|
|
||||||
|
|
||||||
/// Arrange spacing, paragraphs and other block-level nodes into a flow.
|
/// Arrange spacing, paragraphs and other block-level nodes into a flow.
|
||||||
///
|
///
|
@ -1,6 +1,4 @@
|
|||||||
//! Layout along a row and column raster.
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
|
|
||||||
/// Arrange nodes in a grid.
|
/// Arrange nodes in a grid.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,6 +1,4 @@
|
|||||||
//! Hiding of nodes without affecting layout.
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
|
|
||||||
/// Hide a node without affecting layout.
|
/// Hide a node without affecting layout.
|
||||||
#[derive(Debug, Hash)]
|
#[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 crate::library::prelude::*;
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
|
|
||||||
/// Pad a node at the sides.
|
/// Pad a node at the sides.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,10 +1,8 @@
|
|||||||
//! Pages of paper.
|
|
||||||
|
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
use super::ColumnsNode;
|
use super::ColumnsNode;
|
||||||
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
/// Layouts its child onto one or multiple pages.
|
/// Layouts its child onto one or multiple pages.
|
||||||
#[derive(Clone, PartialEq, Hash)]
|
#[derive(Clone, PartialEq, Hash)]
|
@ -1,7 +1,5 @@
|
|||||||
//! Absolute placement of nodes.
|
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
use super::AlignNode;
|
use super::AlignNode;
|
||||||
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
/// Place a node at an absolute position.
|
/// Place a node at an absolute position.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,6 +1,4 @@
|
|||||||
//! Horizontal and vertical spacing between nodes.
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
|
|
||||||
/// Horizontal spacing.
|
/// Horizontal spacing.
|
||||||
pub struct HNode;
|
pub struct HNode;
|
@ -1,7 +1,5 @@
|
|||||||
//! Side-by-side layout of nodes along an axis.
|
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
use super::{AlignNode, SpacingKind};
|
use super::{AlignNode, SpacingKind};
|
||||||
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
/// Arrange nodes and spacing along an axis.
|
/// Arrange nodes and spacing along an axis.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
@ -1,7 +1,5 @@
|
|||||||
//! Affine transformations on nodes.
|
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
use crate::geom::Transform;
|
use crate::geom::Transform;
|
||||||
|
use crate::library::prelude::*;
|
||||||
|
|
||||||
/// Transform a node without affecting layout.
|
/// Transform a node without affecting layout.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
@ -12,6 +10,15 @@ pub struct TransformNode<const T: TransformKind> {
|
|||||||
pub child: LayoutNode,
|
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]
|
#[class]
|
||||||
impl<const T: TransformKind> TransformNode<T> {
|
impl<const T: TransformKind> TransformNode<T> {
|
||||||
/// The origin of the transformation.
|
/// The origin of the transformation.
|
||||||
@ -70,10 +77,10 @@ impl<const T: TransformKind> Layout for TransformNode<T> {
|
|||||||
pub type TransformKind = usize;
|
pub type TransformKind = usize;
|
||||||
|
|
||||||
/// A translation on the X and Y axes.
|
/// A translation on the X and Y axes.
|
||||||
pub const MOVE: TransformKind = 0;
|
const MOVE: TransformKind = 0;
|
||||||
|
|
||||||
/// A rotational transformation.
|
/// A rotational transformation.
|
||||||
pub const ROTATE: TransformKind = 1;
|
const ROTATE: TransformKind = 1;
|
||||||
|
|
||||||
/// A scale transformation.
|
/// 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
|
//! Call [`new`] to obtain a [`Scope`] containing all standard library
|
||||||
//! definitions.
|
//! definitions.
|
||||||
|
|
||||||
pub mod align;
|
pub mod elements;
|
||||||
pub mod columns;
|
pub mod layout;
|
||||||
pub mod container;
|
pub mod prelude;
|
||||||
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 text;
|
pub mod text;
|
||||||
pub mod transform;
|
|
||||||
|
|
||||||
pub mod utility;
|
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::*;
|
use prelude::*;
|
||||||
|
|
||||||
@ -83,72 +15,74 @@ use prelude::*;
|
|||||||
pub fn new() -> Scope {
|
pub fn new() -> Scope {
|
||||||
let mut std = Scope::new();
|
let mut std = Scope::new();
|
||||||
|
|
||||||
// Structure and semantics.
|
// Text.
|
||||||
std.def_class::<PageNode>("page");
|
std.def_class::<text::TextNode>("text");
|
||||||
std.def_class::<PagebreakNode>("pagebreak");
|
std.def_class::<text::ParNode>("par");
|
||||||
std.def_class::<ParNode>("par");
|
std.def_class::<text::ParbreakNode>("parbreak");
|
||||||
std.def_class::<ParbreakNode>("parbreak");
|
std.def_class::<text::LinebreakNode>("linebreak");
|
||||||
std.def_class::<LinebreakNode>("linebreak");
|
std.def_class::<text::StrongNode>("strong");
|
||||||
std.def_class::<TextNode>("text");
|
std.def_class::<text::EmphNode>("emph");
|
||||||
std.def_class::<StrongNode>("strong");
|
std.def_class::<text::RawNode>("raw");
|
||||||
std.def_class::<EmphNode>("emph");
|
std.def_class::<text::UnderlineNode>("underline");
|
||||||
std.def_class::<RawNode>("raw");
|
std.def_class::<text::StrikethroughNode>("strike");
|
||||||
std.def_class::<MathNode>("math");
|
std.def_class::<text::OverlineNode>("overline");
|
||||||
std.def_class::<DecoNode<UNDERLINE>>("underline");
|
std.def_class::<text::LinkNode>("link");
|
||||||
std.def_class::<DecoNode<STRIKETHROUGH>>("strike");
|
|
||||||
std.def_class::<DecoNode<OVERLINE>>("overline");
|
// Elements.
|
||||||
std.def_class::<LinkNode>("link");
|
std.def_class::<elements::MathNode>("math");
|
||||||
std.def_class::<HeadingNode>("heading");
|
std.def_class::<elements::HeadingNode>("heading");
|
||||||
std.def_class::<ListNode<UNORDERED>>("list");
|
std.def_class::<elements::ListNode>("list");
|
||||||
std.def_class::<ListNode<ORDERED>>("enum");
|
std.def_class::<elements::EnumNode>("enum");
|
||||||
std.def_class::<TableNode>("table");
|
std.def_class::<elements::TableNode>("table");
|
||||||
std.def_class::<ImageNode>("image");
|
std.def_class::<elements::ImageNode>("image");
|
||||||
std.def_class::<ShapeNode<RECT>>("rect");
|
std.def_class::<elements::RectNode>("rect");
|
||||||
std.def_class::<ShapeNode<SQUARE>>("square");
|
std.def_class::<elements::SquareNode>("square");
|
||||||
std.def_class::<ShapeNode<ELLIPSE>>("ellipse");
|
std.def_class::<elements::EllipseNode>("ellipse");
|
||||||
std.def_class::<ShapeNode<CIRCLE>>("circle");
|
std.def_class::<elements::CircleNode>("circle");
|
||||||
|
|
||||||
// Layout.
|
// Layout.
|
||||||
std.def_class::<HNode>("h");
|
std.def_class::<layout::PageNode>("page");
|
||||||
std.def_class::<VNode>("v");
|
std.def_class::<layout::PagebreakNode>("pagebreak");
|
||||||
std.def_class::<BoxNode>("box");
|
std.def_class::<layout::HNode>("h");
|
||||||
std.def_class::<BlockNode>("block");
|
std.def_class::<layout::VNode>("v");
|
||||||
std.def_class::<AlignNode>("align");
|
std.def_class::<layout::BoxNode>("box");
|
||||||
std.def_class::<PadNode>("pad");
|
std.def_class::<layout::BlockNode>("block");
|
||||||
std.def_class::<PlaceNode>("place");
|
std.def_class::<layout::AlignNode>("align");
|
||||||
std.def_class::<TransformNode<MOVE>>("move");
|
std.def_class::<layout::PadNode>("pad");
|
||||||
std.def_class::<TransformNode<SCALE>>("scale");
|
std.def_class::<layout::StackNode>("stack");
|
||||||
std.def_class::<TransformNode<ROTATE>>("rotate");
|
std.def_class::<layout::GridNode>("grid");
|
||||||
std.def_class::<HideNode>("hide");
|
std.def_class::<layout::ColumnsNode>("columns");
|
||||||
std.def_class::<StackNode>("stack");
|
std.def_class::<layout::ColbreakNode>("colbreak");
|
||||||
std.def_class::<GridNode>("grid");
|
std.def_class::<layout::PlaceNode>("place");
|
||||||
std.def_class::<ColumnsNode>("columns");
|
std.def_class::<layout::MoveNode>("move");
|
||||||
std.def_class::<ColbreakNode>("colbreak");
|
std.def_class::<layout::ScaleNode>("scale");
|
||||||
|
std.def_class::<layout::RotateNode>("rotate");
|
||||||
|
std.def_class::<layout::HideNode>("hide");
|
||||||
|
|
||||||
// Utility functions.
|
// Utility functions.
|
||||||
std.def_func("assert", assert);
|
std.def_func("assert", utility::assert);
|
||||||
std.def_func("type", type_);
|
std.def_func("type", utility::type_);
|
||||||
std.def_func("repr", repr);
|
std.def_func("repr", utility::repr);
|
||||||
std.def_func("join", join);
|
std.def_func("join", utility::join);
|
||||||
std.def_func("int", int);
|
std.def_func("int", utility::int);
|
||||||
std.def_func("float", float);
|
std.def_func("float", utility::float);
|
||||||
std.def_func("str", str);
|
std.def_func("str", utility::str);
|
||||||
std.def_func("abs", abs);
|
std.def_func("abs", utility::abs);
|
||||||
std.def_func("min", min);
|
std.def_func("min", utility::min);
|
||||||
std.def_func("max", max);
|
std.def_func("max", utility::max);
|
||||||
std.def_func("even", even);
|
std.def_func("even", utility::even);
|
||||||
std.def_func("odd", odd);
|
std.def_func("odd", utility::odd);
|
||||||
std.def_func("mod", modulo);
|
std.def_func("mod", utility::modulo);
|
||||||
std.def_func("range", range);
|
std.def_func("range", utility::range);
|
||||||
std.def_func("rgb", rgb);
|
std.def_func("rgb", utility::rgb);
|
||||||
std.def_func("cmyk", cmyk);
|
std.def_func("cmyk", utility::cmyk);
|
||||||
std.def_func("lower", lower);
|
std.def_func("lower", utility::lower);
|
||||||
std.def_func("upper", upper);
|
std.def_func("upper", utility::upper);
|
||||||
std.def_func("letter", letter);
|
std.def_func("letter", utility::letter);
|
||||||
std.def_func("roman", roman);
|
std.def_func("roman", utility::roman);
|
||||||
std.def_func("symbol", symbol);
|
std.def_func("symbol", utility::symbol);
|
||||||
std.def_func("len", len);
|
std.def_func("len", utility::len);
|
||||||
std.def_func("sorted", sorted);
|
std.def_func("sorted", utility::sorted);
|
||||||
|
|
||||||
// Predefined colors.
|
// Predefined colors.
|
||||||
std.def_const("black", Color::BLACK);
|
std.def_const("black", Color::BLACK);
|
||||||
@ -181,9 +115,9 @@ pub fn new() -> Scope {
|
|||||||
std.def_const("top", Align::Top);
|
std.def_const("top", Align::Top);
|
||||||
std.def_const("horizon", Align::Horizon);
|
std.def_const("horizon", Align::Horizon);
|
||||||
std.def_const("bottom", Align::Bottom);
|
std.def_const("bottom", Align::Bottom);
|
||||||
std.def_const("serif", FontFamily::Serif);
|
std.def_const("serif", text::FontFamily::Serif);
|
||||||
std.def_const("sans-serif", FontFamily::SansSerif);
|
std.def_const("sans-serif", text::FontFamily::SansSerif);
|
||||||
std.def_const("monospace", FontFamily::Monospace);
|
std.def_const("monospace", text::FontFamily::Monospace);
|
||||||
|
|
||||||
std
|
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 super::TextNode;
|
||||||
|
use crate::library::prelude::*;
|
||||||
use crate::util::EcoString;
|
use crate::util::EcoString;
|
||||||
|
|
||||||
/// Link text and other elements to an URL.
|
/// 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 std::sync::Arc;
|
||||||
|
|
||||||
use either::Either;
|
use either::Either;
|
||||||
use unicode_bidi::{BidiInfo, Level};
|
use unicode_bidi::{BidiInfo, Level};
|
||||||
use xi_unicode::LineBreakIterator;
|
use xi_unicode::LineBreakIterator;
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::{shape, ShapedText, TextNode};
|
||||||
use super::{shape, ShapedText, SpacingKind, TextNode};
|
|
||||||
use crate::font::FontStore;
|
use crate::font::FontStore;
|
||||||
|
use crate::library::layout::SpacingKind;
|
||||||
|
use crate::library::prelude::*;
|
||||||
use crate::util::{ArcExt, EcoString, RangeExt, SliceExt};
|
use crate::util::{ArcExt, EcoString, RangeExt, SliceExt};
|
||||||
|
|
||||||
/// Arrange text, spacing and inline-level nodes into a paragraph.
|
/// Arrange text, spacing and inline-level nodes into a paragraph.
|
||||||
@ -127,118 +126,24 @@ impl Layout for ParNode {
|
|||||||
) -> TypResult<Vec<Arc<Frame>>> {
|
) -> TypResult<Vec<Arc<Frame>>> {
|
||||||
// Collect all text into one string and perform BiDi analysis.
|
// Collect all text into one string and perform BiDi analysis.
|
||||||
let text = self.collect_text();
|
let text = self.collect_text();
|
||||||
let level = Level::from_dir(styles.get(Self::DIR));
|
let bidi = BidiInfo::new(&text, match styles.get(Self::DIR) {
|
||||||
let bidi = BidiInfo::new(&text, level);
|
Dir::LTR => Some(Level::ltr()),
|
||||||
|
Dir::RTL => Some(Level::rtl()),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
// Prepare paragraph layout by building a representation on which we can
|
// Prepare paragraph layout by building a representation on which we can
|
||||||
// do line breaking without layouting each and every line from scratch.
|
// do line breaking without layouting each and every line from scratch.
|
||||||
let par = ParLayout::new(ctx, self, bidi, regions, &styles)?;
|
let par = ParLayout::new(ctx, self, bidi, regions, &styles)?;
|
||||||
|
|
||||||
// Break the paragraph into lines.
|
// 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.
|
// Stack the lines into one frame per region.
|
||||||
Ok(stack_lines(&ctx.fonts, lines, regions, styles))
|
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 {
|
impl Debug for ParNode {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str("Par ")?;
|
f.write_str("Par ")?;
|
||||||
@ -337,7 +242,8 @@ impl<'a> ParLayout<'a> {
|
|||||||
cursor += count;
|
cursor += count;
|
||||||
let subrange = start .. cursor;
|
let subrange = start .. cursor;
|
||||||
let text = &bidi.text[subrange.clone()];
|
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));
|
items.push(ParItem::Text(shaped));
|
||||||
ranges.push(subrange);
|
ranges.push(subrange);
|
||||||
}
|
}
|
||||||
@ -613,22 +519,99 @@ impl<'a> LineLayout<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Additional methods for BiDi levels.
|
/// Perform line breaking.
|
||||||
trait LevelExt: Sized {
|
fn break_into_lines<'a>(
|
||||||
fn from_dir(dir: Dir) -> Option<Self>;
|
fonts: &mut FontStore,
|
||||||
fn dir(self) -> Dir;
|
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 {
|
// Find suitable line breaks.
|
||||||
fn from_dir(dir: Dir) -> Option<Self> {
|
for (end, mandatory) in LineBreakIterator::new(&par.bidi.text) {
|
||||||
match dir {
|
// Compute the line and its size.
|
||||||
Dir::LTR => Some(Level::ltr()),
|
let mut line = par.line(fonts, start .. end, mandatory);
|
||||||
Dir::RTL => Some(Level::rtl()),
|
|
||||||
_ => None,
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dir(self) -> Dir {
|
// Finish the current line if there is a mandatory line break (i.e.
|
||||||
if self.is_ltr() { Dir::LTR } else { Dir::RTL }
|
// 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
|
||||||
|
}
|
@ -1,12 +1,10 @@
|
|||||||
//! Monospaced text and code.
|
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use syntect::easy::HighlightLines;
|
use syntect::easy::HighlightLines;
|
||||||
use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet};
|
use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet};
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
use super::prelude::*;
|
use crate::library::prelude::*;
|
||||||
use crate::library::TextNode;
|
use crate::library::text::TextNode;
|
||||||
use crate::source::SourceId;
|
use crate::source::SourceId;
|
||||||
use crate::syntax::{self, RedNode};
|
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 rustybuzz::{Feature, UnicodeBuffer};
|
||||||
use ttf_parser::{GlyphId, OutlineBuilder, Tag};
|
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::*;
|
||||||
use super::Decoration;
|
use crate::font::{FaceId, FontStore, FontVariant};
|
||||||
use crate::font::{
|
use crate::library::prelude::*;
|
||||||
Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight,
|
use crate::util::SliceExt;
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of shaping text.
|
/// The result of shaping text.
|
||||||
///
|
///
|
||||||
@ -448,9 +49,11 @@ pub struct ShapedGlyph {
|
|||||||
pub is_space: bool,
|
pub is_space: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A visual side.
|
/// A side you can go toward.
|
||||||
enum Side {
|
enum Side {
|
||||||
|
/// Go toward the west.
|
||||||
Left,
|
Left,
|
||||||
|
/// Go toward the east.
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -947,168 +550,3 @@ fn measure(
|
|||||||
|
|
||||||
(Size::new(width, top + bottom), top)
|
(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.
|
//! Computational utility functions.
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
mod math;
|
||||||
|
mod numbering;
|
||||||
|
|
||||||
|
pub use math::*;
|
||||||
|
pub use numbering::*;
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::prelude::*;
|
use crate::library::prelude::*;
|
||||||
use super::{Case, TextNode};
|
use crate::library::text::{Case, TextNode};
|
||||||
use crate::eval::Array;
|
|
||||||
|
|
||||||
/// Ensure that a condition is fulfilled.
|
/// Ensure that a condition is fulfilled.
|
||||||
pub fn assert(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
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> {
|
pub fn str(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
||||||
let Spanned { v, span } = args.expect("value")?;
|
let Spanned { v, span } = args.expect("value")?;
|
||||||
Ok(Value::Str(match v {
|
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()))
|
Ok(Value::Color(CmykColor::new(c, m, y, k).into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The absolute value of a numeric value.
|
/// The length of a string, an array or a dictionary.
|
||||||
pub fn abs(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
pub fn len(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
||||||
let Spanned { v, span } = args.expect("numeric value")?;
|
let Spanned { v, span } = args.expect("collection")?;
|
||||||
Ok(match v {
|
Ok(Value::Int(match v {
|
||||||
Value::Int(v) => Value::Int(v.abs()),
|
Value::Str(v) => v.len() as i64,
|
||||||
Value::Float(v) => Value::Float(v.abs()),
|
Value::Array(v) => v.len(),
|
||||||
Value::Length(v) => Value::Length(v.abs()),
|
Value::Dict(v) => v.len(),
|
||||||
Value::Angle(v) => Value::Angle(v.abs()),
|
v => bail!(
|
||||||
Value::Relative(v) => Value::Relative(v.abs()),
|
|
||||||
Value::Fractional(v) => Value::Fractional(v.abs()),
|
|
||||||
Value::Linear(_) => bail!(span, "cannot take absolute value of a linear"),
|
|
||||||
v => bail!(span, "expected numeric value, found {}", v.type_name()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The minimum of a sequence of values.
|
|
||||||
pub fn min(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
|
||||||
minmax(args, Ordering::Less)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The maximum of a sequence of values.
|
|
||||||
pub fn max(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
|
||||||
minmax(args, Ordering::Greater)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether an integer is even.
|
|
||||||
pub fn even(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
|
||||||
Ok(Value::Bool(args.expect::<i64>("integer")? % 2 == 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether an integer is odd.
|
|
||||||
pub fn odd(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
|
||||||
Ok(Value::Bool(args.expect::<i64>("integer")? % 2 != 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The modulo of two numbers.
|
|
||||||
pub fn modulo(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
|
||||||
let Spanned { v: v1, span: span1 } = args.expect("integer or float")?;
|
|
||||||
let Spanned { v: v2, span: span2 } = args.expect("integer or float")?;
|
|
||||||
|
|
||||||
let (a, b) = match (v1, v2) {
|
|
||||||
(Value::Int(a), Value::Int(b)) => match a.checked_rem(b) {
|
|
||||||
Some(res) => return Ok(Value::Int(res)),
|
|
||||||
None => bail!(span2, "divisor must not be zero"),
|
|
||||||
},
|
|
||||||
(Value::Int(a), Value::Float(b)) => (a as f64, b),
|
|
||||||
(Value::Float(a), Value::Int(b)) => (a, b as f64),
|
|
||||||
(Value::Float(a), Value::Float(b)) => (a, b),
|
|
||||||
(Value::Int(_), b) | (Value::Float(_), b) => bail!(
|
|
||||||
span2,
|
|
||||||
format!("expected integer or float, found {}", b.type_name())
|
|
||||||
),
|
|
||||||
(a, _) => bail!(
|
|
||||||
span1,
|
|
||||||
format!("expected integer or float, found {}", a.type_name())
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
span,
|
||||||
"cannot compare {} with {}",
|
"expected string, array or dictionary, found {}",
|
||||||
extremum.type_name(),
|
|
||||||
v.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.
|
/// 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.
|
/// The sorted version of an array.
|
||||||
pub fn sorted(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
pub fn sorted(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
||||||
let Spanned { v, span } = args.expect::<Spanned<Array>>("array")?;
|
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)] = &[
|
/// Converts an integer into a roman numeral.
|
||||||
("M̅", 1000000),
|
pub fn roman(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
||||||
("D̅", 500000),
|
convert(Numbering::Roman, args)
|
||||||
("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),
|
|
||||||
];
|
|
||||||
|
|
||||||
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)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub enum Numbering {
|
pub enum Numbering {
|
||||||
Arabic,
|
Arabic,
|
||||||
@ -92,22 +85,27 @@ impl Numbering {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts an integer into one or multiple letters.
|
static ROMANS: &'static [(&'static str, usize)] = &[
|
||||||
pub fn letter(_: &mut Context, args: &mut Args) -> TypResult<Value> {
|
("M̅", 1000000),
|
||||||
convert(Numbering::Letter, args)
|
("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.
|
static SYMBOLS: &'static [char] = &['*', '†', '‡', '§', '‖', '¶'];
|
||||||
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)))
|
|
||||||
}
|
|
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.
|
// Test overlong word that is not directly after a hard break.
|
@ -1,12 +1,6 @@
|
|||||||
// Test collection functions.
|
// Test collection functions.
|
||||||
// Ref: false
|
// 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 the `len` function.
|
||||||
#test(len(()), 0)
|
#test(len(()), 0)
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
// Test string functions.
|
// Test string handling functions.
|
||||||
|
// Ref: false
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test the `upper`, `lower`, and number formatting functions.
|
// 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("Abc 8")
|
||||||
#upper[def]
|
#upper[def]
|
||||||
|
|
||||||
|
@ -12,7 +12,8 @@ use typst::diag::Error;
|
|||||||
use typst::eval::{Smart, StyleMap, Value};
|
use typst::eval::{Smart, StyleMap, Value};
|
||||||
use typst::frame::{Element, Frame};
|
use typst::frame::{Element, Frame};
|
||||||
use typst::geom::{Length, RgbaColor};
|
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::loading::FsLoader;
|
||||||
use typst::parse::Scanner;
|
use typst::parse::Scanner;
|
||||||
use typst::source::SourceFile;
|
use typst::source::SourceFile;
|
||||||
|