Castable optional and smart values

This commit is contained in:
Laurenz 2021-11-22 14:30:43 +01:00
parent cef46e6c40
commit ed50661378
15 changed files with 215 additions and 103 deletions

View File

@ -252,43 +252,6 @@ pub trait Cast<V>: Sized {
fn cast(value: V) -> StrResult<Self>; fn cast(value: V) -> StrResult<Self>;
} }
impl Cast<Value> for Value {
fn is(_: &Value) -> bool {
true
}
fn cast(value: Value) -> StrResult<Self> {
Ok(value)
}
}
impl<T> Cast<Spanned<Value>> for T
where
T: Cast<Value>,
{
fn is(value: &Spanned<Value>) -> bool {
T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
T::cast(value.v)
}
}
impl<T> Cast<Spanned<Value>> for Spanned<T>
where
T: Cast<Value>,
{
fn is(value: &Spanned<Value>) -> bool {
T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
let span = value.span;
T::cast(value.v).map(|t| Spanned::new(t, span))
}
}
/// Implement traits for primitives. /// Implement traits for primitives.
macro_rules! primitive { macro_rules! primitive {
( (
@ -400,6 +363,113 @@ primitive! { Dict: "dictionary", Dict }
primitive! { Template: "template", Template } primitive! { Template: "template", Template }
primitive! { Function: "function", Func } primitive! { Function: "function", Func }
impl Cast<Value> for Value {
fn is(_: &Value) -> bool {
true
}
fn cast(value: Value) -> StrResult<Self> {
Ok(value)
}
}
impl<T> Cast<Spanned<Value>> for T
where
T: Cast<Value>,
{
fn is(value: &Spanned<Value>) -> bool {
T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
T::cast(value.v)
}
}
impl<T> Cast<Spanned<Value>> for Spanned<T>
where
T: Cast<Value>,
{
fn is(value: &Spanned<Value>) -> bool {
T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
let span = value.span;
T::cast(value.v).map(|t| Spanned::new(t, span))
}
}
/// A value that can be automatically determined.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Smart<T> {
/// The value should be determined smartly based on the
/// circumstances.
Auto,
/// A forced, specific value.
Custom(T),
}
impl<T> Smart<T> {
/// Returns the contained custom value or a provided default value.
pub fn unwrap_or(self, default: T) -> T {
match self {
Self::Auto => default,
Self::Custom(x) => x,
}
}
}
impl<T> Default for Smart<T> {
fn default() -> Self {
Self::Auto
}
}
impl<T> Cast<Value> for Option<T>
where
T: Cast<Value>,
{
fn is(value: &Value) -> bool {
matches!(value, Value::None) || T::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::None => Ok(None),
v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")),
}
}
}
impl<T> Cast<Value> for Smart<T>
where
T: Cast<Value>,
{
fn is(value: &Value) -> bool {
matches!(value, Value::Auto) || T::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::Auto => Ok(Self::Auto),
v => T::cast(v)
.map(Self::Custom)
.map_err(|msg| with_alternative(msg, "auto")),
}
}
}
/// Transform `expected X, found Y` into `expected X or A, found Y`.
fn with_alternative(msg: String, alt: &str) -> String {
let mut parts = msg.split(", found ");
if let (Some(a), Some(b)) = (parts.next(), parts.next()) {
format!("{} or {}, found {}", a, alt, b)
} else {
msg
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -17,7 +17,7 @@ pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
} }
fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> { fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
let stroke = args.named("stroke")?.or_else(|| args.find()).map(Paint::Solid); let stroke = args.named("stroke")?.or_else(|| args.find());
let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find()); let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
let offset = args.named("offset")?; let offset = args.named("offset")?;
let extent = args.named("extent")?.unwrap_or_default(); let extent = args.named("extent")?.unwrap_or_default();

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, Template, Value}; pub use crate::eval::{Args, EvalContext, Smart, Template, 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::*;
@ -144,3 +144,9 @@ dynamic! {
FontFamily: "font family", FontFamily: "font family",
Value::Str(string) => Self::Named(string.to_lowercase()), Value::Str(string) => Self::Named(string.to_lowercase()),
} }
castable! {
Paint,
Expected: "color",
Value::Color(color) => Paint::Solid(color),
}

View File

@ -12,13 +12,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let paper = args.named::<Paper>("paper")?.or_else(|| args.find()); let paper = args.named::<Paper>("paper")?.or_else(|| args.find());
let width = args.named("width")?; let width = args.named("width")?;
let height = args.named("height")?; let height = args.named("height")?;
let flip = args.named("flip")?;
let margins = args.named("margins")?; let margins = args.named("margins")?;
let left = args.named("left")?; let left = args.named("left")?;
let top = args.named("top")?; let top = args.named("top")?;
let right = args.named("right")?; let right = args.named("right")?;
let bottom = args.named("bottom")?; let bottom = args.named("bottom")?;
let flip = args.named("flip")?; let fill = args.named("fill")?;
let fill = args.named("fill")?.map(Paint::Solid);
ctx.template.modify(move |style| { ctx.template.modify(move |style| {
let page = style.page_mut(); let page = style.page_mut();
@ -33,37 +33,37 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
page.size.w = width; page.size.w = width;
} }
if flip.unwrap_or(false) {
std::mem::swap(&mut page.size.w, &mut page.size.h);
}
if let Some(height) = height { if let Some(height) = height {
page.class = PaperClass::Custom; page.class = PaperClass::Custom;
page.size.h = height; page.size.h = height;
} }
if let Some(margins) = margins { if let Some(margins) = margins {
page.margins = Sides::splat(Some(margins)); page.margins = Sides::splat(margins);
} }
if let Some(left) = left { if let Some(left) = left {
page.margins.left = Some(left); page.margins.left = left;
} }
if let Some(top) = top { if let Some(top) = top {
page.margins.top = Some(top); page.margins.top = top;
} }
if let Some(right) = right { if let Some(right) = right {
page.margins.right = Some(right); page.margins.right = right;
} }
if let Some(bottom) = bottom { if let Some(bottom) = bottom {
page.margins.bottom = Some(bottom); page.margins.bottom = bottom;
}
if flip.unwrap_or(false) {
std::mem::swap(&mut page.size.w, &mut page.size.h);
} }
if let Some(fill) = fill { if let Some(fill) = fill {
page.fill = Some(fill); page.fill = fill;
} }
}); });

View File

@ -58,11 +58,11 @@ fn shape_impl(
}; };
// Parse fill & stroke. // Parse fill & stroke.
let fill = args.named("fill")?.map(Paint::Solid); let fill = args.named("fill")?.unwrap_or(None);
let stroke = match (args.named("stroke")?, args.named("thickness")?) { let stroke = match (args.named("stroke")?, args.named("thickness")?) {
(None, None) => fill.is_none().then(|| default), (None, None) => fill.is_none().then(|| default),
(color, thickness) => Some(Stroke { (color, thickness) => color.unwrap_or(Some(default.paint)).map(|paint| Stroke {
paint: color.map(Paint::Solid).unwrap_or(default.paint), paint,
thickness: thickness.unwrap_or(default.thickness), thickness: thickness.unwrap_or(default.thickness),
}), }),
}; };

View File

@ -93,18 +93,16 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
castable! { castable! {
StylisticSet, StylisticSet,
Expected: "none or integer", Expected: "integer",
Value::None => Self(None),
Value::Int(v) => match v { Value::Int(v) => match v {
1 ..= 20 => Self(Some(v as u8)), 1 ..= 20 => Self::new(v as u8),
_ => Err("must be between 1 and 20")?, _ => Err("must be between 1 and 20")?,
}, },
} }
castable! { castable! {
NumberType, NumberType,
Expected: "auto or string", Expected: "string",
Value::Auto => Self::Auto,
Value::Str(string) => match string.as_str() { Value::Str(string) => match string.as_str() {
"lining" => Self::Lining, "lining" => Self::Lining,
"old-style" => Self::OldStyle, "old-style" => Self::OldStyle,
@ -114,8 +112,7 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
castable! { castable! {
NumberWidth, NumberWidth,
Expected: "auto or string", Expected: "string",
Value::Auto => Self::Auto,
Value::Str(string) => match string.as_str() { Value::Str(string) => match string.as_str() {
"proportional" => Self::Proportional, "proportional" => Self::Proportional,
"tabular" => Self::Tabular, "tabular" => Self::Tabular,
@ -629,8 +626,8 @@ fn tags(features: &FontFeatures) -> Vec<Feature> {
} }
let storage; let storage;
if let StylisticSet(Some(set @ 1 ..= 20)) = features.stylistic_set { if let Some(set) = features.stylistic_set {
storage = [b's', b's', b'0' + set / 10, b'0' + set % 10]; storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
feat(&storage, 1); feat(&storage, 1);
} }
@ -648,15 +645,15 @@ fn tags(features: &FontFeatures) -> Vec<Feature> {
} }
match features.numbers.type_ { match features.numbers.type_ {
NumberType::Auto => {} Smart::Auto => {}
NumberType::Lining => feat(b"lnum", 1), Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
NumberType::OldStyle => feat(b"onum", 1), Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
} }
match features.numbers.width { match features.numbers.width {
NumberWidth::Auto => {} Smart::Auto => {}
NumberWidth::Proportional => feat(b"pnum", 1), Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
NumberWidth::Tabular => feat(b"tnum", 1), Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
} }
match features.numbers.position { match features.numbers.position {

View File

@ -9,6 +9,7 @@ use std::rc::Rc;
use ttf_parser::Tag; use ttf_parser::Tag;
use crate::eval::Smart;
use crate::font::*; use crate::font::*;
use crate::geom::*; use crate::geom::*;
use crate::util::EcoString; use crate::util::EcoString;
@ -70,7 +71,7 @@ pub struct PageStyle {
pub size: Size, pub size: Size,
/// The amount of white space on each side of the page. If a side is set to /// 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. /// `None`, the default for the paper class is used.
pub margins: Sides<Option<Linear>>, pub margins: Sides<Smart<Linear>>,
/// The background fill of the page. /// The background fill of the page.
pub fill: Option<Paint>, pub fill: Option<Paint>,
} }
@ -94,7 +95,7 @@ impl Default for PageStyle {
Self { Self {
class: paper.class(), class: paper.class(),
size: paper.size(), size: paper.size(),
margins: Sides::splat(None), margins: Sides::splat(Smart::Auto),
fill: None, fill: None,
} }
} }
@ -301,7 +302,7 @@ pub struct FontFeatures {
/// Whether to apply stylistic alternates. ("salt") /// Whether to apply stylistic alternates. ("salt")
pub alternates: bool, pub alternates: bool,
/// Which stylistic set to apply. ("ss01" - "ss20") /// Which stylistic set to apply. ("ss01" - "ss20")
pub stylistic_set: StylisticSet, pub stylistic_set: Option<StylisticSet>,
/// Configuration of ligature features. /// Configuration of ligature features.
pub ligatures: LigatureFeatures, pub ligatures: LigatureFeatures,
/// Configuration of numbers features. /// Configuration of numbers features.
@ -316,7 +317,7 @@ impl Default for FontFeatures {
kerning: true, kerning: true,
smallcaps: false, smallcaps: false,
alternates: false, alternates: false,
stylistic_set: StylisticSet::default(), stylistic_set: None,
ligatures: LigatureFeatures::default(), ligatures: LigatureFeatures::default(),
numbers: NumberFeatures::default(), numbers: NumberFeatures::default(),
raw: vec![], raw: vec![],
@ -326,11 +327,17 @@ impl Default for FontFeatures {
/// A stylistic set in a font face. /// A stylistic set in a font face.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StylisticSet(pub Option<u8>); pub struct StylisticSet(u8);
impl Default for StylisticSet { impl StylisticSet {
fn default() -> Self { /// Creates a new set, clamping to 1-20.
Self(None) 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
} }
} }
@ -359,9 +366,9 @@ impl Default for LigatureFeatures {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct NumberFeatures { pub struct NumberFeatures {
/// Whether to use lining or old-style numbers. /// Whether to use lining or old-style numbers.
pub type_: NumberType, pub type_: Smart<NumberType>,
/// Whether to use proportional or tabular numbers. /// Whether to use proportional or tabular numbers.
pub width: NumberWidth, pub width: Smart<NumberWidth>,
/// How to position numbers vertically. /// How to position numbers vertically.
pub position: NumberPosition, pub position: NumberPosition,
/// Whether to have a slash through the zero glyph. ("zero") /// Whether to have a slash through the zero glyph. ("zero")
@ -373,8 +380,8 @@ pub struct NumberFeatures {
impl Default for NumberFeatures { impl Default for NumberFeatures {
fn default() -> Self { fn default() -> Self {
Self { Self {
type_: NumberType::Auto, type_: Smart::Auto,
width: NumberWidth::Auto, width: Smart::Auto,
position: NumberPosition::Normal, position: NumberPosition::Normal,
slashed_zero: false, slashed_zero: false,
fractions: false, fractions: false,
@ -385,8 +392,6 @@ impl Default for NumberFeatures {
/// Which kind of numbers / figures to select. /// Which kind of numbers / figures to select.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum NumberType { pub enum NumberType {
/// Select the font's preference.
Auto,
/// Numbers that fit well with capital text. ("lnum") /// Numbers that fit well with capital text. ("lnum")
Lining, Lining,
/// Numbers that fit well into flow of upper- and lowercase text. ("onum") /// Numbers that fit well into flow of upper- and lowercase text. ("onum")
@ -396,8 +401,6 @@ pub enum NumberType {
/// The width of numbers / figures. /// The width of numbers / figures.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum NumberWidth { pub enum NumberWidth {
/// Select the font's preference.
Auto,
/// Number widths are glyph specific. ("pnum") /// Number widths are glyph specific. ("pnum")
Proportional, Proportional,
/// All numbers are of equal width / monospaced. ("tnum") /// All numbers are of equal width / monospaced. ("tnum")

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,26 @@
// Test shape fill & stroke.
---
#let rect with (width: 20pt, height: 10pt)
#let items = for i, rect in (
rect(stroke: none),
rect(),
rect(fill: none),
rect(thickness: 2pt),
rect(stroke: eastern),
rect(stroke: eastern, thickness: 2pt),
rect(fill: eastern),
rect(fill: eastern, stroke: none),
rect(fill: forest, stroke: none, thickness: 2pt),
rect(fill: forest, stroke: conifer),
rect(fill: forest, thickness: 2pt),
rect(fill: forest, stroke: conifer, thickness: 2pt),
) {
(align(vertical: center)[{i + 1}.], rect, [])
}
#grid(
columns: (auto, auto, 1fr, auto, auto, 0fr),
gutter: 5pt,
..items,
)

View File

@ -26,21 +26,9 @@
// Flipped predefined paper. // Flipped predefined paper.
[#page(paper: "a11", flip: true) Flipped A11] [#page(paper: "a11", flip: true) Flipped A11]
---
// Test a combination of pages with bodies and normal content.
#page(width: 80pt, height: 30pt)
[#page() First]
[#page() Second]
#pagebreak()
#pagebreak()
Fourth
[#page(height: 25pt)]
Sixth
[#page() Seventh]
--- ---
#page(width: 80pt, height: 40pt, fill: eastern) #page(width: 80pt, height: 40pt, fill: eastern)
#font(15pt, "Roboto", fill: white, smallcaps: true) #font(15pt, "Roboto", fill: white, smallcaps: true)[Typst]
Typst
#page(width: 40pt, fill: none, margins: auto, top: 10pt)
Hi

View File

@ -18,3 +18,17 @@ C
// No consequences from the page("A4") call here. // No consequences from the page("A4") call here.
#pagebreak() #pagebreak()
D D
---
// Test a combination of pages with bodies and normal content.
#page(width: 80pt, height: 30pt)
[#page() First]
[#page() Second]
#pagebreak()
#pagebreak()
Fourth
[#page(height: 25pt)]
Sixth
[#page() Seventh]

View File

@ -52,10 +52,18 @@ fi vs. #font(ligatures: false)[No fi] \
#font(features: ("smcp",))[Smcp] \ #font(features: ("smcp",))[Smcp] \
fi vs. #font(features: (liga: 0))[No fi] fi vs. #font(features: (liga: 0))[No fi]
---
// Error: 22-27 expected integer or none, found boolean
#font(stylistic-set: false)
--- ---
// Error: 22-24 must be between 1 and 20 // Error: 22-24 must be between 1 and 20
#font(stylistic-set: 25) #font(stylistic-set: 25)
---
// Error: 20-21 expected string or auto, found integer
#font(number-type: 2)
--- ---
// Error: 20-31 expected "lining" or "old-style" // Error: 20-31 expected "lining" or "old-style"
#font(number-type: "different") #font(number-type: "different")

View File

@ -10,7 +10,7 @@ use ttf_parser::{GlyphId, OutlineBuilder};
use walkdir::WalkDir; use walkdir::WalkDir;
use typst::diag::Error; use typst::diag::Error;
use typst::eval::Value; use typst::eval::{Smart, Value};
use typst::font::Face; use typst::font::Face;
use typst::frame::{Element, Frame, Geometry, Shape, Stroke, Text}; use typst::frame::{Element, Frame, Geometry, Shape, Stroke, Text};
use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size}; use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size};
@ -64,7 +64,7 @@ fn main() {
// large and fit them to match their content. // large and fit them to match their content.
let mut style = Style::default(); let mut style = Style::default();
style.page_mut().size = Size::new(Length::pt(120.0), Length::inf()); style.page_mut().size = Size::new(Length::pt(120.0), Length::inf());
style.page_mut().margins = Sides::splat(Some(Length::pt(10.0).into())); style.page_mut().margins = Sides::splat(Smart::Custom(Length::pt(10.0).into()));
style.text_mut().size = Length::pt(10.0); style.text_mut().size = Length::pt(10.0);
// Hook up an assert function into the global scope. // Hook up an assert function into the global scope.