diff --git a/src/eval/value.rs b/src/eval/value.rs index e224438ac..dec5c6c0e 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -252,43 +252,6 @@ pub trait Cast: Sized { fn cast(value: V) -> StrResult; } -impl Cast for Value { - fn is(_: &Value) -> bool { - true - } - - fn cast(value: Value) -> StrResult { - Ok(value) - } -} - -impl Cast> for T -where - T: Cast, -{ - fn is(value: &Spanned) -> bool { - T::is(&value.v) - } - - fn cast(value: Spanned) -> StrResult { - T::cast(value.v) - } -} - -impl Cast> for Spanned -where - T: Cast, -{ - fn is(value: &Spanned) -> bool { - T::is(&value.v) - } - - fn cast(value: Spanned) -> StrResult { - 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 for Value { + fn is(_: &Value) -> bool { + true + } + + fn cast(value: Value) -> StrResult { + Ok(value) + } +} + +impl Cast> for T +where + T: Cast, +{ + fn is(value: &Spanned) -> bool { + T::is(&value.v) + } + + fn cast(value: Spanned) -> StrResult { + T::cast(value.v) + } +} + +impl Cast> for Spanned +where + T: Cast, +{ + fn is(value: &Spanned) -> bool { + T::is(&value.v) + } + + fn cast(value: Spanned) -> StrResult { + 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 { + /// The value should be determined smartly based on the + /// circumstances. + Auto, + /// A forced, specific value. + Custom(T), +} + +impl Smart { + /// 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 Default for Smart { + fn default() -> Self { + Self::Auto + } +} + +impl Cast for Option +where + T: Cast, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::None) || T::is(value) + } + + fn cast(value: Value) -> StrResult { + match value { + Value::None => Ok(None), + v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")), + } + } +} + +impl Cast for Smart +where + T: Cast, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::Auto) || T::is(value) + } + + fn cast(value: Value) -> StrResult { + 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::*; diff --git a/src/library/deco.rs b/src/library/deco.rs index 1f8c051f2..cb0656892 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -17,7 +17,7 @@ pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult { } fn line_impl(args: &mut Args, kind: LineKind) -> TypResult { - 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::("thickness")?.or_else(|| args.find()); let offset = args.named("offset")?; let extent = args.named("extent")?.unwrap_or_default(); diff --git a/src/library/mod.rs b/src/library/mod.rs index 7b8acf9ec..6260e6fcc 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -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), +} diff --git a/src/library/page.rs b/src/library/page.rs index 20871bd9b..b256a5211 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -12,13 +12,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult { let paper = args.named::("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 { 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; } }); diff --git a/src/library/shape.rs b/src/library/shape.rs index abf927e46..f47da82f7 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -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), }), }; diff --git a/src/library/text.rs b/src/library/text.rs index d0b5c8e6c..c0ee80e15 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -93,18 +93,16 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult { 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 { 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 { } 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 { } 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 { diff --git a/src/style/mod.rs b/src/style/mod.rs index 4a8830f82..45dbeb546 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -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>, + pub margins: Sides>, /// The background fill of the page. pub fill: Option, } @@ -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, /// 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); +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, /// Whether to use proportional or tabular numbers. - pub width: NumberWidth, + pub width: Smart, /// 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") diff --git a/tests/ref/elements/fill-stroke.png b/tests/ref/elements/fill-stroke.png new file mode 100644 index 000000000..2d04b3dd6 Binary files /dev/null and b/tests/ref/elements/fill-stroke.png differ diff --git a/tests/ref/layout/page.png b/tests/ref/layout/page.png index dae3d4882..57e4b8f1a 100644 Binary files a/tests/ref/layout/page.png and b/tests/ref/layout/page.png differ diff --git a/tests/ref/layout/pagebreak.png b/tests/ref/layout/pagebreak.png index 043b50864..fbb1d00c5 100644 Binary files a/tests/ref/layout/pagebreak.png and b/tests/ref/layout/pagebreak.png differ diff --git a/tests/typ/elements/fill-stroke.typ b/tests/typ/elements/fill-stroke.typ new file mode 100644 index 000000000..b8f2b4bc1 --- /dev/null +++ b/tests/typ/elements/fill-stroke.typ @@ -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, +) diff --git a/tests/typ/layout/page.typ b/tests/typ/layout/page.typ index 9bb3097d7..1f707327e 100644 --- a/tests/typ/layout/page.typ +++ b/tests/typ/layout/page.typ @@ -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 diff --git a/tests/typ/layout/pagebreak.typ b/tests/typ/layout/pagebreak.typ index ab591c872..f9a935bc7 100644 --- a/tests/typ/layout/pagebreak.typ +++ b/tests/typ/layout/pagebreak.typ @@ -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] diff --git a/tests/typ/text/features.typ b/tests/typ/text/features.typ index fc84514b2..d60583d8c 100644 --- a/tests/typ/text/features.typ +++ b/tests/typ/text/features.typ @@ -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") diff --git a/tests/typeset.rs b/tests/typeset.rs index 723df4a84..6a21cf7b4 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -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.