Accept closures for heading styling

This commit is contained in:
Laurenz 2022-02-17 18:12:13 +01:00
parent 35610a8c6a
commit 261f387535
6 changed files with 140 additions and 35 deletions

View File

@ -108,3 +108,13 @@ impl<T> Trace<T> for TypResult<T> {
}) })
} }
} }
/// Transform `expected X, found Y` into `expected X or A, found Y`.
pub 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
}
}

View File

@ -531,7 +531,7 @@ impl<T: Cast> Cast for Smart<T> {
} }
/// Transform `expected X, found Y` into `expected X or A, found Y`. /// Transform `expected X, found Y` into `expected X or A, found Y`.
fn with_alternative(msg: String, alt: &str) -> String { pub fn with_alternative(msg: String, alt: &str) -> String {
let mut parts = msg.split(", found "); let mut parts = msg.split(", found ");
if let (Some(a), Some(b)) = (parts.next(), parts.next()) { if let (Some(a), Some(b)) = (parts.next(), parts.next()) {
format!("{} or {}, found {}", a, alt, b) format!("{} or {}, found {}", a, alt, b)

View File

@ -15,24 +15,25 @@ pub struct HeadingNode {
#[class] #[class]
impl HeadingNode { impl HeadingNode {
/// The heading's font family. /// The heading's font family. Just the normal text family if `auto`.
pub const FAMILY: Smart<FontFamily> = Smart::Auto; pub const FAMILY: Leveled<Smart<FontFamily>> = Leveled::Value(Smart::Auto);
/// The size of text in the heading. Just the surrounding text size if /// The color of text in the heading. Just the normal text color if `auto`.
/// `auto`. pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto);
pub const SIZE: Smart<Linear> = Smart::Auto; /// The size of text in the heading.
/// The fill color of text in the heading. Just the surrounding text color pub const SIZE: Leveled<Linear> = Leveled::Mapping(|level| {
/// if `auto`. let upscale = (1.6 - 0.1 * level as f64).max(0.75);
pub const FILL: Smart<Paint> = Smart::Auto; Relative::new(upscale).into()
});
/// Whether text in the heading is strengthend. /// Whether text in the heading is strengthend.
pub const STRONG: bool = true; pub const STRONG: Leveled<bool> = Leveled::Value(true);
/// Whether text in the heading is emphasized. /// Whether text in the heading is emphasized.
pub const EMPH: bool = false; pub const EMPH: Leveled<bool> = Leveled::Value(false);
/// Whether the heading is underlined. /// Whether the heading is underlined.
pub const UNDERLINE: bool = false; pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
/// The extra padding above the heading. /// The extra padding above the heading.
pub const ABOVE: Length = Length::zero(); pub const ABOVE: Leveled<Length> = Leveled::Value(Length::zero());
/// The extra padding below the heading. /// The extra padding below the heading.
pub const BELOW: Length = Length::zero(); pub const BELOW: Leveled<Length> = Leveled::Value(Length::zero());
fn construct(_: &mut Vm, args: &mut Args) -> TypResult<Template> { fn construct(_: &mut Vm, args: &mut Args) -> TypResult<Template> {
Ok(Template::show(Self { Ok(Template::show(Self {
@ -43,52 +44,51 @@ impl HeadingNode {
} }
impl Show for HeadingNode { impl Show for HeadingNode {
fn show(&self, _: &mut Vm, styles: StyleChain) -> TypResult<Template> { fn show(&self, vm: &mut Vm, styles: StyleChain) -> TypResult<Template> {
macro_rules! resolve {
($key:expr) => {
styles.get_cloned($key).resolve(vm, self.level)?
};
}
let mut map = StyleMap::new(); let mut map = StyleMap::new();
map.set(TextNode::SIZE, resolve!(Self::SIZE));
let upscale = (1.6 - 0.1 * self.level as f64).max(0.75); if let Smart::Custom(family) = resolve!(Self::FAMILY) {
map.set(
TextNode::SIZE,
styles.get(Self::SIZE).unwrap_or(Relative::new(upscale).into()),
);
if let Smart::Custom(family) = styles.get_ref(Self::FAMILY) {
map.set( map.set(
TextNode::FAMILY, TextNode::FAMILY,
std::iter::once(family) std::iter::once(family)
.chain(styles.get_ref(TextNode::FAMILY)) .chain(styles.get_ref(TextNode::FAMILY).iter().cloned())
.cloned()
.collect(), .collect(),
); );
} }
if let Smart::Custom(fill) = styles.get(Self::FILL) { if let Smart::Custom(fill) = resolve!(Self::FILL) {
map.set(TextNode::FILL, fill); map.set(TextNode::FILL, fill);
} }
if styles.get(Self::STRONG) { if resolve!(Self::STRONG) {
map.set(TextNode::STRONG, true); map.set(TextNode::STRONG, true);
} }
if styles.get(Self::EMPH) { if resolve!(Self::EMPH) {
map.set(TextNode::EMPH, true); map.set(TextNode::EMPH, true);
} }
let mut seq = vec![];
let mut body = self.body.clone(); let mut body = self.body.clone();
if styles.get(Self::UNDERLINE) { if resolve!(Self::UNDERLINE) {
body = body.underlined(); body = body.underlined();
} }
let mut seq = vec![]; let above = resolve!(Self::ABOVE);
let above = styles.get(Self::ABOVE);
if !above.is_zero() { if !above.is_zero() {
seq.push(Template::Vertical(above.into())); seq.push(Template::Vertical(above.into()));
} }
seq.push(body); seq.push(body);
let below = styles.get(Self::BELOW); let below = resolve!(Self::BELOW);
if !below.is_zero() { if !below.is_zero() {
seq.push(Template::Vertical(below.into())); seq.push(Template::Vertical(below.into()));
} }
@ -98,3 +98,50 @@ impl Show for HeadingNode {
)) ))
} }
} }
/// Either the value or a closure mapping to the value.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Leveled<T> {
/// A bare value.
Value(T),
/// A simple mapping from a heading level to a value.
Mapping(fn(usize) -> T),
/// A closure mapping from a heading level to a value.
Func(Func, Span),
}
impl<T: Cast> Leveled<T> {
/// Resolve the value based on the level.
pub fn resolve(self, vm: &mut Vm, level: usize) -> TypResult<T> {
match self {
Self::Value(value) => Ok(value),
Self::Mapping(mapping) => Ok(mapping(level)),
Self::Func(func, span) => {
let args = Args {
span,
items: vec![Arg {
span,
name: None,
value: Spanned::new(Value::Int(level as i64), span),
}],
};
func.call(vm, args)?.cast().at(span)
}
}
}
}
impl<T: Cast> Cast<Spanned<Value>> for Leveled<T> {
fn is(value: &Spanned<Value>) -> bool {
matches!(&value.v, Value::Func(_)) || T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
match value.v {
Value::Func(f) => Ok(Self::Func(f, value.span)),
v => T::cast(v)
.map(Self::Value)
.map_err(|msg| with_alternative(msg, "function")),
}
}
}

View File

@ -62,10 +62,10 @@ pub mod prelude {
pub use typst_macros::class; pub use typst_macros::class;
pub use crate::diag::{At, TypResult}; pub use crate::diag::{with_alternative, At, StrResult, TypResult};
pub use crate::eval::{ pub use crate::eval::{
Args, Construct, Merge, Property, Scope, Set, Show, ShowNode, Smart, StyleChain, Arg, Args, Cast, Construct, Func, Merge, Property, Scope, Set, Show, ShowNode,
StyleMap, StyleVec, Template, Value, Smart, StyleChain, StyleMap, StyleVec, Template, Value,
}; };
pub use crate::frame::*; pub use crate::frame::*;
pub use crate::geom::*; pub use crate::geom::*;

BIN
tests/ref/style/closure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,48 @@
// Test styles with closure.
---
#set heading(
size: 10pt,
fill: lvl => if even(lvl) { red } else { blue },
)
= Heading 1
== Heading 2
=== Heading 3
==== Heading 4
---
// Test in constructor.
#heading(
level: 3,
size: 10pt,
strong: lvl => {
assert(lvl == 3)
false
}
)[Level 3]
---
// Error: 22-26 expected font family or auto or function, found length
#set heading(family: 10pt)
= Heading
---
// Error: 29-38 cannot add integer and string
#set heading(strong: lvl => lvl + "2")
= Heading
---
// Error: 22-34 expected font family or auto, found boolean
#set heading(family: lvl => false)
= Heading
---
// Error: 22-37 missing argument: b
#set heading(family: (a, b) => a + b)
= Heading
---
// Error: 22-30 unexpected argument
#set heading(family: () => {})
= Heading