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`.
fn with_alternative(msg: String, alt: &str) -> String {
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)

View File

@ -15,24 +15,25 @@ pub struct HeadingNode {
#[class]
impl HeadingNode {
/// The heading's font family.
pub const FAMILY: Smart<FontFamily> = Smart::Auto;
/// The size of text in the heading. Just the surrounding text size if
/// `auto`.
pub const SIZE: Smart<Linear> = Smart::Auto;
/// The fill color of text in the heading. Just the surrounding text color
/// if `auto`.
pub const FILL: Smart<Paint> = Smart::Auto;
/// The heading's font family. Just the normal text family if `auto`.
pub const FAMILY: Leveled<Smart<FontFamily>> = Leveled::Value(Smart::Auto);
/// The color of text in the heading. Just the normal text color if `auto`.
pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto);
/// The size of text in the heading.
pub const SIZE: Leveled<Linear> = Leveled::Mapping(|level| {
let upscale = (1.6 - 0.1 * level as f64).max(0.75);
Relative::new(upscale).into()
});
/// 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.
pub const EMPH: bool = false;
pub const EMPH: Leveled<bool> = Leveled::Value(false);
/// Whether the heading is underlined.
pub const UNDERLINE: bool = false;
pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
/// 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.
pub const BELOW: Length = Length::zero();
pub const BELOW: Leveled<Length> = Leveled::Value(Length::zero());
fn construct(_: &mut Vm, args: &mut Args) -> TypResult<Template> {
Ok(Template::show(Self {
@ -43,52 +44,51 @@ impl 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();
map.set(TextNode::SIZE, resolve!(Self::SIZE));
let upscale = (1.6 - 0.1 * self.level as f64).max(0.75);
map.set(
TextNode::SIZE,
styles.get(Self::SIZE).unwrap_or(Relative::new(upscale).into()),
);
if let Smart::Custom(family) = styles.get_ref(Self::FAMILY) {
if let Smart::Custom(family) = resolve!(Self::FAMILY) {
map.set(
TextNode::FAMILY,
std::iter::once(family)
.chain(styles.get_ref(TextNode::FAMILY))
.cloned()
.chain(styles.get_ref(TextNode::FAMILY).iter().cloned())
.collect(),
);
}
if let Smart::Custom(fill) = styles.get(Self::FILL) {
if let Smart::Custom(fill) = resolve!(Self::FILL) {
map.set(TextNode::FILL, fill);
}
if styles.get(Self::STRONG) {
if resolve!(Self::STRONG) {
map.set(TextNode::STRONG, true);
}
if styles.get(Self::EMPH) {
if resolve!(Self::EMPH) {
map.set(TextNode::EMPH, true);
}
let mut seq = vec![];
let mut body = self.body.clone();
if styles.get(Self::UNDERLINE) {
if resolve!(Self::UNDERLINE) {
body = body.underlined();
}
let mut seq = vec![];
let above = styles.get(Self::ABOVE);
let above = resolve!(Self::ABOVE);
if !above.is_zero() {
seq.push(Template::Vertical(above.into()));
}
seq.push(body);
let below = styles.get(Self::BELOW);
let below = resolve!(Self::BELOW);
if !below.is_zero() {
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 crate::diag::{At, TypResult};
pub use crate::diag::{with_alternative, At, StrResult, TypResult};
pub use crate::eval::{
Args, Construct, Merge, Property, Scope, Set, Show, ShowNode, Smart, StyleChain,
StyleMap, StyleVec, Template, Value,
Arg, Args, Cast, Construct, Func, Merge, Property, Scope, Set, Show, ShowNode,
Smart, StyleChain, StyleMap, StyleVec, Template, Value,
};
pub use crate::frame::*;
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