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>;
}
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.
macro_rules! primitive {
(
@ -400,6 +363,113 @@ primitive! { Dict: "dictionary", Dict }
primitive! { Template: "template", Template }
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)]
mod tests {
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> {
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 offset = args.named("offset")?;
let extent = args.named("extent")?.unwrap_or_default();

View File

@ -26,7 +26,7 @@ mod prelude {
pub use std::rc::Rc;
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::geom::*;
pub use crate::layout::*;
@ -144,3 +144,9 @@ dynamic! {
FontFamily: "font family",
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 width = args.named("width")?;
let height = args.named("height")?;
let flip = args.named("flip")?;
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 flip = args.named("flip")?;
let fill = args.named("fill")?.map(Paint::Solid);
let fill = args.named("fill")?;
ctx.template.modify(move |style| {
let page = style.page_mut();
@ -33,37 +33,37 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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 {
page.class = PaperClass::Custom;
page.size.h = height;
}
if let Some(margins) = margins {
page.margins = Sides::splat(Some(margins));
page.margins = Sides::splat(margins);
}
if let Some(left) = left {
page.margins.left = Some(left);
page.margins.left = left;
}
if let Some(top) = top {
page.margins.top = Some(top);
page.margins.top = top;
}
if let Some(right) = right {
page.margins.right = Some(right);
page.margins.right = right;
}
if let Some(bottom) = bottom {
page.margins.bottom = Some(bottom);
}
if flip.unwrap_or(false) {
std::mem::swap(&mut page.size.w, &mut page.size.h);
page.margins.bottom = bottom;
}
if let Some(fill) = fill {
page.fill = Some(fill);
page.fill = fill;
}
});

View File

@ -58,11 +58,11 @@ fn shape_impl(
};
// 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")?) {
(None, None) => fill.is_none().then(|| default),
(color, thickness) => Some(Stroke {
paint: color.map(Paint::Solid).unwrap_or(default.paint),
(color, thickness) => color.unwrap_or(Some(default.paint)).map(|paint| Stroke {
paint,
thickness: thickness.unwrap_or(default.thickness),
}),
};

View File

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

View File

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

View File

@ -10,7 +10,7 @@ use ttf_parser::{GlyphId, OutlineBuilder};
use walkdir::WalkDir;
use typst::diag::Error;
use typst::eval::Value;
use typst::eval::{Smart, Value};
use typst::font::Face;
use typst::frame::{Element, Frame, Geometry, Shape, Stroke, Text};
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.
let mut style = Style::default();
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);
// Hook up an assert function into the global scope.