mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Castable optional and smart values
This commit is contained in:
parent
cef46e6c40
commit
ed50661378
@ -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::*;
|
||||
|
@ -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();
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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),
|
||||
}),
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
BIN
tests/ref/elements/fill-stroke.png
Normal file
BIN
tests/ref/elements/fill-stroke.png
Normal file
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 |
26
tests/typ/elements/fill-stroke.typ
Normal file
26
tests/typ/elements/fill-stroke.typ
Normal 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,
|
||||
)
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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")
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user