Set Rules Episode II: Attack of the properties

This commit is contained in:
Laurenz 2021-12-07 16:36:39 +01:00
parent 26bdc1f0f6
commit 40b87d4066
19 changed files with 1119 additions and 1223 deletions

View File

@ -19,17 +19,18 @@ debug = 0
opt-level = 2 opt-level = 2
[dependencies] [dependencies]
fxhash = "0.2.1" fxhash = "0.2"
image = { version = "0.23", default-features = false, features = ["png", "jpeg"] } image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
itertools = "0.10" itertools = "0.10"
miniz_oxide = "0.4" miniz_oxide = "0.4"
once_cell = "1"
pdf-writer = "0.4" pdf-writer = "0.4"
rustybuzz = "0.4" rustybuzz = "0.4"
serde = { version = "1", features = ["derive", "rc"] } serde = { version = "1", features = ["derive", "rc"] }
svg2pdf = { version = "0.1", default-features = false, features = ["text", "png", "jpeg"] } svg2pdf = { version = "0.1", default-features = false, features = ["text", "png", "jpeg"] }
ttf-parser = "0.12" ttf-parser = "0.12"
unicode-bidi = "0.3.5" unicode-bidi = "0.3.5"
unicode-segmentation = "1.8" unicode-segmentation = "1"
unicode-xid = "0.2" unicode-xid = "0.2"
usvg = { version = "0.19", default-features = false, features = ["text"] } usvg = { version = "0.19", default-features = false, features = ["text"] }
xi-unicode = "0.3" xi-unicode = "0.3"

View File

@ -6,6 +6,8 @@ mod array;
mod dict; mod dict;
#[macro_use] #[macro_use]
mod value; mod value;
#[macro_use]
mod styles;
mod capture; mod capture;
mod function; mod function;
mod node; mod node;
@ -18,6 +20,7 @@ pub use dict::*;
pub use function::*; pub use function::*;
pub use node::*; pub use node::*;
pub use scope::*; pub use scope::*;
pub use styles::*;
pub use value::*; pub use value::*;
use std::cell::RefMut; use std::cell::RefMut;
@ -31,13 +34,12 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult}; use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult};
use crate::geom::{Angle, Fractional, Length, Relative, Spec}; use crate::geom::{Angle, Fractional, Length, Relative, Spec};
use crate::image::ImageStore; use crate::image::ImageStore;
use crate::library::{GridNode, TrackSizing}; use crate::library::{GridNode, TextNode, TrackSizing};
use crate::loading::Loader; use crate::loading::Loader;
use crate::source::{SourceId, SourceStore}; use crate::source::{SourceId, SourceStore};
use crate::style::Style;
use crate::syntax::ast::*; use crate::syntax::ast::*;
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
use crate::util::{BoolExt, EcoString, RefMutExt}; use crate::util::{EcoString, RefMutExt};
use crate::Context; use crate::Context;
/// Evaluate a parsed source file into a module. /// Evaluate a parsed source file into a module.
@ -70,8 +72,8 @@ pub struct EvalContext<'a> {
pub modules: HashMap<SourceId, Module>, pub modules: HashMap<SourceId, Module>,
/// The active scopes. /// The active scopes.
pub scopes: Scopes<'a>, pub scopes: Scopes<'a>,
/// The active style. /// The active styles.
pub style: Style, pub styles: Styles,
} }
impl<'a> EvalContext<'a> { impl<'a> EvalContext<'a> {
@ -84,7 +86,7 @@ impl<'a> EvalContext<'a> {
route: vec![source], route: vec![source],
modules: HashMap::new(), modules: HashMap::new(),
scopes: Scopes::new(Some(&ctx.std)), scopes: Scopes::new(Some(&ctx.std)),
style: ctx.style.clone(), styles: Styles::new(),
} }
} }
@ -158,14 +160,10 @@ impl Eval for Markup {
type Output = Node; type Output = Node;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let snapshot = ctx.style.clone();
let mut result = Node::new(); let mut result = Node::new();
for piece in self.nodes() { for piece in self.nodes() {
result += piece.eval(ctx)?; result += piece.eval(ctx)?;
} }
ctx.style = snapshot;
Ok(result) Ok(result)
} }
} }
@ -179,11 +177,11 @@ impl Eval for MarkupNode {
Self::Linebreak => Node::Linebreak, Self::Linebreak => Node::Linebreak,
Self::Parbreak => Node::Parbreak, Self::Parbreak => Node::Parbreak,
Self::Strong => { Self::Strong => {
ctx.style.text_mut().strong.flip(); ctx.styles.set(TextNode::STRONG, !ctx.styles.get(TextNode::STRONG));
Node::new() Node::new()
} }
Self::Emph => { Self::Emph => {
ctx.style.text_mut().emph.flip(); ctx.styles.set(TextNode::EMPH, !ctx.styles.get(TextNode::EMPH));
Node::new() Node::new()
} }
Self::Text(text) => Node::Text(text.clone()), Self::Text(text) => Node::Text(text.clone()),

View File

@ -9,6 +9,7 @@ use crate::geom::SpecAxis;
use crate::layout::{Layout, PackedNode}; use crate::layout::{Layout, PackedNode};
use crate::library::{ use crate::library::{
Decoration, DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, Spacing, Decoration, DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, Spacing,
TextNode,
}; };
use crate::util::EcoString; use crate::util::EcoString;
@ -158,10 +159,10 @@ impl NodePacker {
fn walk(&mut self, node: Node) { fn walk(&mut self, node: Node) {
match node { match node {
Node::Space => { Node::Space => {
self.push_inline(ParChild::Text(' '.into())); self.push_inline(ParChild::Text(TextNode(' '.into())));
} }
Node::Linebreak => { Node::Linebreak => {
self.push_inline(ParChild::Text('\n'.into())); self.push_inline(ParChild::Text(TextNode('\n'.into())));
} }
Node::Parbreak => { Node::Parbreak => {
self.parbreak(); self.parbreak();
@ -170,7 +171,7 @@ impl NodePacker {
self.pagebreak(); self.pagebreak();
} }
Node::Text(text) => { Node::Text(text) => {
self.push_inline(ParChild::Text(text)); self.push_inline(ParChild::Text(TextNode(text)));
} }
Node::Spacing(axis, amount) => match axis { Node::Spacing(axis, amount) => match axis {
SpecAxis::Horizontal => self.push_inline(ParChild::Spacing(amount)), SpecAxis::Horizontal => self.push_inline(ParChild::Spacing(amount)),

124
src/eval/styles.rs Normal file
View File

@ -0,0 +1,124 @@
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::rc::Rc;
// Possible optimizations:
// - Ref-count map for cheaper cloning and smaller footprint
// - Store map in `Option` to make empty maps non-allocating
// - Store small properties inline
/// A map of style properties.
#[derive(Default, Clone)]
pub struct Styles {
map: HashMap<TypeId, Rc<dyn Any>>,
}
impl Styles {
/// Create a new, empty style map.
pub fn new() -> Self {
Self { map: HashMap::new() }
}
/// Set the value for a style property.
pub fn set<P: Property>(&mut self, _: P, value: P::Value) {
self.map.insert(TypeId::of::<P>(), Rc::new(value));
}
/// Get the value of a copyable style property.
///
/// Returns the property's default value if the map does not contain an
/// entry for it.
pub fn get<P: Property>(&self, key: P) -> P::Value
where
P::Value: Copy,
{
self.get_inner(key).copied().unwrap_or_else(P::default)
}
/// Get a reference to a style property.
///
/// Returns a reference to the property's default value if the map does not
/// contain an entry for it.
pub fn get_ref<P: Property>(&self, key: P) -> &P::Value {
self.get_inner(key).unwrap_or_else(|| P::default_ref())
}
/// Get a reference to a style directly in this map.
fn get_inner<P: Property>(&self, _: P) -> Option<&P::Value> {
self.map
.get(&TypeId::of::<P>())
.and_then(|boxed| boxed.downcast_ref())
}
}
impl Debug for Styles {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
// TODO(set): Better debug printing possible?
f.pad("Styles(..)")
}
}
/// Stylistic property keys.
pub trait Property: 'static {
/// The type of this property, for example, this could be
/// [`Length`](crate::geom::Length) for a `WIDTH` property.
type Value;
/// The default value of the property.
fn default() -> Self::Value;
/// A static reference to the default value of the property.
///
/// This is automatically implemented through lazy-initialization in the
/// `properties!` macro. This way, expensive defaults don't need to be
/// recreated all the time.
fn default_ref() -> &'static Self::Value;
}
macro_rules! set {
($ctx:expr, $target:expr => $source:expr) => {
if let Some(v) = $source {
$ctx.styles.set($target, v);
}
};
}
macro_rules! properties {
($node:ty, $(
$(#[$attr:meta])*
$name:ident: $type:ty = $default:expr
),* $(,)?) => {
// TODO(set): Fix possible name clash.
mod properties {
use std::marker::PhantomData;
use super::*;
$(#[allow(non_snake_case)] mod $name {
use $crate::eval::Property;
use once_cell::sync::Lazy;
use super::*;
pub struct Key<T>(pub PhantomData<T>);
impl Property for Key<$type> {
type Value = $type;
fn default() -> Self::Value {
$default
}
fn default_ref() -> &'static Self::Value {
static LAZY: Lazy<$type> = Lazy::new(|| $default);
&*LAZY
}
}
})*
impl $node {
$($(#[$attr])* pub const $name: $name::Key<$type>
= $name::Key(PhantomData);)*
}
}
};
}

View File

@ -171,14 +171,19 @@ pub struct Text {
pub face_id: FaceId, pub face_id: FaceId,
/// The font size. /// The font size.
pub size: Length, pub size: Length,
/// The width of the text run.
pub width: Length,
/// Glyph color. /// Glyph color.
pub fill: Paint, pub fill: Paint,
/// The glyphs. /// The glyphs.
pub glyphs: Vec<Glyph>, pub glyphs: Vec<Glyph>,
} }
impl Text {
/// The width of the text run.
pub fn width(&self) -> Length {
self.glyphs.iter().map(|g| g.x_advance.to_length(self.size)).sum()
}
}
/// A glyph in a run of shaped text. /// A glyph in a run of shaped text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Glyph { pub struct Glyph {

View File

@ -44,13 +44,12 @@ pub mod library;
pub mod loading; pub mod loading;
pub mod parse; pub mod parse;
pub mod source; pub mod source;
pub mod style;
pub mod syntax; pub mod syntax;
use std::rc::Rc; use std::rc::Rc;
use crate::diag::TypResult; use crate::diag::TypResult;
use crate::eval::{Module, Scope}; use crate::eval::{Module, Scope, Styles};
use crate::font::FontStore; use crate::font::FontStore;
use crate::frame::Frame; use crate::frame::Frame;
use crate::image::ImageStore; use crate::image::ImageStore;
@ -59,7 +58,6 @@ use crate::layout::{EvictionPolicy, LayoutCache};
use crate::library::DocumentNode; use crate::library::DocumentNode;
use crate::loading::Loader; use crate::loading::Loader;
use crate::source::{SourceId, SourceStore}; use crate::source::{SourceId, SourceStore};
use crate::style::Style;
/// The core context which holds the loader, configuration and cached artifacts. /// The core context which holds the loader, configuration and cached artifacts.
pub struct Context { pub struct Context {
@ -76,8 +74,8 @@ pub struct Context {
pub layouts: LayoutCache, pub layouts: LayoutCache,
/// The standard library scope. /// The standard library scope.
std: Scope, std: Scope,
/// The default style. /// The default styles.
style: Style, styles: Styles,
} }
impl Context { impl Context {
@ -96,9 +94,9 @@ impl Context {
&self.std &self.std
} }
/// A read-only reference to the style. /// A read-only reference to the styles.
pub fn style(&self) -> &Style { pub fn styles(&self) -> &Styles {
&self.style &self.styles
} }
/// Evaluate a source file and return the resulting module. /// Evaluate a source file and return the resulting module.
@ -136,7 +134,7 @@ impl Context {
/// This struct is created by [`Context::builder`]. /// This struct is created by [`Context::builder`].
pub struct ContextBuilder { pub struct ContextBuilder {
std: Option<Scope>, std: Option<Scope>,
style: Option<Style>, styles: Option<Styles>,
#[cfg(feature = "layout-cache")] #[cfg(feature = "layout-cache")]
policy: EvictionPolicy, policy: EvictionPolicy,
#[cfg(feature = "layout-cache")] #[cfg(feature = "layout-cache")]
@ -151,9 +149,9 @@ impl ContextBuilder {
self self
} }
/// The initial properties for page size, font selection and so on. /// The default properties for page size, font selection and so on.
pub fn style(mut self, style: Style) -> Self { pub fn styles(mut self, styles: Styles) -> Self {
self.style = Some(style); self.styles = Some(styles);
self self
} }
@ -185,7 +183,7 @@ impl ContextBuilder {
#[cfg(feature = "layout-cache")] #[cfg(feature = "layout-cache")]
layouts: LayoutCache::new(self.policy, self.max_size), layouts: LayoutCache::new(self.policy, self.max_size),
std: self.std.unwrap_or_else(library::new), std: self.std.unwrap_or_else(library::new),
style: self.style.unwrap_or_default(), styles: self.styles.unwrap_or_default(),
} }
} }
} }
@ -194,7 +192,7 @@ impl Default for ContextBuilder {
fn default() -> Self { fn default() -> Self {
Self { Self {
std: None, std: None,
style: None, styles: None,
#[cfg(feature = "layout-cache")] #[cfg(feature = "layout-cache")]
policy: EvictionPolicy::default(), policy: EvictionPolicy::default(),
#[cfg(feature = "layout-cache")] #[cfg(feature = "layout-cache")]

View File

@ -2,18 +2,6 @@ use super::prelude::*;
/// `align`: Configure the alignment along the layouting axes. /// `align`: Configure the alignment along the layouting axes.
pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
castable! {
Spec<Option<Align>>,
Expected: "1d or 2d alignment",
@align: Align => {
let mut aligns = Spec::default();
aligns.set(align.axis(), Some(*align));
aligns
},
@aligns: Spec<Align> => aligns.map(Some),
}
let aligns = args.expect::<Spec<_>>("alignment")?; let aligns = args.expect::<Spec<_>>("alignment")?;
let body = args.expect::<Node>("body")?; let body = args.expect::<Node>("body")?;
@ -62,3 +50,19 @@ impl Layout for AlignNode {
frames frames
} }
} }
dynamic! {
Align: "alignment",
}
castable! {
Spec<Option<Align>>,
Expected: "1d or 2d alignment",
@align: Align => {
let mut aligns = Spec::default();
aligns.set(align.axis(), Some(*align));
aligns
},
@aligns: Spec<Align> => aligns.map(Some),
}

View File

@ -31,7 +31,11 @@ fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let url = args.expect::<EcoString>("url")?; let url = args.expect::<EcoString>("url")?;
let body = args.find().unwrap_or_else(|| { let body = args.find().unwrap_or_else(|| {
Node::Text(url.trim_start_matches("mailto:").trim_start_matches("tel:").into()) let mut text = url.as_str();
for prefix in ["mailto:", "tel:"] {
text = text.trim_start_matches(prefix);
}
Node::Text(text.into())
}); });
Ok(Value::Node(body.decorate(Decoration::Link(url)))) Ok(Value::Node(body.decorate(Decoration::Link(url))))
} }
@ -120,7 +124,7 @@ impl LineDecoration {
let extent = self.extent.resolve(text.size); let extent = self.extent.resolve(text.size);
let subpos = Point::new(pos.x - extent, pos.y + offset); let subpos = Point::new(pos.x - extent, pos.y + offset);
let target = Point::new(text.width + 2.0 * extent, Length::zero()); let target = Point::new(text.width() + 2.0 * extent, Length::zero());
let shape = Shape::stroked(Geometry::Line(target), stroke); let shape = Shape::stroked(Geometry::Line(target), stroke);
frame.push(subpos, Element::Shape(shape)); frame.push(subpos, Element::Shape(shape));
} }

View File

@ -39,8 +39,11 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
row_gutter.unwrap_or(base_gutter), row_gutter.unwrap_or(base_gutter),
); );
let children = args.all().map(Node::into_block).collect(); Ok(Value::block(GridNode {
Ok(Value::block(GridNode { tracks, gutter, children })) tracks,
gutter,
children: args.all().map(Node::into_block).collect(),
}))
} }
/// A node that arranges its children in a grid. /// A node that arranges its children in a grid.

View File

@ -26,7 +26,7 @@ mod prelude {
pub use std::rc::Rc; pub use std::rc::Rc;
pub use crate::diag::{At, TypResult}; pub use crate::diag::{At, TypResult};
pub use crate::eval::{Args, EvalContext, Node, Smart, Value}; pub use crate::eval::{Args, EvalContext, Node, Property, Smart, Styles, Value};
pub use crate::frame::*; pub use crate::frame::*;
pub use crate::geom::*; pub use crate::geom::*;
pub use crate::layout::*; pub use crate::layout::*;
@ -54,7 +54,6 @@ pub use utility::*;
use crate::eval::{Scope, Value}; use crate::eval::{Scope, Value};
use crate::geom::*; use crate::geom::*;
use crate::style::FontFamily;
/// Construct a scope containing all standard library definitions. /// Construct a scope containing all standard library definitions.
pub fn new() -> Scope { pub fn new() -> Scope {
@ -139,15 +138,6 @@ dynamic! {
Dir: "direction", Dir: "direction",
} }
dynamic! {
Align: "alignment",
}
dynamic! {
FontFamily: "font family",
Value::Str(string) => Self::Named(string.to_lowercase()),
}
castable! { castable! {
Paint, Paint,
Expected: "color", Expected: "color",

View File

@ -1,70 +1,43 @@
#![allow(unused)]
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use super::prelude::*; use super::prelude::*;
use super::PadNode; use super::PadNode;
use crate::style::{Paper, PaperClass};
/// `page`: Configure pages. /// `page`: Configure pages.
pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
castable! { castable! {
Paper, Paper,
Expected: "string", Expected: "string",
Value::Str(string) => Paper::from_name(&string).ok_or("unknown paper")?, Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?,
}
if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
ctx.styles.set(PageNode::CLASS, paper.class());
ctx.styles.set(PageNode::WIDTH, Smart::Custom(paper.width()));
ctx.styles.set(PageNode::HEIGHT, Smart::Custom(paper.height()));
}
if let Some(width) = args.named("width")? {
ctx.styles.set(PageNode::CLASS, PaperClass::Custom);
ctx.styles.set(PageNode::WIDTH, width);
}
if let Some(height) = args.named("height")? {
ctx.styles.set(PageNode::CLASS, PaperClass::Custom);
ctx.styles.set(PageNode::HEIGHT, height);
} }
let paper = args.named::<Paper>("paper")?.or_else(|| args.find());
let width = args.named::<Smart<_>>("width")?;
let height = args.named::<Smart<_>>("height")?;
let flip = args.named("flip")?;
let margins = args.named("margins")?; let margins = args.named("margins")?;
let left = args.named("left")?;
let top = args.named("top")?;
let right = args.named("right")?;
let bottom = args.named("bottom")?;
let fill = args.named("fill")?;
let page = ctx.style.page_mut(); set!(ctx, PageNode::FLIPPED => args.named("flipped")?);
set!(ctx, PageNode::LEFT => args.named("left")?.or(margins));
if let Some(paper) = paper { set!(ctx, PageNode::TOP => args.named("top")?.or(margins));
page.class = paper.class(); set!(ctx, PageNode::RIGHT => args.named("right")?.or(margins));
page.size = paper.size(); set!(ctx, PageNode::BOTTOM => args.named("bottom")?.or(margins));
} set!(ctx, PageNode::FILL => args.named("fill")?);
if let Some(width) = width {
page.class = PaperClass::Custom;
page.size.x = width.unwrap_or(Length::inf());
}
if let Some(height) = height {
page.class = PaperClass::Custom;
page.size.y = height.unwrap_or(Length::inf());
}
if flip.unwrap_or(false) {
std::mem::swap(&mut page.size.x, &mut page.size.y);
}
if let Some(margins) = margins {
page.margins = Sides::splat(margins);
}
if let Some(left) = left {
page.margins.left = left;
}
if let Some(top) = top {
page.margins.top = top;
}
if let Some(right) = right {
page.margins.right = right;
}
if let Some(bottom) = bottom {
page.margins.bottom = bottom;
}
if let Some(fill) = fill {
page.fill = fill;
}
Ok(Value::None) Ok(Value::None)
} }
@ -74,29 +47,69 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
Ok(Value::Node(Node::Pagebreak)) Ok(Value::Node(Node::Pagebreak))
} }
/// Layouts its children onto one or multiple pages. /// Layouts its child onto one or multiple pages.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct PageNode(pub PackedNode); pub struct PageNode(pub PackedNode);
properties! {
PageNode,
/// The unflipped width of the page.
WIDTH: Smart<Length> = Smart::Custom(Paper::default().width()),
/// The unflipped height of the page.
HEIGHT: Smart<Length> = Smart::Custom(Paper::default().height()),
/// The class of paper. Defines the default margins.
CLASS: PaperClass = Paper::default().class(),
/// Whether the page is flipped into landscape orientation.
FLIPPED: bool = false,
/// The left margin.
LEFT: Smart<Linear> = Smart::Auto,
/// The right margin.
RIGHT: Smart<Linear> = Smart::Auto,
/// The top margin.
TOP: Smart<Linear> = Smart::Auto,
/// The bottom margin.
BOTTOM: Smart<Linear> = Smart::Auto,
/// The page's background color.
FILL: Option<Paint> = None,
}
impl PageNode { impl PageNode {
/// Layout the page run into a sequence of frames, one per page. /// Layout the page run into a sequence of frames, one per page.
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
// TODO(set): Get style from styles. // TODO(set): Take styles as parameter.
let style = crate::style::PageStyle::default(); let styles = Styles::new();
// When one of the lengths is infinite the page fits its content along // When one of the lengths is infinite the page fits its content along
// that axis. // that axis.
let expand = style.size.map(Length::is_finite); let width = styles.get(Self::WIDTH).unwrap_or(Length::inf());
let regions = Regions::repeat(style.size, style.size, expand); let height = styles.get(Self::HEIGHT).unwrap_or(Length::inf());
let mut size = Size::new(width, height);
if styles.get(Self::FLIPPED) {
std::mem::swap(&mut size.x, &mut size.y);
}
// Determine the margins.
let class = styles.get(Self::CLASS);
let default = class.default_margins();
let padding = Sides {
left: styles.get(Self::LEFT).unwrap_or(default.left),
right: styles.get(Self::RIGHT).unwrap_or(default.right),
top: styles.get(Self::TOP).unwrap_or(default.top),
bottom: styles.get(Self::BOTTOM).unwrap_or(default.bottom),
};
// Pad the child.
let padded = PadNode { child: self.0.clone(), padding }.pack();
// Layout the child. // Layout the child.
let padding = style.margins(); let expand = size.map(Length::is_finite);
let padded = PadNode { child: self.0.clone(), padding }.pack(); let regions = Regions::repeat(size, size, expand);
let mut frames: Vec<_> = let mut frames: Vec<_> =
padded.layout(ctx, &regions).into_iter().map(|c| c.item).collect(); padded.layout(ctx, &regions).into_iter().map(|c| c.item).collect();
// Add background fill if requested. // Add background fill if requested.
if let Some(fill) = style.fill { if let Some(fill) = styles.get(Self::FILL) {
for frame in &mut frames { for frame in &mut frames {
let shape = Shape::filled(Geometry::Rect(frame.size), fill); let shape = Shape::filled(Geometry::Rect(frame.size), fill);
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
@ -106,3 +119,256 @@ impl PageNode {
frames frames
} }
} }
/// Specification of a paper.
#[derive(Debug, Copy, Clone)]
pub struct Paper {
/// The broad class this paper belongs to.
class: PaperClass,
/// The width of the paper in millimeters.
width: f64,
/// The height of the paper in millimeters.
height: f64,
}
impl Paper {
/// The class of the paper.
pub fn class(self) -> PaperClass {
self.class
}
/// The width of the paper.
pub fn width(self) -> Length {
Length::mm(self.width)
}
/// The height of the paper.
pub fn height(self) -> Length {
Length::mm(self.height)
}
}
impl Default for Paper {
fn default() -> Self {
Paper::A4
}
}
/// Defines default margins for a class of related papers.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum PaperClass {
Custom,
Base,
US,
Newspaper,
Book,
}
impl PaperClass {
/// The default margins for this page class.
fn default_margins(self) -> Sides<Linear> {
let f = |r| Relative::new(r).into();
let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b));
match self {
Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842),
Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842),
Self::US => s(0.1760, 0.1092, 0.1760, 0.0910),
Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294),
Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965),
}
}
}
/// Defines paper constants and a paper parsing implementation.
macro_rules! papers {
($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => {
/// Predefined papers.
///
/// Each paper is parsable from its name in kebab-case.
impl Paper {
$(pub const $var: Self = Self {
class: PaperClass::$class,
width: $width,
height: $height,
};)*
}
impl FromStr for Paper {
type Err = ParsePaperError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
match name.to_lowercase().as_str() {
$($($pats)* => Ok(Self::$var),)*
_ => Err(ParsePaperError),
}
}
}
/// The error when parsing a [`Paper`] from a string fails.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct ParsePaperError;
impl Display for ParsePaperError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("invalid paper name")
}
}
impl std::error::Error for ParsePaperError {}
};
}
// All paper sizes in mm.
//
// Resources:
// - https://papersizes.io/
// - https://en.wikipedia.org/wiki/Paper_size
// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
papers! {
// ---------------------------------------------------------------------- //
// ISO 216 A Series
(A0: Base, 841.0, 1189.0, "a0")
(A1: Base, 594.0, 841.0, "a1")
(A2: Base, 420.0, 594.0, "a2")
(A3: Base, 297.0, 420.0, "a3")
(A4: Base, 210.0, 297.0, "a4")
(A5: Base, 148.0, 210.0, "a5")
(A6: Book, 105.0, 148.0, "a6")
(A7: Base, 74.0, 105.0, "a7")
(A8: Base, 52.0, 74.0, "a8")
(A9: Base, 37.0, 52.0, "a9")
(A10: Base, 26.0, 37.0, "a10")
(A11: Base, 18.0, 26.0, "a11")
// ISO 216 B Series
(ISO_B1: Base, 707.0, 1000.0, "iso-b1")
(ISO_B2: Base, 500.0, 707.0, "iso-b2")
(ISO_B3: Base, 353.0, 500.0, "iso-b3")
(ISO_B4: Base, 250.0, 353.0, "iso-b4")
(ISO_B5: Book, 176.0, 250.0, "iso-b5")
(ISO_B6: Book, 125.0, 176.0, "iso-b6")
(ISO_B7: Base, 88.0, 125.0, "iso-b7")
(ISO_B8: Base, 62.0, 88.0, "iso-b8")
// ISO 216 C Series
(ISO_C3: Base, 324.0, 458.0, "iso-c3")
(ISO_C4: Base, 229.0, 324.0, "iso-c4")
(ISO_C5: Base, 162.0, 229.0, "iso-c5")
(ISO_C6: Base, 114.0, 162.0, "iso-c6")
(ISO_C7: Base, 81.0, 114.0, "iso-c7")
(ISO_C8: Base, 57.0, 81.0, "iso-c8")
// DIN D Series (extension to ISO)
(DIN_D3: Base, 272.0, 385.0, "din-d3")
(DIN_D4: Base, 192.0, 272.0, "din-d4")
(DIN_D5: Base, 136.0, 192.0, "din-d5")
(DIN_D6: Base, 96.0, 136.0, "din-d6")
(DIN_D7: Base, 68.0, 96.0, "din-d7")
(DIN_D8: Base, 48.0, 68.0, "din-d8")
// SIS (used in academia)
(SIS_G5: Base, 169.0, 239.0, "sis-g5")
(SIS_E5: Base, 115.0, 220.0, "sis-e5")
// ANSI Extensions
(ANSI_A: Base, 216.0, 279.0, "ansi-a")
(ANSI_B: Base, 279.0, 432.0, "ansi-b")
(ANSI_C: Base, 432.0, 559.0, "ansi-c")
(ANSI_D: Base, 559.0, 864.0, "ansi-d")
(ANSI_E: Base, 864.0, 1118.0, "ansi-e")
// ANSI Architectural Paper
(ARCH_A: Base, 229.0, 305.0, "arch-a")
(ARCH_B: Base, 305.0, 457.0, "arch-b")
(ARCH_C: Base, 457.0, 610.0, "arch-c")
(ARCH_D: Base, 610.0, 914.0, "arch-d")
(ARCH_E1: Base, 762.0, 1067.0, "arch-e1")
(ARCH_E: Base, 914.0, 1219.0, "arch-e")
// JIS B Series
(JIS_B0: Base, 1030.0, 1456.0, "jis-b0")
(JIS_B1: Base, 728.0, 1030.0, "jis-b1")
(JIS_B2: Base, 515.0, 728.0, "jis-b2")
(JIS_B3: Base, 364.0, 515.0, "jis-b3")
(JIS_B4: Base, 257.0, 364.0, "jis-b4")
(JIS_B5: Base, 182.0, 257.0, "jis-b5")
(JIS_B6: Base, 128.0, 182.0, "jis-b6")
(JIS_B7: Base, 91.0, 128.0, "jis-b7")
(JIS_B8: Base, 64.0, 91.0, "jis-b8")
(JIS_B9: Base, 45.0, 64.0, "jis-b9")
(JIS_B10: Base, 32.0, 45.0, "jis-b10")
(JIS_B11: Base, 22.0, 32.0, "jis-b11")
// SAC D Series
(SAC_D0: Base, 764.0, 1064.0, "sac-d0")
(SAC_D1: Base, 532.0, 760.0, "sac-d1")
(SAC_D2: Base, 380.0, 528.0, "sac-d2")
(SAC_D3: Base, 264.0, 376.0, "sac-d3")
(SAC_D4: Base, 188.0, 260.0, "sac-d4")
(SAC_D5: Base, 130.0, 184.0, "sac-d5")
(SAC_D6: Base, 92.0, 126.0, "sac-d6")
// ISO 7810 ID
(ISO_ID_1: Base, 85.6, 53.98, "iso-id-1")
(ISO_ID_2: Base, 74.0, 105.0, "iso-id-2")
(ISO_ID_3: Base, 88.0, 125.0, "iso-id-3")
// ---------------------------------------------------------------------- //
// Asia
(ASIA_F4: Base, 210.0, 330.0, "asia-f4")
// Japan
(JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4")
(JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5")
(JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6")
(JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4")
(JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5")
(JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card")
// China
(CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card")
// Europe
(EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card")
// French Traditional (AFNOR)
(FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière")
(FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture")
(FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition")
(FR_RAISIN: Base, 500.0, 650.0, "fr-raisin")
(FR_CARRE: Base, 450.0, 560.0, "fr-carré")
(FR_JESUS: Base, 560.0, 760.0, "fr-jésus")
// United Kingdom Imperial
(UK_BRIEF: Base, 406.4, 342.9, "uk-brief")
(UK_DRAFT: Base, 254.0, 406.4, "uk-draft")
(UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap")
(UK_QUARTO: Base, 203.2, 254.0, "uk-quarto")
(UK_CROWN: Base, 508.0, 381.0, "uk-crown")
(UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a")
(UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b")
// Unites States
(US_LETTER: US, 215.9, 279.4, "us-letter")
(US_LEGAL: US, 215.9, 355.6, "us-legal")
(US_TABLOID: US, 279.4, 431.8, "us-tabloid")
(US_EXECUTIVE: US, 184.15, 266.7, "us-executive")
(US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio")
(US_STATEMENT: US, 139.7, 215.9, "us-statement")
(US_LEDGER: US, 431.8, 279.4, "us-ledger")
(US_OFICIO: US, 215.9, 340.36, "us-oficio")
(US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter")
(US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal")
(US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card")
(US_DIGEST: Book, 139.7, 215.9, "us-digest")
(US_TRADE: Book, 152.4, 228.6, "us-trade")
// ---------------------------------------------------------------------- //
// Other
(NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact")
(NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner")
(NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet")
(PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9")
(PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3")
}

View File

@ -6,8 +6,7 @@ use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator; use xi_unicode::LineBreakIterator;
use super::prelude::*; use super::prelude::*;
use super::{shape, Decoration, ShapedText, Spacing}; use super::{shape, Decoration, ShapedText, Spacing, TextNode};
use crate::style::TextStyle;
use crate::util::{EcoString, RangeExt, RcExt, SliceExt}; use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
/// `par`: Configure paragraphs. /// `par`: Configure paragraphs.
@ -38,24 +37,14 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
align = Some(v); align = Some(v);
} }
let par = ctx.style.par_mut(); if let (Some(dir), None) = (dir, align) {
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
if let Some(dir) = dir {
par.dir = dir;
par.align = if dir == Dir::LTR { Align::Left } else { Align::Right };
} }
if let Some(align) = align { set!(ctx, ParNode::DIR => dir);
par.align = align; set!(ctx, ParNode::ALIGN => align);
} set!(ctx, ParNode::LEADING => leading);
set!(ctx, ParNode::SPACING => spacing);
if let Some(leading) = leading {
par.leading = leading;
}
if let Some(spacing) = spacing {
par.spacing = spacing;
}
Ok(Value::None) Ok(Value::None)
} }
@ -64,24 +53,39 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ParNode(pub Vec<ParChild>); pub struct ParNode(pub Vec<ParChild>);
properties! {
ParNode,
/// The direction for text and inline objects.
DIR: Dir = Dir::LTR,
/// How to align text and inline objects in their line.
ALIGN: Align = Align::Left,
// TODO(set): Make relative to font size.
/// The spacing between lines (dependent on scaled font size).
LEADING: Length = Length::pt(6.5),
/// The spacing between paragraphs (dependent on scaled font size).
SPACING: Length = Length::pt(12.0),
}
impl Layout for ParNode { impl Layout for ParNode {
fn layout( fn layout(
&self, &self,
ctx: &mut LayoutContext, ctx: &mut LayoutContext,
regions: &Regions, regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> { ) -> Vec<Constrained<Rc<Frame>>> {
// TODO(set): Take styles as parameter.
let styles = Styles::new();
// Collect all text into one string used for BiDi analysis. // Collect all text into one string used for BiDi analysis.
let text = self.collect_text(); let text = self.collect_text();
// Find out the BiDi embedding levels. // Find out the BiDi embedding levels.
// TODO(set): Get dir from styles. let default_level = Level::from_dir(styles.get(Self::DIR));
let bidi = BidiInfo::new(&text, Level::from_dir(Dir::LTR)); let bidi = BidiInfo::new(&text, default_level);
// 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.
// TODO(set): Get text style from styles. let layouter = ParLayouter::new(self, ctx, regions, bidi, &styles);
let style = crate::style::TextStyle::default();
let layouter = ParLayouter::new(self, ctx, regions, bidi, &style);
// Find suitable linebreaks. // Find suitable linebreaks.
layouter.layout(ctx, regions.clone()) layouter.layout(ctx, regions.clone())
@ -115,7 +119,7 @@ impl ParNode {
fn strings(&self) -> impl Iterator<Item = &str> { fn strings(&self) -> impl Iterator<Item = &str> {
self.0.iter().map(|child| match child { self.0.iter().map(|child| match child {
ParChild::Spacing(_) => " ", ParChild::Spacing(_) => " ",
ParChild::Text(ref piece, ..) => piece, ParChild::Text(ref piece, ..) => &piece.0,
ParChild::Node(..) => "\u{FFFC}", ParChild::Node(..) => "\u{FFFC}",
ParChild::Decorate(_) | ParChild::Undecorate => "", ParChild::Decorate(_) | ParChild::Undecorate => "",
}) })
@ -128,7 +132,8 @@ pub enum ParChild {
/// Spacing between other nodes. /// Spacing between other nodes.
Spacing(Spacing), Spacing(Spacing),
/// A run of text and how to align it in its line. /// A run of text and how to align it in its line.
Text(EcoString), // TODO(set): A single text run may also have its own style.
Text(TextNode),
/// Any child node and how to align it in its line. /// Any child node and how to align it in its line.
Node(PackedNode), Node(PackedNode),
/// A decoration that applies until a matching `Undecorate`. /// A decoration that applies until a matching `Undecorate`.
@ -188,7 +193,7 @@ impl<'a> ParLayouter<'a> {
ctx: &mut LayoutContext, ctx: &mut LayoutContext,
regions: &Regions, regions: &Regions,
bidi: BidiInfo<'a>, bidi: BidiInfo<'a>,
style: &'a TextStyle, styles: &'a Styles,
) -> Self { ) -> Self {
let mut items = vec![]; let mut items = vec![];
let mut ranges = vec![]; let mut ranges = vec![];
@ -215,7 +220,7 @@ impl<'a> ParLayouter<'a> {
cursor += group.len(); cursor += group.len();
let subrange = start .. cursor; let subrange = start .. cursor;
let text = &bidi.text[subrange.clone()]; let text = &bidi.text[subrange.clone()];
let shaped = shape(ctx, text, style, level.dir()); let shaped = shape(ctx, text, styles, level.dir());
items.push(ParItem::Text(shaped)); items.push(ParItem::Text(shaped));
ranges.push(subrange); ranges.push(subrange);
} }
@ -243,9 +248,8 @@ impl<'a> ParLayouter<'a> {
} }
Self { Self {
// TODO(set): Get alignment and leading from styles. align: styles.get(ParNode::ALIGN),
align: Align::Left, leading: styles.get(ParNode::LEADING),
leading: Length::pt(6.0),
bidi, bidi,
items, items,
ranges, ranges,
@ -540,7 +544,7 @@ impl<'a> LineLayout<'a> {
// Compute the reordered ranges in visual order (left to right). // Compute the reordered ranges in visual order (left to right).
self.par.bidi.visual_runs(para, self.line.clone()) self.par.bidi.visual_runs(para, self.line.clone())
} else { } else {
<_>::default() (vec![], vec![])
}; };
runs.into_iter() runs.into_iter()

View File

@ -7,24 +7,21 @@ pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let tx = args.named("dx")?.unwrap_or_default(); let tx = args.named("dx")?.unwrap_or_default();
let ty = args.named("dy")?.unwrap_or_default(); let ty = args.named("dy")?.unwrap_or_default();
let body: Node = args.expect("body")?; let body: Node = args.expect("body")?;
Ok(Value::block(PlacedNode { Ok(Value::block(PlacedNode(
child: body.into_block().moved(Point::new(tx, ty)).aligned(aligns), body.into_block().moved(Point::new(tx, ty)).aligned(aligns),
})) )))
} }
/// A node that places its child absolutely. /// A node that places its child absolutely.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct PlacedNode { pub struct PlacedNode(pub PackedNode);
/// The node to be placed.
pub child: PackedNode,
}
impl PlacedNode { impl PlacedNode {
/// Whether this node wants to be placed relative to its its parent's base /// Whether this node wants to be placed relative to its its parent's base
/// origin. instead of relative to the parent's current flow/cursor /// origin. instead of relative to the parent's current flow/cursor
/// position. /// position.
pub fn out_of_flow(&self) -> bool { pub fn out_of_flow(&self) -> bool {
self.child self.0
.downcast::<AlignNode>() .downcast::<AlignNode>()
.map_or(false, |node| node.aligns.y.is_some()) .map_or(false, |node| node.aligns.y.is_some())
} }
@ -46,7 +43,7 @@ impl Layout for PlacedNode {
Regions::one(regions.base, regions.base, expand) Regions::one(regions.base, regions.base, expand)
}; };
let mut frames = self.child.layout(ctx, &pod); let mut frames = self.0.layout(ctx, &pod);
let Constrained { item: frame, cts } = &mut frames[0]; let Constrained { item: frame, cts } = &mut frames[0];
// If expansion is off, zero all sizes so that we don't take up any // If expansion is off, zero all sizes so that we don't take up any

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@ fn transform_impl(args: &mut Args, transform: Transform) -> TypResult<Value> {
.named("origin")? .named("origin")?
.unwrap_or(Spec::splat(None)) .unwrap_or(Spec::splat(None))
.unwrap_or(Align::CENTER_HORIZON); .unwrap_or(Align::CENTER_HORIZON);
Ok(Value::inline( Ok(Value::inline(
body.into_block().transformed(transform, origin), body.into_block().transformed(transform, origin),
)) ))

View File

@ -1,419 +0,0 @@
//! Style properties.
mod paper;
pub use paper::*;
use std::fmt::{self, Debug, Formatter};
use std::rc::Rc;
use ttf_parser::Tag;
use crate::eval::Smart;
use crate::font::*;
use crate::geom::*;
use crate::util::EcoString;
/// Defines a set of properties a template can be instantiated with.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Style {
/// The page settings.
pub page: Rc<PageStyle>,
/// The paragraph settings.
pub par: Rc<ParStyle>,
/// The current text settings.
pub text: Rc<TextStyle>,
}
impl Style {
/// Access the `page` style mutably.
pub fn page_mut(&mut self) -> &mut PageStyle {
Rc::make_mut(&mut self.page)
}
/// Access the `par` style mutably.
pub fn par_mut(&mut self) -> &mut ParStyle {
Rc::make_mut(&mut self.par)
}
/// Access the `text` style mutably.
pub fn text_mut(&mut self) -> &mut TextStyle {
Rc::make_mut(&mut self.text)
}
/// The resolved line spacing.
pub fn leading(&self) -> Length {
self.par.leading.resolve(self.text.size)
}
/// The resolved paragraph spacing.
pub fn par_spacing(&self) -> Length {
self.par.spacing.resolve(self.text.size)
}
}
impl Default for Style {
fn default() -> Self {
Self {
page: Rc::new(PageStyle::default()),
par: Rc::new(ParStyle::default()),
text: Rc::new(TextStyle::default()),
}
}
}
/// Defines style properties of pages.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct PageStyle {
/// The class of this page.
pub class: PaperClass,
/// The width and height of the page.
pub size: Size,
/// The amount of white space on each side of the page. If a side is set to
/// `None`, the default for the paper class is used.
pub margins: Sides<Smart<Linear>>,
/// The background fill of the page.
pub fill: Option<Paint>,
}
impl PageStyle {
/// The resolved margins.
pub fn margins(&self) -> Sides<Linear> {
let default = self.class.default_margins();
Sides {
left: self.margins.left.unwrap_or(default.left),
top: self.margins.top.unwrap_or(default.top),
right: self.margins.right.unwrap_or(default.right),
bottom: self.margins.bottom.unwrap_or(default.bottom),
}
}
}
impl Default for PageStyle {
fn default() -> Self {
let paper = Paper::A4;
Self {
class: paper.class(),
size: paper.size(),
margins: Sides::splat(Smart::Auto),
fill: None,
}
}
}
/// Defines style properties of paragraphs.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ParStyle {
/// The direction for text and inline objects.
pub dir: Dir,
/// How to align text and inline objects in their line.
pub align: Align,
/// The spacing between lines (dependent on scaled font size).
pub leading: Linear,
/// The spacing between paragraphs (dependent on scaled font size).
pub spacing: Linear,
}
impl Default for ParStyle {
fn default() -> Self {
Self {
dir: Dir::LTR,
align: Align::Left,
leading: Relative::new(0.65).into(),
spacing: Relative::new(1.2).into(),
}
}
}
/// Defines style properties of text.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct TextStyle {
/// The font size.
pub size: Length,
/// The selected font variant (the final variant also depends on `strong`
/// and `emph`).
pub variant: FontVariant,
/// The top end of the text bounding box.
pub top_edge: VerticalFontMetric,
/// The bottom end of the text bounding box.
pub bottom_edge: VerticalFontMetric,
/// Glyph color.
pub fill: Paint,
/// A list of font families with generic class definitions (the final
/// family list also depends on `monospace`).
pub families: Rc<FamilyStyle>,
/// OpenType features.
pub features: Rc<FontFeatures>,
/// The amount of space that should be added between character.
pub tracking: Em,
/// Whether 300 extra font weight should be added to what is defined by the
/// `variant`.
pub strong: bool,
/// Whether the the font style defined by the `variant` should be inverted.
pub emph: bool,
/// Whether a monospace font should be preferred.
pub monospace: bool,
/// Whether font fallback to a base list should occur.
pub fallback: bool,
}
impl TextStyle {
/// The resolved variant with `strong` and `emph` factored in.
pub fn variant(&self) -> FontVariant {
let mut variant = self.variant;
if self.strong {
variant.weight = variant.weight.thicken(300);
}
if self.emph {
variant.style = match variant.style {
FontStyle::Normal => FontStyle::Italic,
FontStyle::Italic => FontStyle::Normal,
FontStyle::Oblique => FontStyle::Normal,
}
}
variant
}
/// The resolved family iterator.
pub fn families(&self) -> impl Iterator<Item = &str> + Clone {
let head = if self.monospace {
self.families.monospace.as_slice()
} else {
&[]
};
let core = self.families.list.iter().flat_map(move |family| {
match family {
FontFamily::Named(name) => std::slice::from_ref(name),
FontFamily::Serif => &self.families.serif,
FontFamily::SansSerif => &self.families.sans_serif,
FontFamily::Monospace => &self.families.monospace,
}
});
let tail = if self.fallback {
self.families.base.as_slice()
} else {
&[]
};
head.iter().chain(core).chain(tail).map(EcoString::as_str)
}
/// Access the `families` style mutably.
pub fn families_mut(&mut self) -> &mut FamilyStyle {
Rc::make_mut(&mut self.families)
}
/// Access the font `features` mutably.
pub fn features_mut(&mut self) -> &mut FontFeatures {
Rc::make_mut(&mut self.features)
}
}
impl Default for TextStyle {
fn default() -> Self {
Self {
size: Length::pt(11.0),
variant: FontVariant {
style: FontStyle::Normal,
weight: FontWeight::REGULAR,
stretch: FontStretch::NORMAL,
},
top_edge: VerticalFontMetric::CapHeight,
bottom_edge: VerticalFontMetric::Baseline,
fill: RgbaColor::BLACK.into(),
families: Rc::new(FamilyStyle::default()),
features: Rc::new(FontFeatures::default()),
tracking: Em::zero(),
strong: false,
emph: false,
monospace: false,
fallback: true,
}
}
}
/// Font list with family definitions.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct FamilyStyle {
/// The user-defined list of font families.
pub list: Vec<FontFamily>,
/// Definition of serif font families.
pub serif: Vec<EcoString>,
/// Definition of sans-serif font families.
pub sans_serif: Vec<EcoString>,
/// Definition of monospace font families used for raw text.
pub monospace: Vec<EcoString>,
/// Base fonts that are tried as last resort.
pub base: Vec<EcoString>,
}
impl Default for FamilyStyle {
fn default() -> Self {
Self {
list: vec![FontFamily::SansSerif],
serif: vec!["ibm plex serif".into()],
sans_serif: vec!["ibm plex sans".into()],
monospace: vec!["ibm plex mono".into()],
base: vec![
"ibm plex sans".into(),
"latin modern math".into(),
"twitter color emoji".into(),
],
}
}
}
/// 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 family with a name.
Named(EcoString),
}
impl Debug for FontFamily {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self {
Self::Serif => "serif",
Self::SansSerif => "sans-serif",
Self::Monospace => "monospace",
Self::Named(s) => s,
})
}
}
/// Whether various kinds of ligatures should appear.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct FontFeatures {
/// Whether to apply kerning ("kern").
pub kerning: bool,
/// Whether the text should use small caps. ("smcp")
pub smallcaps: bool,
/// Whether to apply stylistic alternates. ("salt")
pub alternates: bool,
/// Which stylistic set to apply. ("ss01" - "ss20")
pub stylistic_set: Option<StylisticSet>,
/// Configuration of ligature features.
pub ligatures: LigatureFeatures,
/// Configuration of numbers features.
pub numbers: NumberFeatures,
/// Raw OpenType features to apply.
pub raw: Vec<(Tag, u32)>,
}
impl Default for FontFeatures {
fn default() -> Self {
Self {
kerning: true,
smallcaps: false,
alternates: false,
stylistic_set: None,
ligatures: LigatureFeatures::default(),
numbers: NumberFeatures::default(),
raw: vec![],
}
}
}
/// 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
}
}
/// Whether various kinds of ligatures should appear.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct LigatureFeatures {
/// Standard ligatures. ("liga", "clig")
pub standard: bool,
/// Ligatures that should be used sparringly. ("dlig")
pub discretionary: bool,
/// Historical ligatures. ("hlig")
pub historical: bool,
}
impl Default for LigatureFeatures {
fn default() -> Self {
Self {
standard: true,
discretionary: false,
historical: false,
}
}
}
/// Defines the style of numbers.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct NumberFeatures {
/// Whether to use lining or old-style numbers.
pub type_: Smart<NumberType>,
/// Whether to use proportional or tabular numbers.
pub width: Smart<NumberWidth>,
/// How to position numbers vertically.
pub position: NumberPosition,
/// Whether to have a slash through the zero glyph. ("zero")
pub slashed_zero: bool,
/// Whether to convert fractions. ("frac")
pub fractions: bool,
}
impl Default for NumberFeatures {
fn default() -> Self {
Self {
type_: Smart::Auto,
width: Smart::Auto,
position: NumberPosition::Normal,
slashed_zero: false,
fractions: false,
}
}
}
/// 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,
}
/// 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,
}
/// 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,
}

View File

@ -1,233 +0,0 @@
use crate::geom::{Length, Linear, Relative, Sides, Size};
/// Specification of a paper.
#[derive(Debug, Copy, Clone)]
pub struct Paper {
/// The broad class this paper belongs to.
class: PaperClass,
/// The width of the paper in millimeters.
width: f64,
/// The height of the paper in millimeters.
height: f64,
}
/// Defines default margins for a class of related papers.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum PaperClass {
Custom,
Base,
US,
Newspaper,
Book,
}
impl PaperClass {
/// The default margins for this page class.
pub fn default_margins(self) -> Sides<Linear> {
let f = |r| Relative::new(r).into();
let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b));
match self {
Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842),
Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842),
Self::US => s(0.1760, 0.1092, 0.1760, 0.0910),
Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294),
Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965),
}
}
}
macro_rules! papers {
($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => {
impl Paper {
/// Parse a paper from its name.
///
/// Both lower and upper case are fine.
pub fn from_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
$($($pats)* => Some(Self::$var),)*
_ => None,
}
}
/// The class of the paper.
pub fn class(self) -> PaperClass {
self.class
}
/// The size of the paper.
pub fn size(self) -> Size {
Size::new(Length::mm(self.width), Length::mm(self.height))
}
}
/// Predefined papers.
///
/// Each paper is parsable from its name in kebab-case.
impl Paper {
$(papers!(@$var, stringify!($($pats)*), $class, $width, $height);)*
}
};
(@$var:ident, $names:expr, $class:ident, $width:expr, $height:expr) => {
pub const $var: Self = Self {
class: PaperClass::$class,
width: $width,
height: $height,
};
};
}
// All paper sizes in mm.
//
// Resources:
// - https://papersizes.io/
// - https://en.wikipedia.org/wiki/Paper_size
// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
papers! {
// ---------------------------------------------------------------------- //
// ISO 216 A Series
(A0: Base, 841.0, 1189.0, "a0")
(A1: Base, 594.0, 841.0, "a1")
(A2: Base, 420.0, 594.0, "a2")
(A3: Base, 297.0, 420.0, "a3")
(A4: Base, 210.0, 297.0, "a4")
(A5: Base, 148.0, 210.0, "a5")
(A6: Book, 105.0, 148.0, "a6")
(A7: Base, 74.0, 105.0, "a7")
(A8: Base, 52.0, 74.0, "a8")
(A9: Base, 37.0, 52.0, "a9")
(A10: Base, 26.0, 37.0, "a10")
(A11: Base, 18.0, 26.0, "a11")
// ISO 216 B Series
(ISO_B1: Base, 707.0, 1000.0, "iso-b1")
(ISO_B2: Base, 500.0, 707.0, "iso-b2")
(ISO_B3: Base, 353.0, 500.0, "iso-b3")
(ISO_B4: Base, 250.0, 353.0, "iso-b4")
(ISO_B5: Book, 176.0, 250.0, "iso-b5")
(ISO_B6: Book, 125.0, 176.0, "iso-b6")
(ISO_B7: Base, 88.0, 125.0, "iso-b7")
(ISO_B8: Base, 62.0, 88.0, "iso-b8")
// ISO 216 C Series
(ISO_C3: Base, 324.0, 458.0, "iso-c3")
(ISO_C4: Base, 229.0, 324.0, "iso-c4")
(ISO_C5: Base, 162.0, 229.0, "iso-c5")
(ISO_C6: Base, 114.0, 162.0, "iso-c6")
(ISO_C7: Base, 81.0, 114.0, "iso-c7")
(ISO_C8: Base, 57.0, 81.0, "iso-c8")
// DIN D Series (extension to ISO)
(DIN_D3: Base, 272.0, 385.0, "din-d3")
(DIN_D4: Base, 192.0, 272.0, "din-d4")
(DIN_D5: Base, 136.0, 192.0, "din-d5")
(DIN_D6: Base, 96.0, 136.0, "din-d6")
(DIN_D7: Base, 68.0, 96.0, "din-d7")
(DIN_D8: Base, 48.0, 68.0, "din-d8")
// SIS (used in academia)
(SIS_G5: Base, 169.0, 239.0, "sis-g5")
(SIS_E5: Base, 115.0, 220.0, "sis-e5")
// ANSI Extensions
(ANSI_A: Base, 216.0, 279.0, "ansi-a")
(ANSI_B: Base, 279.0, 432.0, "ansi-b")
(ANSI_C: Base, 432.0, 559.0, "ansi-c")
(ANSI_D: Base, 559.0, 864.0, "ansi-d")
(ANSI_E: Base, 864.0, 1118.0, "ansi-e")
// ANSI Architectural Paper
(ARCH_A: Base, 229.0, 305.0, "arch-a")
(ARCH_B: Base, 305.0, 457.0, "arch-b")
(ARCH_C: Base, 457.0, 610.0, "arch-c")
(ARCH_D: Base, 610.0, 914.0, "arch-d")
(ARCH_E1: Base, 762.0, 1067.0, "arch-e1")
(ARCH_E: Base, 914.0, 1219.0, "arch-e")
// JIS B Series
(JIS_B0: Base, 1030.0, 1456.0, "jis-b0")
(JIS_B1: Base, 728.0, 1030.0, "jis-b1")
(JIS_B2: Base, 515.0, 728.0, "jis-b2")
(JIS_B3: Base, 364.0, 515.0, "jis-b3")
(JIS_B4: Base, 257.0, 364.0, "jis-b4")
(JIS_B5: Base, 182.0, 257.0, "jis-b5")
(JIS_B6: Base, 128.0, 182.0, "jis-b6")
(JIS_B7: Base, 91.0, 128.0, "jis-b7")
(JIS_B8: Base, 64.0, 91.0, "jis-b8")
(JIS_B9: Base, 45.0, 64.0, "jis-b9")
(JIS_B10: Base, 32.0, 45.0, "jis-b10")
(JIS_B11: Base, 22.0, 32.0, "jis-b11")
// SAC D Series
(SAC_D0: Base, 764.0, 1064.0, "sac-d0")
(SAC_D1: Base, 532.0, 760.0, "sac-d1")
(SAC_D2: Base, 380.0, 528.0, "sac-d2")
(SAC_D3: Base, 264.0, 376.0, "sac-d3")
(SAC_D4: Base, 188.0, 260.0, "sac-d4")
(SAC_D5: Base, 130.0, 184.0, "sac-d5")
(SAC_D6: Base, 92.0, 126.0, "sac-d6")
// ISO 7810 ID
(ISO_ID_1: Base, 85.6, 53.98, "iso-id-1")
(ISO_ID_2: Base, 74.0, 105.0, "iso-id-2")
(ISO_ID_3: Base, 88.0, 125.0, "iso-id-3")
// ---------------------------------------------------------------------- //
// Asia
(ASIA_F4: Base, 210.0, 330.0, "asia-f4")
// Japan
(JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4")
(JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5")
(JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6")
(JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4")
(JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5")
(JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card")
// China
(CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card")
// Europe
(EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card")
// French Traditional (AFNOR)
(FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière")
(FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture")
(FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition")
(FR_RAISIN: Base, 500.0, 650.0, "fr-raisin")
(FR_CARRE: Base, 450.0, 560.0, "fr-carré")
(FR_JESUS: Base, 560.0, 760.0, "fr-jésus")
// United Kingdom Imperial
(UK_BRIEF: Base, 406.4, 342.9, "uk-brief")
(UK_DRAFT: Base, 254.0, 406.4, "uk-draft")
(UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap")
(UK_QUARTO: Base, 203.2, 254.0, "uk-quarto")
(UK_CROWN: Base, 508.0, 381.0, "uk-crown")
(UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a")
(UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b")
// Unites States
(US_LETTER: US, 215.9, 279.4, "us-letter")
(US_LEGAL: US, 215.9, 355.6, "us-legal")
(US_TABLOID: US, 279.4, 431.8, "us-tabloid")
(US_EXECUTIVE: US, 184.15, 266.7, "us-executive")
(US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio")
(US_STATEMENT: US, 139.7, 215.9, "us-statement")
(US_LEDGER: US, 431.8, 279.4, "us-ledger")
(US_OFICIO: US, 215.9, 340.36, "us-oficio")
(US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter")
(US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal")
(US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card")
(US_DIGEST: Book, 139.7, 215.9, "us-digest")
(US_TRADE: Book, 152.4, 228.6, "us-trade")
// ---------------------------------------------------------------------- //
// Other
(NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact")
(NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner")
(NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet")
(PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9")
(PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3")
}

View File

@ -13,18 +13,6 @@ use std::ops::Range;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
/// Additional methods for booleans.
pub trait BoolExt {
/// Toggle the value of the bool in place.
fn flip(&mut self);
}
impl BoolExt for bool {
fn flip(&mut self) {
*self = !*self;
}
}
/// Additional methods for options. /// Additional methods for options.
pub trait OptionExt<T> { pub trait OptionExt<T> {
/// Sets `other` as the value if `self` is `None` or if it contains a value /// Sets `other` as the value if `self` is `None` or if it contains a value

View File

@ -12,20 +12,17 @@ use usvg::FitTo;
use walkdir::WalkDir; use walkdir::WalkDir;
use typst::diag::Error; use typst::diag::Error;
use typst::eval::{Smart, Value}; use typst::eval::{Smart, Styles, Value};
use typst::font::Face; use typst::font::Face;
use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
use typst::geom::{ use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Size, Transform};
self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size, Transform,
};
use typst::image::{Image, RasterImage, Svg}; use typst::image::{Image, RasterImage, Svg};
use typst::layout::layout; use typst::layout::layout;
#[cfg(feature = "layout-cache")] #[cfg(feature = "layout-cache")]
use typst::library::DocumentNode; use typst::library::{DocumentNode, PageNode, 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;
use typst::style::Style;
use typst::syntax::Span; use typst::syntax::Span;
use typst::Context; use typst::Context;
@ -64,12 +61,17 @@ fn main() {
println!("Running {} tests", len); println!("Running {} tests", len);
} }
// We want to have "unbounded" pages, so we allow them to be infinitely // Set page width to 120pt with 10pt margins, so that the inner page is
// large and fit them to match their content. // exactly 100pt wide. Page height is unbounded and font size is 10pt so
let mut style = Style::default(); // that it multiplies to nice round numbers.
style.page_mut().size = Size::new(Length::pt(120.0), Length::inf()); let mut styles = Styles::new();
style.page_mut().margins = Sides::splat(Smart::Custom(Length::pt(10.0).into())); styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0)));
style.text_mut().size = Length::pt(10.0); styles.set(PageNode::HEIGHT, Smart::Auto);
styles.set(PageNode::LEFT, Smart::Custom(Length::pt(10.0).into()));
styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into()));
styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into()));
styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into()));
styles.set(TextNode::SIZE, Length::pt(10.0));
// Hook up an assert function into the global scope. // Hook up an assert function into the global scope.
let mut std = typst::library::new(); let mut std = typst::library::new();
@ -87,7 +89,7 @@ fn main() {
// Create loader and context. // Create loader and context.
let loader = FsLoader::new().with_path(FONT_DIR).wrap(); let loader = FsLoader::new().with_path(FONT_DIR).wrap();
let mut ctx = Context::builder().std(std).style(style).build(loader); let mut ctx = Context::builder().std(std).styles(styles).build(loader);
// Run all the tests. // Run all the tests.
let mut ok = true; let mut ok = true;