Int, Float, Relative and Linear values 🍉

This commit is contained in:
Laurenz 2020-10-03 15:07:57 +02:00
parent 0fc25d732d
commit 95bae5725c
17 changed files with 461 additions and 262 deletions

View File

@ -6,8 +6,8 @@ use std::ops::Index;
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
/// A dictionary data structure, which maps from integers (`u64`) or strings to /// A dictionary data structure, which maps from integers and strings to a
/// a generic value type. /// generic value type.
/// ///
/// The dictionary can be used to model arrays by assigning values to successive /// The dictionary can be used to model arrays by assigning values to successive
/// indices from `0..n`. The `push` method offers special support for this /// indices from `0..n`. The `push` method offers special support for this
@ -293,32 +293,40 @@ impl<'a> From<&'a str> for RefKey<'a> {
} }
} }
/// A dictionary entry which tracks key and value span. /// A dictionary entry which combines key span and value.
///
/// This exists because a key in a directory can't track its span by itself.
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct SpannedEntry<V> { pub struct SpannedEntry<V> {
pub key: Span, pub key_span: Span,
pub val: Spanned<V>, pub value: Spanned<V>,
} }
impl<V> SpannedEntry<V> { impl<V> SpannedEntry<V> {
/// Create a new entry. /// Create a new entry.
pub fn new(key: Span, val: Spanned<V>) -> Self { pub fn new(key: Span, val: Spanned<V>) -> Self {
Self { key, val } Self { key_span: key, value: val }
} }
/// Create an entry with the same span for key and value. /// Create an entry with the same span for key and value.
pub fn val(val: Spanned<V>) -> Self { pub fn val(val: Spanned<V>) -> Self {
Self { key: val.span, val } Self { key_span: val.span, value: val }
} }
/// Convert from `&SpannedEntry<T>` to `SpannedEntry<&T>` /// Convert from `&SpannedEntry<T>` to `SpannedEntry<&T>`
pub fn as_ref(&self) -> SpannedEntry<&V> { pub fn as_ref(&self) -> SpannedEntry<&V> {
SpannedEntry { key: self.key, val: self.val.as_ref() } SpannedEntry {
key_span: self.key_span,
value: self.value.as_ref(),
}
} }
/// Map the entry to a different value type. /// Map the entry to a different value type.
pub fn map<U>(self, f: impl FnOnce(V) -> U) -> SpannedEntry<U> { pub fn map<U>(self, f: impl FnOnce(V) -> U) -> SpannedEntry<U> {
SpannedEntry { key: self.key, val: self.val.map(f) } SpannedEntry {
key_span: self.key_span,
value: self.value.map(f),
}
} }
} }
@ -326,10 +334,10 @@ impl<V: Debug> Debug for SpannedEntry<V> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() { if f.alternate() {
f.write_str("key")?; f.write_str("key")?;
self.key.fmt(f)?; self.key_span.fmt(f)?;
f.write_str(" ")?; f.write_str(" ")?;
} }
self.val.fmt(f) self.value.fmt(f)
} }
} }

View File

@ -8,8 +8,8 @@ use fontdock::{FontStretch, FontStyle, FontWeight};
use super::dict::{Dict, SpannedEntry}; use super::dict::{Dict, SpannedEntry};
use crate::color::RgbaColor; use crate::color::RgbaColor;
use crate::geom::Linear;
use crate::layout::{Command, Commands, Dir, LayoutContext, SpecAlign}; use crate::layout::{Command, Commands, Dir, LayoutContext, SpecAlign};
use crate::length::{Length, ScaleLength};
use crate::paper::Paper; use crate::paper::Paper;
use crate::syntax::{Ident, Span, SpanWith, Spanned, SynNode, SynTree}; use crate::syntax::{Ident, Span, SpanWith, Spanned, SynNode, SynTree};
use crate::{DynFuture, Feedback, Pass}; use crate::{DynFuture, Feedback, Pass};
@ -21,10 +21,21 @@ pub enum Value {
Ident(Ident), Ident(Ident),
/// A boolean: `true, false`. /// A boolean: `true, false`.
Bool(bool), Bool(bool),
/// A number: `1.2, 200%`. /// An integer: `120`.
Number(f64), Int(i64),
/// A floating-point number: `1.2, 200%`.
Float(f64),
/// A length: `2cm, 5.2in`. /// A length: `2cm, 5.2in`.
Length(Length), Length(f64),
/// A relative value: `50%`.
///
/// Note: `50%` is represented as `0.5` here, but as `50.0` in the
/// corresponding [literal].
///
/// [literal]: ../syntax/ast/enum.Lit.html#variant.Percent
Relative(f64),
/// A combination of an absolute length and a relative value.
Linear(Linear),
/// A color value with alpha channel: `#f79143ff`. /// A color value with alpha channel: `#f79143ff`.
Color(RgbaColor), Color(RgbaColor),
/// A string: `"string"`. /// A string: `"string"`.
@ -40,14 +51,16 @@ pub enum Value {
} }
impl Value { impl Value {
/// A natural-language name of the type of this expression, e.g. /// The natural-language name of this value for use in error messages.
/// "identifier".
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { match self {
Self::Ident(_) => "identifier", Self::Ident(_) => "identifier",
Self::Bool(_) => "bool", Self::Bool(_) => "bool",
Self::Number(_) => "number", Self::Int(_) => "integer",
Self::Float(_) => "float",
Self::Relative(_) => "relative",
Self::Length(_) => "length", Self::Length(_) => "length",
Self::Linear(_) => "linear",
Self::Color(_) => "color", Self::Color(_) => "color",
Self::Str(_) => "string", Self::Str(_) => "string",
Self::Dict(_) => "dict", Self::Dict(_) => "dict",
@ -71,13 +84,13 @@ impl Spanned<Value> {
let mut end = None; let mut end = None;
for entry in dict.into_values() { for entry in dict.into_values() {
if let Some(last_end) = end { if let Some(last_end) = end {
let span = Span::new(last_end, entry.key.start); let span = Span::new(last_end, entry.key_span.start);
let tree = vec![SynNode::Space.span_with(span)]; let tree = vec![SynNode::Space.span_with(span)];
commands.push(Command::LayoutSyntaxTree(tree)); commands.push(Command::LayoutSyntaxTree(tree));
} }
end = Some(entry.val.span.end); end = Some(entry.value.span.end);
commands.extend(entry.val.into_commands()); commands.extend(entry.value.into_commands());
} }
commands commands
} }
@ -100,11 +113,14 @@ impl Debug for Value {
match self { match self {
Self::Ident(i) => i.fmt(f), Self::Ident(i) => i.fmt(f),
Self::Bool(b) => b.fmt(f), Self::Bool(b) => b.fmt(f),
Self::Number(n) => n.fmt(f), Self::Int(i) => i.fmt(f),
Self::Length(s) => s.fmt(f), Self::Float(n) => n.fmt(f),
Self::Length(l) => l.fmt(f),
Self::Relative(r) => r.fmt(f),
Self::Linear(l) => l.fmt(f),
Self::Color(c) => c.fmt(f), Self::Color(c) => c.fmt(f),
Self::Str(s) => s.fmt(f), Self::Str(s) => s.fmt(f),
Self::Dict(t) => t.fmt(f), Self::Dict(d) => d.fmt(f),
Self::Tree(t) => t.fmt(f), Self::Tree(t) => t.fmt(f),
Self::Func(c) => c.fmt(f), Self::Func(c) => c.fmt(f),
Self::Commands(c) => c.fmt(f), Self::Commands(c) => c.fmt(f),
@ -117,18 +133,19 @@ impl Debug for Value {
/// The first argument is a dictionary containing the arguments passed to the /// The first argument is a dictionary containing the arguments passed to the
/// function. The function may be asynchronous (as such it returns a dynamic /// function. The function may be asynchronous (as such it returns a dynamic
/// future) and it may emit diagnostics, which are contained in the returned /// future) and it may emit diagnostics, which are contained in the returned
/// `Pass`. In the end, the function must evaluate to `Value`. Your typical /// `Pass`. In the end, the function must evaluate to [`Value`]. A typical
/// typesetting function will return a `Commands` value which will instruct the /// typesetting function will return a `Commands` value which will instruct the
/// layouting engine to do what the function pleases. /// layouting engine to do what the function pleases.
/// ///
/// The dynamic function object is wrapped in an `Rc` to keep `Value` clonable. /// The dynamic function object is wrapped in an `Rc` to keep [`Value`]
/// clonable.
///
/// [`Value`]: enum.Value.html
#[derive(Clone)] #[derive(Clone)]
pub struct FuncValue(pub Rc<FuncType>); pub struct FuncValue(pub Rc<FuncType>);
/// The dynamic function type backtick [`FuncValue`]. /// The signature of executable functions.
/// type FuncType = dyn Fn(Span, DictValue, LayoutContext<'_>) -> DynFuture<Pass<Value>>;
/// [`FuncValue`]: struct.FuncValue.html
pub type FuncType = dyn Fn(Span, DictValue, LayoutContext<'_>) -> DynFuture<Pass<Value>>;
impl FuncValue { impl FuncValue {
/// Create a new function value from a rust function or closure. /// Create a new function value from a rust function or closure.
@ -175,7 +192,7 @@ impl DictValue {
/// skipping and ignoring all non-matching entries with lower keys. /// skipping and ignoring all non-matching entries with lower keys.
pub fn take<T: TryFromValue>(&mut self) -> Option<T> { pub fn take<T: TryFromValue>(&mut self) -> Option<T> {
for (&key, entry) in self.nums() { for (&key, entry) in self.nums() {
let expr = entry.val.as_ref(); let expr = entry.value.as_ref();
if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
self.remove(key); self.remove(key);
return Some(val); return Some(val);
@ -197,7 +214,7 @@ impl DictValue {
) -> Option<T> { ) -> Option<T> {
while let Some((num, _)) = self.first() { while let Some((num, _)) = self.first() {
let entry = self.remove(num).unwrap(); let entry = self.remove(num).unwrap();
if let Some(val) = T::try_from_value(entry.val.as_ref(), f) { if let Some(val) = T::try_from_value(entry.value.as_ref(), f) {
return Some(val); return Some(val);
} }
} }
@ -214,7 +231,7 @@ impl DictValue {
T: TryFromValue, T: TryFromValue,
{ {
self.remove(key).and_then(|entry| { self.remove(key).and_then(|entry| {
let expr = entry.val.as_ref(); let expr = entry.value.as_ref();
T::try_from_value(expr, f) T::try_from_value(expr, f)
}) })
} }
@ -230,7 +247,7 @@ impl DictValue {
let mut skip = 0; let mut skip = 0;
std::iter::from_fn(move || { std::iter::from_fn(move || {
for (&key, entry) in self.nums().skip(skip) { for (&key, entry) in self.nums().skip(skip) {
let expr = entry.val.as_ref(); let expr = entry.value.as_ref();
if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
self.remove(key); self.remove(key);
return Some((key, val)); return Some((key, val));
@ -265,7 +282,7 @@ impl DictValue {
let mut skip = 0; let mut skip = 0;
std::iter::from_fn(move || { std::iter::from_fn(move || {
for (key, entry) in self.strs().skip(skip) { for (key, entry) in self.strs().skip(skip) {
let expr = entry.val.as_ref(); let expr = entry.value.as_ref();
if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
let key = key.clone(); let key = key.clone();
self.remove(&key); self.remove(&key);
@ -281,7 +298,7 @@ impl DictValue {
/// Generated `"unexpected argument"` errors for all remaining entries. /// Generated `"unexpected argument"` errors for all remaining entries.
pub fn unexpected(&self, f: &mut Feedback) { pub fn unexpected(&self, f: &mut Feedback) {
for entry in self.values() { for entry in self.values() {
error!(@f, entry.key.join(entry.val.span), "unexpected argument"); error!(@f, entry.key_span.join(entry.value.span), "unexpected argument");
} }
} }
} }
@ -351,20 +368,50 @@ impl<T: TryFromValue> TryFromValue for Spanned<T> {
impl_match!(Value, "value", v => v.clone()); impl_match!(Value, "value", v => v.clone());
impl_match!(Ident, "identifier", Value::Ident(i) => i.clone()); impl_match!(Ident, "identifier", Value::Ident(i) => i.clone());
impl_match!(String, "string", Value::Str(s) => s.clone());
impl_match!(bool, "bool", &Value::Bool(b) => b); impl_match!(bool, "bool", &Value::Bool(b) => b);
impl_match!(f64, "number", &Value::Number(n) => n); impl_match!(i64, "integer", &Value::Int(i) => i);
impl_match!(Length, "length", &Value::Length(l) => l); impl_match!(f64, "float",
&Value::Int(i) => i as f64,
&Value::Float(f) => f,
);
impl_match!(Abs, "length", &Value::Length(l) => Abs(l));
impl_match!(Rel, "relative", &Value::Relative(r) => Rel(r));
impl_match!(Linear, "linear",
&Value::Linear(l) => l,
&Value::Length(l) => Linear::abs(l),
&Value::Relative(r) => Linear::rel(r),
);
impl_match!(String, "string", Value::Str(s) => s.clone());
impl_match!(SynTree, "tree", Value::Tree(t) => t.clone()); impl_match!(SynTree, "tree", Value::Tree(t) => t.clone());
impl_match!(DictValue, "dict", Value::Dict(t) => t.clone()); impl_match!(DictValue, "dict", Value::Dict(t) => t.clone());
impl_match!(FuncValue, "function", Value::Func(f) => f.clone()); impl_match!(FuncValue, "function", Value::Func(f) => f.clone());
impl_match!(ScaleLength, "number or length",
&Value::Length(length) => ScaleLength::Absolute(length),
&Value::Number(scale) => ScaleLength::Scaled(scale),
);
/// A value type that matches identifiers and strings and implements /// A value type that matches [length] values.
/// `Into<String>`. ///
/// [length]: enum.Value.html#variant.Length
pub struct Abs(pub f64);
impl From<Abs> for f64 {
fn from(abs: Abs) -> f64 {
abs.0
}
}
/// A value type that matches [relative] values.
///
/// [relative]: enum.Value.html#variant.Relative
pub struct Rel(pub f64);
impl From<Rel> for f64 {
fn from(rel: Rel) -> f64 {
rel.0
}
}
/// A value type that matches [identifier] and [string] values.
///
/// [identifier]: enum.Value.html#variant.Ident
/// [string]: enum.Value.html#variant.Str
pub struct StringLike(pub String); pub struct StringLike(pub String);
impl From<StringLike> for String { impl From<StringLike> for String {
@ -410,19 +457,19 @@ impl_ident!(Paper, "paper", Self::from_name);
impl TryFromValue for FontWeight { impl TryFromValue for FontWeight {
fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> { fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
match value.v { match value.v {
&Value::Number(weight) => { &Value::Int(weight) => {
const MIN: u16 = 100; const MIN: i64 = 100;
const MAX: u16 = 900; const MAX: i64 = 900;
let weight = if weight < MIN {
if weight < MIN as f64 {
error!(@f, value.span, "the minimum font weight is {}", MIN); error!(@f, value.span, "the minimum font weight is {}", MIN);
Some(Self::THIN) MIN
} else if weight > MAX as f64 { } else if weight > MAX {
error!(@f, value.span, "the maximum font weight is {}", MAX); error!(@f, value.span, "the maximum font weight is {}", MAX);
Some(Self::BLACK) MAX
} else { } else {
FontWeight::from_number(weight.round() as u16) weight
} };
Self::from_number(weight as u16)
} }
Value::Ident(ident) => { Value::Ident(ident) => {
let weight = Self::from_str(ident); let weight = Self::from_str(ident);
@ -434,7 +481,7 @@ impl TryFromValue for FontWeight {
other => { other => {
error!( error!(
@f, value.span, @f, value.span,
"expected font weight (name or number), found {}", "expected font weight (name or integer), found {}",
other.name(), other.name(),
); );
None None
@ -490,7 +537,7 @@ mod tests {
assert_eq!(dict.take_key::<f64>("hi", &mut f), None); assert_eq!(dict.take_key::<f64>("hi", &mut f), None);
assert_eq!(f.diagnostics, [error!( assert_eq!(f.diagnostics, [error!(
Span::ZERO, Span::ZERO,
"expected number, found bool" "expected float, found bool"
)]); )]);
assert!(dict.is_empty()); assert!(dict.is_empty());
} }
@ -499,13 +546,13 @@ mod tests {
fn test_dict_take_all_removes_the_correct_entries() { fn test_dict_take_all_removes_the_correct_entries() {
let mut dict = Dict::new(); let mut dict = Dict::new();
dict.insert(1, entry(Value::Bool(false))); dict.insert(1, entry(Value::Bool(false)));
dict.insert(3, entry(Value::Number(0.0))); dict.insert(3, entry(Value::Float(0.0)));
dict.insert(7, entry(Value::Bool(true))); dict.insert(7, entry(Value::Bool(true)));
assert_eq!(dict.take_all_num::<bool>().collect::<Vec<_>>(), [ assert_eq!(dict.take_all_num::<bool>().collect::<Vec<_>>(), [
(1, false), (1, false),
(7, true) (7, true)
],); ],);
assert_eq!(dict.len(), 1); assert_eq!(dict.len(), 1);
assert_eq!(dict[3].val.v, Value::Number(0.0)); assert_eq!(dict[3].value.v, Value::Float(0.0));
} }
} }

View File

@ -3,6 +3,9 @@
#[doc(no_inline)] #[doc(no_inline)]
pub use kurbo::*; pub use kurbo::*;
use std::fmt::{self, Debug, Formatter};
use std::ops::*;
use crate::layout::primitive::{Dir, GenAlign, LayoutAlign, LayoutSystem, SpecAxis}; use crate::layout::primitive::{Dir, GenAlign, LayoutAlign, LayoutSystem, SpecAxis};
/// Additional methods for [sizes]. /// Additional methods for [sizes].
@ -176,3 +179,128 @@ impl<T> Sides<T> {
} }
} }
} }
/// A function that depends linearly on one value.
///
/// This represents a function `f(x) = rel * x + abs`.
#[derive(Copy, Clone, PartialEq)]
pub struct Linear {
/// The relative part.
pub rel: f64,
/// The absolute part.
pub abs: f64,
}
impl Linear {
/// The constant zero function.
pub const ZERO: Linear = Linear { rel: 0.0, abs: 0.0 };
/// Create a new linear function.
pub fn new(rel: f64, abs: f64) -> Self {
Self { rel, abs }
}
/// Create a new linear function with only a relative component.
pub fn rel(rel: f64) -> Self {
Self { rel, abs: 0.0 }
}
/// Create a new linear function with only an absolute component.
pub fn abs(abs: f64) -> Self {
Self { rel: 0.0, abs }
}
/// Evaluate the linear function with the given value.
pub fn eval(self, x: f64) -> f64 {
self.rel * x + self.abs
}
}
impl Add for Linear {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
rel: self.rel + other.rel,
abs: self.abs + other.abs,
}
}
}
impl AddAssign for Linear {
fn add_assign(&mut self, other: Self) {
self.rel += other.rel;
self.abs += other.abs;
}
}
impl Sub for Linear {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
rel: self.rel - other.rel,
abs: self.abs - other.abs,
}
}
}
impl SubAssign for Linear {
fn sub_assign(&mut self, other: Self) {
self.rel -= other.rel;
self.abs -= other.abs;
}
}
impl Mul<f64> for Linear {
type Output = Self;
fn mul(self, other: f64) -> Self {
Self {
rel: self.rel + other,
abs: self.abs + other,
}
}
}
impl MulAssign<f64> for Linear {
fn mul_assign(&mut self, other: f64) {
self.rel *= other;
self.abs *= other;
}
}
impl Mul<Linear> for f64 {
type Output = Linear;
fn mul(self, other: Linear) -> Linear {
Linear {
rel: self + other.rel,
abs: self + other.abs,
}
}
}
impl Div<f64> for Linear {
type Output = Self;
fn div(self, other: f64) -> Self {
Self {
rel: self.rel / other,
abs: self.abs / other,
}
}
}
impl DivAssign<f64> for Linear {
fn div_assign(&mut self, other: f64) {
self.rel /= other;
self.abs /= other;
}
}
impl Debug for Linear {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}x + {}", self.rel, self.abs)
}
}

View File

@ -115,7 +115,9 @@ impl<'a> TreeLayouter<'a> {
async fn layout_heading(&mut self, heading: &NodeHeading) { async fn layout_heading(&mut self, heading: &NodeHeading) {
let style = self.style.text.clone(); let style = self.style.text.clone();
self.style.text.font_scale *= 1.5 - 0.1 * heading.level.v as f64;
let factor = 1.5 - 0.1 * heading.level.v as f64;
self.style.text.font_size.scale *= factor;
self.style.text.strong = true; self.style.text.strong = true;
self.layout_parbreak(); self.layout_parbreak();

View File

@ -177,38 +177,6 @@ impl Display for ParseLengthError {
} }
} }
/// Either an absolute length or a factor of some entity.
#[derive(Copy, Clone, PartialEq)]
pub enum ScaleLength {
Absolute(Length),
Scaled(f64),
}
impl ScaleLength {
/// Use the absolute value or scale the entity.
pub fn raw_scaled(&self, entity: f64) -> f64 {
match *self {
ScaleLength::Absolute(l) => l.as_raw(),
ScaleLength::Scaled(s) => s * entity,
}
}
}
impl Display for ScaleLength {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
ScaleLength::Absolute(length) => write!(f, "{}", length),
ScaleLength::Scaled(scale) => write!(f, "{}%", scale * 100.0),
}
}
}
impl Debug for ScaleLength {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(self, f)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,5 +1,5 @@
use super::*; use super::*;
use crate::length::ScaleLength; use crate::geom::Linear;
/// `box`: Layouts its contents into a box. /// `box`: Layouts its contents into a box.
/// ///
@ -19,17 +19,17 @@ pub async fn boxed(
ctx.spaces.truncate(1); ctx.spaces.truncate(1);
ctx.repeat = false; ctx.repeat = false;
if let Some(width) = args.take_key::<ScaleLength>("width", &mut f) { if let Some(width) = args.take_key::<Linear>("width", &mut f) {
let length = width.raw_scaled(ctx.base.width); let abs = width.eval(ctx.base.width);
ctx.base.width = length; ctx.base.width = abs;
ctx.spaces[0].size.width = length; ctx.spaces[0].size.width = abs;
ctx.spaces[0].expansion.horizontal = true; ctx.spaces[0].expansion.horizontal = true;
} }
if let Some(height) = args.take_key::<ScaleLength>("height", &mut f) { if let Some(height) = args.take_key::<Linear>("height", &mut f) {
let length = height.raw_scaled(ctx.base.height); let abs = height.eval(ctx.base.height);
ctx.base.height = length; ctx.base.height = abs;
ctx.spaces[0].size.height = length; ctx.spaces[0].size.height = abs;
ctx.spaces[0].expansion.vertical = true; ctx.spaces[0].expansion.vertical = true;
} }

View File

@ -5,20 +5,18 @@ use crate::color::RgbaColor;
pub async fn rgb(span: Span, mut args: DictValue, _: LayoutContext<'_>) -> Pass<Value> { pub async fn rgb(span: Span, mut args: DictValue, _: LayoutContext<'_>) -> Pass<Value> {
let mut f = Feedback::new(); let mut f = Feedback::new();
let r = args.expect::<Spanned<f64>>("red value", span, &mut f); let r = args.expect::<Spanned<i64>>("red value", span, &mut f);
let g = args.expect::<Spanned<f64>>("green value", span, &mut f); let g = args.expect::<Spanned<i64>>("green value", span, &mut f);
let b = args.expect::<Spanned<f64>>("blue value", span, &mut f); let b = args.expect::<Spanned<i64>>("blue value", span, &mut f);
let a = args.take::<Spanned<f64>>(); let a = args.take::<Spanned<i64>>();
let mut clamp = |component: Option<Spanned<f64>>, default| { let mut clamp = |component: Option<Spanned<i64>>, default| {
component component.map_or(default, |c| {
.map(|c| { if c.v < 0 || c.v > 255 {
if c.v < 0.0 || c.v > 255.0 { error!(@f, c.span, "should be between 0 and 255")
error!(@f, c.span, "should be between 0 and 255") }
} c.v.max(0).min(255) as u8
c.v.min(255.0).max(0.0).round() as u8 })
})
.unwrap_or(default)
}; };
let color = RgbaColor::new(clamp(r, 0), clamp(g, 0), clamp(b, 0), clamp(a, 255)); let color = RgbaColor::new(clamp(r, 0), clamp(g, 0), clamp(b, 0), clamp(a, 255));

View File

@ -2,7 +2,7 @@ use fontdock::{FontStretch, FontStyle, FontWeight};
use super::*; use super::*;
use crate::eval::StringLike; use crate::eval::StringLike;
use crate::length::ScaleLength; use crate::geom::Linear;
/// `font`: Configure the font. /// `font`: Configure the font.
/// ///
@ -56,13 +56,12 @@ pub async fn font(_: Span, mut args: DictValue, ctx: LayoutContext<'_>) -> Pass<
let content = args.take::<SynTree>(); let content = args.take::<SynTree>();
if let Some(s) = args.take::<ScaleLength>() { if let Some(linear) = args.take::<Linear>() {
match s { if linear.rel == 0.0 {
ScaleLength::Absolute(length) => { text.font_size.base = linear.abs;
text.base_font_size = length.as_raw(); text.font_size.scale = Linear::rel(1.0);
text.font_scale = 1.0; } else {
} text.font_size.scale = linear;
ScaleLength::Scaled(scale) => text.font_scale = scale,
} }
} }

View File

@ -1,8 +1,8 @@
use std::mem; use std::mem;
use super::*; use super::*;
use crate::geom::Sides; use crate::eval::Abs;
use crate::length::{Length, ScaleLength}; use crate::geom::{Linear, Sides};
use crate::paper::{Paper, PaperClass}; use crate::paper::{Paper, PaperClass};
/// `page`: Configure pages. /// `page`: Configure pages.
@ -28,33 +28,33 @@ pub async fn page(_: Span, mut args: DictValue, ctx: LayoutContext<'_>) -> Pass<
style.size = paper.size(); style.size = paper.size();
} }
if let Some(width) = args.take_key::<Length>("width", &mut f) { if let Some(Abs(width)) = args.take_key::<Abs>("width", &mut f) {
style.class = PaperClass::Custom; style.class = PaperClass::Custom;
style.size.width = width.as_raw(); style.size.width = width;
} }
if let Some(height) = args.take_key::<Length>("height", &mut f) { if let Some(Abs(height)) = args.take_key::<Abs>("height", &mut f) {
style.class = PaperClass::Custom; style.class = PaperClass::Custom;
style.size.height = height.as_raw(); style.size.height = height;
} }
if let Some(margins) = args.take_key::<ScaleLength>("margins", &mut f) { if let Some(margins) = args.take_key::<Linear>("margins", &mut f) {
style.margins = Sides::uniform(Some(margins)); style.margins = Sides::uniform(Some(margins));
} }
if let Some(left) = args.take_key::<ScaleLength>("left", &mut f) { if let Some(left) = args.take_key::<Linear>("left", &mut f) {
style.margins.left = Some(left); style.margins.left = Some(left);
} }
if let Some(top) = args.take_key::<ScaleLength>("top", &mut f) { if let Some(top) = args.take_key::<Linear>("top", &mut f) {
style.margins.top = Some(top); style.margins.top = Some(top);
} }
if let Some(right) = args.take_key::<ScaleLength>("right", &mut f) { if let Some(right) = args.take_key::<Linear>("right", &mut f) {
style.margins.right = Some(right); style.margins.right = Some(right);
} }
if let Some(bottom) = args.take_key::<ScaleLength>("bottom", &mut f) { if let Some(bottom) = args.take_key::<Linear>("bottom", &mut f) {
style.margins.bottom = Some(bottom); style.margins.bottom = Some(bottom);
} }

View File

@ -1,6 +1,6 @@
use super::*; use super::*;
use crate::geom::Linear;
use crate::layout::SpacingKind; use crate::layout::SpacingKind;
use crate::length::ScaleLength;
/// `h`: Add horizontal spacing. /// `h`: Add horizontal spacing.
/// ///
@ -26,10 +26,10 @@ fn spacing(
) -> Pass<Value> { ) -> Pass<Value> {
let mut f = Feedback::new(); let mut f = Feedback::new();
let spacing = args.expect::<ScaleLength>("spacing", name, &mut f); let spacing = args.expect::<Linear>("spacing", name, &mut f);
let commands = if let Some(spacing) = spacing { let commands = if let Some(spacing) = spacing {
let axis = axis.to_gen(ctx.sys); let axis = axis.to_gen(ctx.sys);
let spacing = spacing.raw_scaled(ctx.style.text.font_size()); let spacing = spacing.eval(ctx.style.text.font_size());
vec![AddSpacing(spacing, SpacingKind::Hard, axis)] vec![AddSpacing(spacing, SpacingKind::Hard, axis)]
} else { } else {
vec![] vec![]

View File

@ -1,7 +1,7 @@
//! Predefined papers. //! Predefined papers.
use crate::geom::{Sides, Size}; use crate::geom::{Linear, Sides, Size};
use crate::length::{Length, ScaleLength}; use crate::length::Length;
/// Specification of a paper. /// Specification of a paper.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
@ -38,15 +38,15 @@ pub enum PaperClass {
impl PaperClass { impl PaperClass {
/// The default margin ratios for this page class. /// The default margin ratios for this page class.
pub fn default_margins(self) -> Sides<ScaleLength> { pub fn default_margins(self) -> Sides<Linear> {
let s = ScaleLength::Scaled; let f = Linear::rel;
let f = |l, r, t, b| Sides::new(s(l), s(r), s(t), s(b)); let s = |l, r, t, b| Sides::new(f(l), f(r), f(t), f(b));
match self { match self {
Self::Custom => f(0.1190, 0.0842, 0.1190, 0.0842), Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842),
Self::Base => f(0.1190, 0.0842, 0.1190, 0.0842), Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842),
Self::US => f(0.1760, 0.1092, 0.1760, 0.0910), Self::US => s(0.1760, 0.1092, 0.1760, 0.0910),
Self::Newspaper => f(0.0455, 0.0587, 0.0455, 0.0294), Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294),
Self::Book => f(0.1200, 0.0852, 0.1500, 0.0965), Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965),
} }
} }
} }

View File

@ -72,6 +72,7 @@ fn node(p: &mut Parser, at_start: bool) -> Option<Spanned<SynNode>> {
SynNode::Text(p.eaten_from(start).into()) SynNode::Text(p.eaten_from(start).into())
} }
} }
Token::NonBreakingSpace => SynNode::Text("\u{00A0}".into()),
Token::Raw(token) => SynNode::Raw(raw(p, token)), Token::Raw(token) => SynNode::Raw(raw(p, token)),
Token::UnicodeEscape(token) => SynNode::Text(unicode_escape(p, token, start)), Token::UnicodeEscape(token) => SynNode::Text(unicode_escape(p, token, start)),
@ -425,8 +426,10 @@ fn value(p: &mut Parser) -> Option<Expr> {
// Atomic values. // Atomic values.
Token::Bool(b) => Expr::Lit(Lit::Bool(b)), Token::Bool(b) => Expr::Lit(Lit::Bool(b)),
Token::Number(f) => Expr::Lit(Lit::Float(f)), Token::Int(i) => Expr::Lit(Lit::Int(i)),
Token::Float(f) => Expr::Lit(Lit::Float(f)),
Token::Length(l) => Expr::Lit(Lit::Length(l)), Token::Length(l) => Expr::Lit(Lit::Length(l)),
Token::Percent(p) => Expr::Lit(Lit::Percent(p)),
Token::Hex(hex) => Expr::Lit(Lit::Color(color(p, hex, start))), Token::Hex(hex) => Expr::Lit(Lit::Color(color(p, hex, start))),
Token::Str(token) => Expr::Lit(Lit::Str(string(p, token))), Token::Str(token) => Expr::Lit(Lit::Str(string(p, token))),

View File

@ -57,13 +57,13 @@ fn Id(ident: &str) -> Expr {
fn Bool(b: bool) -> Expr { fn Bool(b: bool) -> Expr {
Expr::Lit(Lit::Bool(b)) Expr::Lit(Lit::Bool(b))
} }
fn _Int(int: i64) -> Expr { fn Int(int: i64) -> Expr {
Expr::Lit(Lit::Int(int)) Expr::Lit(Lit::Int(int))
} }
fn Float(float: f64) -> Expr { fn Float(float: f64) -> Expr {
Expr::Lit(Lit::Float(float)) Expr::Lit(Lit::Float(float))
} }
fn _Percent(percent: f64) -> Expr { fn Percent(percent: f64) -> Expr {
Expr::Lit(Lit::Percent(percent)) Expr::Lit(Lit::Percent(percent))
} }
fn Len(length: Length) -> Expr { fn Len(length: Length) -> Expr {
@ -334,7 +334,7 @@ fn test_parse_function_names() {
t!("[ f]" => F!("f")); t!("[ f]" => F!("f"));
// An invalid name. // An invalid name.
e!("[12]" => s(1, 3, "expected function name, found number")); e!("[12]" => s(1, 3, "expected function name, found integer"));
e!("[ 🌎]" => s(3, 7, "expected function name, found invalid token")); e!("[ 🌎]" => s(3, 7, "expected function name, found invalid token"));
} }
@ -345,7 +345,7 @@ fn test_parse_chaining() {
t!("[box >> pad: 1pt][Hi]" => F!("box"; Tree![ t!("[box >> pad: 1pt][Hi]" => F!("box"; Tree![
F!("pad"; Len(Length::pt(1.0)), Tree!(T("Hi"))) F!("pad"; Len(Length::pt(1.0)), Tree!(T("Hi")))
])); ]));
t!("[bold: 400, >> emph >> sub: 1cm]" => F!("bold"; Float(400.0), Tree![ t!("[bold: 400, >> emph >> sub: 1cm]" => F!("bold"; Int(400), Tree![
F!("emph"; Tree!(F!("sub"; Len(Length::cm(1.0))))) F!("emph"; Tree!(F!("sub"; Len(Length::cm(1.0)))))
])); ]));
@ -374,7 +374,7 @@ fn test_parse_colon_starting_func_args() {
#[test] #[test]
fn test_parse_function_bodies() { fn test_parse_function_bodies() {
t!("[val: 1][*Hi*]" => F!("val"; Float(1.0), Tree![B, T("Hi"), B])); t!("[val: 1][*Hi*]" => F!("val"; Int(1), Tree![B, T("Hi"), B]));
e!(" [val][ */]" => s(8, 10, "unexpected end of block comment")); e!(" [val][ */]" => s(8, 10, "unexpected end of block comment"));
// Raw in body. // Raw in body.
@ -406,7 +406,7 @@ fn test_parse_values() {
v!("false" => Bool(false)); v!("false" => Bool(false));
v!("1.0e-4" => Float(1e-4)); v!("1.0e-4" => Float(1e-4));
v!("3.14" => Float(3.14)); v!("3.14" => Float(3.14));
v!("50%" => Float(0.5)); v!("50%" => Percent(50.0));
v!("4.5cm" => Len(Length::cm(4.5))); v!("4.5cm" => Len(Length::cm(4.5)));
v!("12e1pt" => Len(Length::pt(12e1))); v!("12e1pt" => Len(Length::pt(12e1)));
v!("#f7a20500" => Color(RgbaColor::new(0xf7, 0xa2, 0x05, 0x00))); v!("#f7a20500" => Color(RgbaColor::new(0xf7, 0xa2, 0x05, 0x00)));
@ -440,43 +440,43 @@ fn test_parse_expressions() {
v!("(hi)" => Id("hi")); v!("(hi)" => Id("hi"));
// Operations. // Operations.
v!("-1" => Unary(Neg, Float(1.0))); v!("-1" => Unary(Neg, Int(1)));
v!("-- 1" => Unary(Neg, Unary(Neg, Float(1.0)))); v!("-- 1" => Unary(Neg, Unary(Neg, Int(1))));
v!("3.2in + 6pt" => Binary(Add, Len(Length::inches(3.2)), Len(Length::pt(6.0)))); v!("3.2in + 6pt" => Binary(Add, Len(Length::inches(3.2)), Len(Length::pt(6.0))));
v!("5 - 0.01" => Binary(Sub, Float(5.0), Float(0.01))); v!("5 - 0.01" => Binary(Sub, Int(5), Float(0.01)));
v!("(3mm * 2)" => Binary(Mul, Len(Length::mm(3.0)), Float(2.0))); v!("(3mm * 2)" => Binary(Mul, Len(Length::mm(3.0)), Int(2)));
v!("12e-3cm/1pt" => Binary(Div, Len(Length::cm(12e-3)), Len(Length::pt(1.0)))); v!("12e-3cm/1pt" => Binary(Div, Len(Length::cm(12e-3)), Len(Length::pt(1.0))));
// More complex. // More complex.
v!("(3.2in + 6pt)*(5/2-1)" => Binary( v!("(3.2in + 6pt)*(5/2-1)" => Binary(
Mul, Mul,
Binary(Add, Len(Length::inches(3.2)), Len(Length::pt(6.0))), Binary(Add, Len(Length::inches(3.2)), Len(Length::pt(6.0))),
Binary(Sub, Binary(Div, Float(5.0), Float(2.0)), Float(1.0)) Binary(Sub, Binary(Div, Int(5), Int(2)), Int(1))
)); ));
v!("(6.3E+2+4* - 3.2pt)/2" => Binary( v!("(6.3E+2+4* - 3.2pt)/2" => Binary(
Div, Div,
Binary(Add, Float(6.3e2), Binary( Binary(Add, Float(6.3e2), Binary(
Mul, Mul,
Float(4.0), Int(4),
Unary(Neg, Len(Length::pt(3.2))) Unary(Neg, Len(Length::pt(3.2)))
)), )),
Float(2.0) Int(2)
)); ));
// Associativity of multiplication and division. // Associativity of multiplication and division.
v!("3/4*5" => Binary(Mul, Binary(Div, Float(3.0), Float(4.0)), Float(5.0))); v!("3/4*5" => Binary(Mul, Binary(Div, Int(3), Int(4)), Int(5)));
// Spanned. // Spanned.
ts!("[val: 1 + 3]" => s(0, 12, F!( ts!("[val: 1 + 3]" => s(0, 12, F!(
s(1, 4, "val"); s(6, 11, Binary( s(1, 4, "val"); s(6, 11, Binary(
s(8, 9, Add), s(8, 9, Add),
s(6, 7, Float(1.0)), s(6, 7, Int(1)),
s(10, 11, Float(3.0)) s(10, 11, Int(3))
)) ))
))); )));
// Span of parenthesized expression contains parens. // Span of parenthesized expression contains parens.
ts!("[val: (1)]" => s(0, 10, F!(s(1, 4, "val"); s(6, 9, Float(1.0))))); ts!("[val: (1)]" => s(0, 10, F!(s(1, 4, "val"); s(6, 9, Int(1)))));
// Invalid expressions. // Invalid expressions.
v!("4pt--" => Len(Length::pt(4.0))); v!("4pt--" => Len(Length::pt(4.0)));
@ -494,8 +494,8 @@ fn test_parse_dicts() {
v!("(false)" => Bool(false)); v!("(false)" => Bool(false));
v!("(true,)" => Dict![Bool(true)]); v!("(true,)" => Dict![Bool(true)]);
v!("(key=val)" => Dict!["key" => Id("val")]); v!("(key=val)" => Dict!["key" => Id("val")]);
v!("(1, 2)" => Dict![Float(1.0), Float(2.0)]); v!("(1, 2)" => Dict![Int(1), Int(2)]);
v!("(1, key=\"value\")" => Dict![Float(1.0), "key" => Str("value")]); v!("(1, key=\"value\")" => Dict![Int(1), "key" => Str("value")]);
// Decorations. // Decorations.
d!("[val: key=hi]" => s(6, 9, DictKey)); d!("[val: key=hi]" => s(6, 9, DictKey));
@ -513,7 +513,7 @@ fn test_parse_dicts() {
#[test] #[test]
fn test_parse_dicts_compute_func_calls() { fn test_parse_dicts_compute_func_calls() {
v!("empty()" => Call!("empty")); v!("empty()" => Call!("empty"));
v!("add ( 1 , 2 )" => Call!("add"; Float(1.0), Float(2.0))); v!("add ( 1 , 2 )" => Call!("add"; Int(1), Int(2)));
v!("items(\"fire\", #f93a6d)" => Call!("items"; v!("items(\"fire\", #f93a6d)" => Call!("items";
Str("fire"), Color(RgbaColor::new(0xf9, 0x3a, 0x6d, 0xff)) Str("fire"), Color(RgbaColor::new(0xf9, 0x3a, 0x6d, 0xff))
)); ));
@ -522,7 +522,7 @@ fn test_parse_dicts_compute_func_calls() {
v!("css(1pt, rgb(90, 102, 254), \"solid\")" => Call!( v!("css(1pt, rgb(90, 102, 254), \"solid\")" => Call!(
"css"; "css";
Len(Length::pt(1.0)), Len(Length::pt(1.0)),
Call!("rgb"; Float(90.0), Float(102.0), Float(254.0)), Call!("rgb"; Int(90), Int(102), Int(254)),
Str("solid"), Str("solid"),
)); ));
@ -539,10 +539,10 @@ fn test_parse_dicts_compute_func_calls() {
fn test_parse_dicts_nested() { fn test_parse_dicts_nested() {
v!("(1, ( ab=(), d = (3, 14pt) )), false" => v!("(1, ( ab=(), d = (3, 14pt) )), false" =>
Dict![ Dict![
Float(1.0), Int(1),
Dict!( Dict!(
"ab" => Dict![], "ab" => Dict![],
"d" => Dict!(Float(3.0), Len(Length::pt(14.0))), "d" => Dict!(Int(3), Len(Length::pt(14.0))),
), ),
], ],
Bool(false), Bool(false),
@ -576,7 +576,7 @@ fn test_parse_dicts_errors() {
s(10, 11, "expected value, found equals sign")); s(10, 11, "expected value, found equals sign"));
// Unexpected equals sign. // Unexpected equals sign.
v!("z=y=4" => "z" => Id("y"), Float(4.0)); v!("z=y=4" => "z" => Id("y"), Int(4));
e!("[val: z=y=4]" => e!("[val: z=y=4]" =>
s(9, 9, "expected comma"), s(9, 9, "expected comma"),
s(9, 10, "expected value, found equals sign")); s(9, 10, "expected value, found equals sign"));

View File

@ -87,8 +87,8 @@ impl<'s> Iterator for Tokens<'s> {
'*' if self.mode == Body => Token::Star, '*' if self.mode == Body => Token::Star,
'_' if self.mode == Body => Token::Underscore, '_' if self.mode == Body => Token::Underscore,
'#' if self.mode == Body => Token::Hashtag, '#' if self.mode == Body => Token::Hashtag,
'~' if self.mode == Body => Token::NonBreakingSpace,
'`' if self.mode == Body => self.read_raw(), '`' if self.mode == Body => self.read_raw(),
'~' if self.mode == Body => Token::Text("\u{00A0}"),
'\\' if self.mode == Body => self.read_escaped(), '\\' if self.mode == Body => self.read_escaped(),
// Syntactic elements in headers. // Syntactic elements in headers.
@ -273,10 +273,12 @@ impl Debug for Tokens<'_> {
fn parse_expr(text: &str) -> Token<'_> { fn parse_expr(text: &str) -> Token<'_> {
if let Ok(b) = text.parse::<bool>() { if let Ok(b) = text.parse::<bool>() {
Token::Bool(b) Token::Bool(b)
} else if let Ok(int) = text.parse::<i64>() {
Token::Int(int)
} else if let Ok(num) = text.parse::<f64>() { } else if let Ok(num) = text.parse::<f64>() {
Token::Number(num) Token::Float(num)
} else if let Some(num) = parse_percent(text) { } else if let Some(percent) = parse_percent(text) {
Token::Number(num / 100.0) Token::Percent(percent)
} else if let Ok(length) = text.parse::<Length>() { } else if let Ok(length) = text.parse::<Length>() {
Token::Length(length) Token::Length(length)
} else if is_ident(text) { } else if is_ident(text) {
@ -298,10 +300,10 @@ mod tests {
use crate::parse::tests::check; use crate::parse::tests::check;
use Token::{ use Token::{
BlockComment as BC, Bool, Chain, Hex, Hyphen as Min, Ident as Id, BlockComment as BC, Bool, Chain, Float, Hex, Hyphen as Min, Ident as Id, Int,
LeftBrace as LB, LeftBracket as L, LeftParen as LP, Length as Len, LeftBrace as LB, LeftBracket as L, LeftParen as LP, Length as Len,
LineComment as LC, Number as Num, Plus, RightBrace as RB, RightBracket as R, LineComment as LC, NonBreakingSpace as Nbsp, Percent, Plus, RightBrace as RB,
RightParen as RP, Slash, Space as S, Star, Text as T, *, RightBracket as R, RightParen as RP, Slash, Space as S, Star, Text as T, *,
}; };
fn Str(string: &str, terminated: bool) -> Token { fn Str(string: &str, terminated: bool) -> Token {
@ -337,7 +339,7 @@ mod tests {
t!(Body, " \n\t \n " => S(2)); t!(Body, " \n\t \n " => S(2));
t!(Body, "\n\r" => S(2)); t!(Body, "\n\r" => S(2));
t!(Body, " \r\r\n \x0D" => S(3)); t!(Body, " \r\r\n \x0D" => S(3));
t!(Body, "a~b" => T("a"), T("\u{00A0}"), T("b")); t!(Body, "a~b" => T("a"), Nbsp, T("b"));
} }
#[test] #[test]
@ -424,24 +426,24 @@ mod tests {
t!(Header, ">main" => Invalid(">main")); t!(Header, ">main" => Invalid(">main"));
t!(Header, "🌓, 🌍," => Invalid("🌓"), Comma, S(0), Invalid("🌍"), Comma); t!(Header, "🌓, 🌍," => Invalid("🌓"), Comma, S(0), Invalid("🌍"), Comma);
t!(Header, "{abc}" => LB, Id("abc"), RB); t!(Header, "{abc}" => LB, Id("abc"), RB);
t!(Header, "(1,2)" => LP, Num(1.0), Comma, Num(2.0), RP); t!(Header, "(1,2)" => LP, Int(1), Comma, Int(2), RP);
t!(Header, "12_pt, 12pt" => Invalid("12_pt"), Comma, S(0), Len(Length::pt(12.0))); t!(Header, "12_pt, 12pt" => Invalid("12_pt"), Comma, S(0), Len(Length::pt(12.0)));
t!(Header, "f: arg >> g" => Id("f"), Colon, S(0), Id("arg"), S(0), Chain, S(0), Id("g")); t!(Header, "f: arg >> g" => Id("f"), Colon, S(0), Id("arg"), S(0), Chain, S(0), Id("g"));
t!(Header, "=3.14" => Equals, Num(3.14)); t!(Header, "=3.14" => Equals, Float(3.14));
t!(Header, "arg, _b, _1" => Id("arg"), Comma, S(0), Id("_b"), Comma, S(0), Id("_1")); t!(Header, "arg, _b, _1" => Id("arg"), Comma, S(0), Id("_b"), Comma, S(0), Id("_1"));
t!(Header, "a:b" => Id("a"), Colon, Id("b")); t!(Header, "a:b" => Id("a"), Colon, Id("b"));
t!(Header, "(){}:=," => LP, RP, LB, RB, Colon, Equals, Comma); t!(Header, "(){}:=," => LP, RP, LB, RB, Colon, Equals, Comma);
t!(Body, "c=d, " => T("c=d,"), S(0)); t!(Body, "c=d, " => T("c=d,"), S(0));
t!(Body, "a: b" => T("a:"), S(0), T("b")); t!(Body, "a: b" => T("a:"), S(0), T("b"));
t!(Header, "a: true, x=1" => Id("a"), Colon, S(0), Bool(true), Comma, S(0), t!(Header, "a: true, x=1" => Id("a"), Colon, S(0), Bool(true), Comma, S(0),
Id("x"), Equals, Num(1.0)); Id("x"), Equals, Int(1));
} }
#[test] #[test]
fn tokenize_numeric_values() { fn tokenize_numeric_values() {
t!(Header, "12.3e5" => Num(12.3e5)); t!(Header, "12.3e5" => Float(12.3e5));
t!(Header, "120%" => Num(1.2)); t!(Header, "120%" => Percent(120.0));
t!(Header, "12e4%" => Num(1200.0)); t!(Header, "12e4%" => Percent(120000.0));
t!(Header, "1e5in" => Len(Length::inches(100000.0))); t!(Header, "1e5in" => Len(Length::inches(100000.0)));
t!(Header, "2.3cm" => Len(Length::cm(2.3))); t!(Header, "2.3cm" => Len(Length::cm(2.3)));
t!(Header, "02.4mm" => Len(Length::mm(2.4))); t!(Header, "02.4mm" => Len(Length::mm(2.4)));
@ -456,7 +458,7 @@ mod tests {
t!(Header, "\"hello" => Str("hello", false)); t!(Header, "\"hello" => Str("hello", false));
t!(Header, "\"hello world\"" => Str("hello world", true)); t!(Header, "\"hello world\"" => Str("hello world", true));
t!(Header, "\"hello\nworld\"" => Str("hello\nworld", true)); t!(Header, "\"hello\nworld\"" => Str("hello\nworld", true));
t!(Header, r#"1"hello\nworld"false"# => Num(1.0), Str("hello\\nworld", true), Bool(false)); t!(Header, r#"1"hello\nworld"false"# => Int(1), Str("hello\\nworld", true), Bool(false));
t!(Header, r#""a\"bc""# => Str(r#"a\"bc"#, true)); t!(Header, r#""a\"bc""# => Str(r#"a\"bc"#, true));
t!(Header, r#""a\\"bc""# => Str(r#"a\\"#, true), Id("bc"), Str("", false)); t!(Header, r#""a\\"bc""# => Str(r#"a\\"#, true), Id("bc"), Str("", false));
t!(Header, r#""a\tbc"# => Str("a\\tbc", false)); t!(Header, r#""a\tbc"# => Str("a\\tbc", false));
@ -466,12 +468,12 @@ mod tests {
#[test] #[test]
fn tokenize_math() { fn tokenize_math() {
t!(Header, "12e-3in" => Len(Length::inches(12e-3))); t!(Header, "12e-3in" => Len(Length::inches(12e-3)));
t!(Header, "-1" => Min, Num(1.0)); t!(Header, "-1" => Min, Int(1));
t!(Header, "--1" => Min, Min, Num(1.0)); t!(Header, "--1" => Min, Min, Int(1));
t!(Header, "- 1" => Min, S(0), Num(1.0)); t!(Header, "- 1" => Min, S(0), Int(1));
t!(Header, "6.1cm + 4pt,a=1*2" => Len(Length::cm(6.1)), S(0), Plus, S(0), Len(Length::pt(4.0)), t!(Header, "6.1cm + 4pt,a=1*2" => Len(Length::cm(6.1)), S(0), Plus, S(0), Len(Length::pt(4.0)),
Comma, Id("a"), Equals, Num(1.0), Star, Num(2.0)); Comma, Id("a"), Equals, Int(1), Star, Int(2));
t!(Header, "(5 - 1) / 2.1" => LP, Num(5.0), S(0), Min, S(0), Num(1.0), RP, t!(Header, "(5 - 1) / 2.1" => LP, Int(5), S(0), Min, S(0), Int(1), RP,
S(0), Slash, S(0), Num(2.1)); S(0), Slash, S(0), Float(2.1));
} }
} }

View File

@ -2,8 +2,8 @@
use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight}; use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight};
use crate::geom::{Insets, Sides, Size}; use crate::geom::{Insets, Linear, Sides, Size};
use crate::length::{Length, ScaleLength}; use crate::length::Length;
use crate::paper::{Paper, PaperClass, PAPER_A4}; use crate::paper::{Paper, PaperClass, PAPER_A4};
/// Defines properties of pages and text. /// Defines properties of pages and text.
@ -28,37 +28,35 @@ pub struct TextStyle {
/// Whether the emphasis toggle is active or inactive. This determines /// Whether the emphasis toggle is active or inactive. This determines
/// whether the next `_` makes italic or non-italic. /// whether the next `_` makes italic or non-italic.
pub emph: bool, pub emph: bool,
/// The base font size. /// The font size.
pub base_font_size: f64, pub font_size: FontSize,
/// The font scale to apply on the base font size. /// The word spacing (relative to the the font size).
pub font_scale: f64, pub word_spacing: Linear,
/// The word spacing (as a multiple of the font size). /// The line spacing (relative to the the font size).
pub word_spacing_scale: f64, pub line_spacing: Linear,
/// The line spacing (as a multiple of the font size). /// The paragraphs spacing (relative to the the font size).
pub line_spacing_scale: f64, pub par_spacing: Linear,
/// The paragraphs spacing (as a multiple of the font size).
pub paragraph_spacing_scale: f64,
} }
impl TextStyle { impl TextStyle {
/// The scaled font size. /// The absolute font size.
pub fn font_size(&self) -> f64 { pub fn font_size(&self) -> f64 {
self.base_font_size * self.font_scale self.font_size.eval()
} }
/// The absolute word spacing. /// The absolute word spacing.
pub fn word_spacing(&self) -> f64 { pub fn word_spacing(&self) -> f64 {
self.word_spacing_scale * self.font_size() self.word_spacing.eval(self.font_size())
} }
/// The absolute line spacing. /// The absolute line spacing.
pub fn line_spacing(&self) -> f64 { pub fn line_spacing(&self) -> f64 {
(self.line_spacing_scale - 1.0) * self.font_size() self.line_spacing.eval(self.font_size())
} }
/// The absolute paragraph spacing. /// The absolute paragraph spacing.
pub fn paragraph_spacing(&self) -> f64 { pub fn paragraph_spacing(&self) -> f64 {
(self.paragraph_spacing_scale - 1.0) * self.font_size() self.par_spacing.eval(self.font_size())
} }
} }
@ -85,15 +83,41 @@ impl Default for TextStyle {
}, },
strong: false, strong: false,
emph: false, emph: false,
base_font_size: Length::pt(11.0).as_raw(), font_size: FontSize::abs(Length::pt(11.0).as_raw()),
font_scale: 1.0, word_spacing: Linear::rel(0.25),
word_spacing_scale: 0.25, line_spacing: Linear::rel(0.2),
line_spacing_scale: 1.2, par_spacing: Linear::rel(0.5),
paragraph_spacing_scale: 1.5,
} }
} }
} }
/// The font size, defined by base and scale.
#[derive(Debug, Clone, PartialEq)]
pub struct FontSize {
/// The base font size, updated whenever the font size is set absolutely.
pub base: f64,
/// The scale to apply on the base font size, updated when the font size
/// is set relatively.
pub scale: Linear,
}
impl FontSize {
/// Create a new font size.
pub fn new(base: f64, scale: Linear) -> Self {
Self { base, scale }
}
/// Create a new font size with the given `base` and a scale of `1.0`.
pub fn abs(base: f64) -> Self {
Self::new(base, Linear::rel(1.0))
}
/// Compute the absolute font size.
pub fn eval(&self) -> f64 {
self.scale.eval(self.base)
}
}
/// Defines the size and margins of a page. /// Defines the size and margins of a page.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct PageStyle { pub struct PageStyle {
@ -103,7 +127,7 @@ pub struct PageStyle {
pub size: Size, pub size: Size,
/// The amount of white space in the order [left, top, right, bottom]. If a /// The amount of white space in the order [left, top, right, bottom]. If a
/// side is set to `None`, the default for the paper class is used. /// side is set to `None`, the default for the paper class is used.
pub margins: Sides<Option<ScaleLength>>, pub margins: Sides<Option<Linear>>,
} }
impl PageStyle { impl PageStyle {
@ -121,10 +145,10 @@ impl PageStyle {
let Size { width, height } = self.size; let Size { width, height } = self.size;
let default = self.class.default_margins(); let default = self.class.default_margins();
Insets { Insets {
x0: -self.margins.left.unwrap_or(default.left).raw_scaled(width), x0: -self.margins.left.unwrap_or(default.left).eval(width),
y0: -self.margins.top.unwrap_or(default.top).raw_scaled(height), y0: -self.margins.top.unwrap_or(default.top).eval(height),
x1: -self.margins.right.unwrap_or(default.right).raw_scaled(width), x1: -self.margins.right.unwrap_or(default.right).eval(width),
y1: -self.margins.bottom.unwrap_or(default.bottom).raw_scaled(height), y1: -self.margins.bottom.unwrap_or(default.bottom).eval(height),
} }
} }
} }

View File

@ -18,10 +18,15 @@ pub enum Lit {
Int(i64), Int(i64),
/// A floating-point literal: `1.2`, `10e-4`. /// A floating-point literal: `1.2`, `10e-4`.
Float(f64), Float(f64),
/// A percent literal: `50%`.
Percent(f64),
/// A length literal: `12pt`, `3cm`. /// A length literal: `12pt`, `3cm`.
Length(Length), Length(Length),
/// A percent literal: `50%`.
///
/// Note: `50%` is represented as `50.0` here, but as `0.5` in the
/// corresponding [value].
///
/// [value]: ../../eval/enum.Value.html#variant.Relative
Percent(f64),
/// A color literal: `#ffccee`. /// A color literal: `#ffccee`.
Color(RgbaColor), Color(RgbaColor),
/// A string literal: `"hello!"`. /// A string literal: `"hello!"`.
@ -42,10 +47,10 @@ impl Lit {
match *self { match *self {
Lit::Ident(ref i) => Value::Ident(i.clone()), Lit::Ident(ref i) => Value::Ident(i.clone()),
Lit::Bool(b) => Value::Bool(b), Lit::Bool(b) => Value::Bool(b),
Lit::Int(i) => Value::Number(i as f64), Lit::Int(i) => Value::Int(i),
Lit::Float(f) => Value::Number(f as f64), Lit::Float(f) => Value::Float(f),
Lit::Percent(p) => Value::Number(p as f64 / 100.0), Lit::Length(l) => Value::Length(l.as_raw()),
Lit::Length(l) => Value::Length(l), Lit::Percent(p) => Value::Relative(p / 100.0),
Lit::Color(c) => Value::Color(c), Lit::Color(c) => Value::Color(c),
Lit::Str(ref s) => Value::Str(s.clone()), Lit::Str(ref s) => Value::Str(s.clone()),
Lit::Dict(ref d) => Value::Dict(d.eval(ctx, f).await), Lit::Dict(ref d) => Value::Dict(d.eval(ctx, f).await),

View File

@ -5,79 +5,91 @@ use crate::length::Length;
/// A minimal semantic entity of source code. /// A minimal semantic entity of source code.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub enum Token<'s> { pub enum Token<'s> {
/// One or more whitespace characters. The contained `usize` denotes the /// One or more whitespace characters.
/// number of newlines that were contained in the whitespace. ///
/// The contained `usize` denotes the number of newlines that were contained
/// in the whitespace.
Space(usize), Space(usize),
/// A consecutive non-markup string. /// A consecutive non-markup string.
Text(&'s str), Text(&'s str),
/// A line comment with inner string contents `//<str>\n`. /// A line comment with inner string contents `//<str>\n`.
LineComment(&'s str), LineComment(&'s str),
/// A block comment with inner string contents `/*<str>*/`. The comment /// A block comment with inner string contents `/*<str>*/`.
/// can contain nested block comments. ///
/// The comment can contain nested block comments.
BlockComment(&'s str), BlockComment(&'s str),
/// A star. It can appear in a function header where it signifies the /// A star: `*`.
/// multiplication of expressions or the body where it modifies the styling.
Star, Star,
/// An underscore in body-text. /// An underscore: `_`.
Underscore, Underscore,
/// A backslash followed by whitespace in text. /// A backslash followed by whitespace: `\`.
Backslash, Backslash,
/// A hashtag indicating a section heading. /// A hashtag indicating a section heading: `#`.
Hashtag, Hashtag,
/// A raw block. /// A non-breaking space: `~`.
NonBreakingSpace,
/// A raw block: `` `...` ``.
Raw(TokenRaw<'s>), Raw(TokenRaw<'s>),
/// A unicode escape sequence. /// A unicode escape sequence: `\u{1F5FA}`.
UnicodeEscape(TokenUnicodeEscape<'s>), UnicodeEscape(TokenUnicodeEscape<'s>),
/// A left bracket starting a function invocation or body: `[`. /// A left bracket: `[`.
LeftBracket, LeftBracket,
/// A right bracket ending a function invocation or body: `]`. /// A right bracket: `]`.
RightBracket, RightBracket,
/// A left brace indicating the start of content: `{`. /// A left brace: `{`.
LeftBrace, LeftBrace,
/// A right brace indicating the end of content: `}`. /// A right brace: `}`.
RightBrace, RightBrace,
/// A left parenthesis in a function header: `(`. /// A left parenthesis: `(`.
LeftParen, LeftParen,
/// A right parenthesis in a function header: `)`. /// A right parenthesis: `)`.
RightParen, RightParen,
/// A colon in a function header: `:`. /// A colon: `:`.
Colon, Colon,
/// A comma in a function header: `,`. /// A comma: `,`.
Comma, Comma,
/// An equals sign in a function header: `=`. /// An equals sign: `=`.
Equals, Equals,
/// A double forward chevron in a function header: `>>`. /// A double forward chevron: `>>`.
Chain, Chain,
/// A plus in a function header, signifying the addition of expressions. /// A plus: `+`.
Plus, Plus,
/// A hyphen in a function header, signifying the subtraction of /// A hyphen: `-`.
/// expressions.
Hyphen, Hyphen,
/// A slash in a function header, signifying the division of expressions. /// A slash: `/`.
Slash, Slash,
/// An identifier in a function header: `center`. /// An identifier: `center`.
Ident(&'s str), Ident(&'s str),
/// A boolean in a function header: `true | false`. /// A boolean: `true`, `false`.
Bool(bool), Bool(bool),
/// A number in a function header: `3.14`. /// An integer: `120`.
Number(f64), Int(i64),
/// A length in a function header: `12pt`. /// A floating-point number: `1.2`, `10e-4`.
Float(f64),
/// A length: `12pt`, `3cm`.
Length(Length), Length(Length),
/// A hex value in a function header: `#20d82a`. /// A percentage: `50%`.
///
/// Note: `50%` is represented as `50.0` here, as in the corresponding
/// [literal].
///
/// [literal]: ../ast/enum.Lit.html#variant.Percent
Percent(f64),
/// A hex value: `#20d82a`.
Hex(&'s str), Hex(&'s str),
/// A quoted string in a function header: `"..."`. /// A quoted string: `"..."`.
Str(TokenStr<'s>), Str(TokenStr<'s>),
/// Things that are not valid in the context they appeared in. /// Things that are not valid in the context they appeared in.
Invalid(&'s str), Invalid(&'s str),
} }
/// A quoted string in a function header: `"..."`. /// A quoted string: `"..."`.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct TokenStr<'s> { pub struct TokenStr<'s> {
/// The string inside the quotes. /// The string inside the quotes.
@ -90,7 +102,7 @@ pub struct TokenStr<'s> {
pub terminated: bool, pub terminated: bool,
} }
/// A unicode escape sequence. /// A unicode escape sequence: `\u{1F5FA}`.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct TokenUnicodeEscape<'s> { pub struct TokenUnicodeEscape<'s> {
/// The escape sequence between two braces. /// The escape sequence between two braces.
@ -99,7 +111,7 @@ pub struct TokenUnicodeEscape<'s> {
pub terminated: bool, pub terminated: bool,
} }
/// A raw block. /// A raw block: `` `...` ``.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct TokenRaw<'s> { pub struct TokenRaw<'s> {
/// The raw text between the backticks. /// The raw text between the backticks.
@ -111,7 +123,7 @@ pub struct TokenRaw<'s> {
} }
impl<'s> Token<'s> { impl<'s> Token<'s> {
/// The natural-language name for this token for use in error messages. /// The natural-language name of this token for use in error messages.
pub fn name(self) -> &'static str { pub fn name(self) -> &'static str {
match self { match self {
Self::Space(_) => "space", Self::Space(_) => "space",
@ -124,6 +136,7 @@ impl<'s> Token<'s> {
Self::Underscore => "underscore", Self::Underscore => "underscore",
Self::Backslash => "backslash", Self::Backslash => "backslash",
Self::Hashtag => "hashtag", Self::Hashtag => "hashtag",
Self::NonBreakingSpace => "non-breaking space",
Self::Raw { .. } => "raw block", Self::Raw { .. } => "raw block",
Self::UnicodeEscape { .. } => "unicode escape sequence", Self::UnicodeEscape { .. } => "unicode escape sequence",
@ -144,8 +157,10 @@ impl<'s> Token<'s> {
Self::Ident(_) => "identifier", Self::Ident(_) => "identifier",
Self::Bool(_) => "bool", Self::Bool(_) => "bool",
Self::Number(_) => "number", Self::Int(_) => "integer",
Self::Float(_) => "float",
Self::Length(_) => "length", Self::Length(_) => "length",
Self::Percent(_) => "percentage",
Self::Hex(_) => "hex value", Self::Hex(_) => "hex value",
Self::Str { .. } => "string", Self::Str { .. } => "string",