diff --git a/library/src/compute/construct.rs b/library/src/compute/construct.rs index 784513f64..176409dd7 100644 --- a/library/src/compute/construct.rs +++ b/library/src/compute/construct.rs @@ -240,6 +240,67 @@ castable! { }, } +/// # Symbol +/// Create a custom symbol with modifiers. +/// +/// ## Example +/// ``` +/// #let envelope = symbol( +/// "🖂", +/// ("stamped", "🖃"), +/// ("stamped.pen", "🖆"), +/// ("lightning", "🖄"), +/// ("fly", "🖅"), +/// ) +/// +/// #envelope +/// #envelope.stamped +/// #envelope.stamped.pen +/// #envelope.lightning +/// #envelope.fly +/// ``` +/// +/// ## Parameters +/// - variants: Variant (positional, variadic) +/// The variants of the symbol. +/// +/// Can be a just a string consisting of a single character for the +/// modifierless variant or an array with two strings specifying the modifiers +/// and the symbol. Individual modifiers should be separated by dots. When +/// displaying a symbol, Typst selects the first from the variants that have +/// all attached modifiers and the minimum number of other modifiers. +/// +/// - returns: symbol +/// +/// ## Category +/// construct +#[func] +pub fn symbol(args: &mut Args) -> SourceResult { + let mut list: Vec<(EcoString, char)> = vec![]; + for Spanned { v, span } in args.all::>()? { + if list.iter().any(|(prev, _)| &v.0 == prev) { + bail!(span, "duplicate variant"); + } + list.push((v.0, v.1)); + } + Ok(Value::Symbol(Symbol::runtime(list))) +} + +/// A value that can be cast to a symbol. +struct Variant(EcoString, char); + +castable! { + Variant, + c: char => Self(EcoString::new(), c), + array: Array => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?), + _ => Err("point array must contain exactly two entries")?, + } + }, +} + /// # String /// Convert a value to a string. /// diff --git a/library/src/lib.rs b/library/src/lib.rs index 41c621cba..08ff171a9 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -101,6 +101,7 @@ fn global(sym: Module, math: Module) -> Module { global.def_func::("luma"); global.def_func::("rgb"); global.def_func::("cmyk"); + global.def_func::("symbol"); global.def_func::("str"); global.def_func::("label"); global.def_func::("regex"); diff --git a/src/model/symbol.rs b/src/model/symbol.rs index 146f7502a..435048acf 100644 --- a/src/model/symbol.rs +++ b/src/model/symbol.rs @@ -1,6 +1,7 @@ use std::cmp::Reverse; use std::collections::BTreeSet; use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::sync::Arc; use crate::diag::StrResult; use crate::util::EcoString; @@ -38,7 +39,8 @@ pub struct Symbol { #[derive(Clone, Eq, PartialEq, Hash)] enum Repr { Single(char), - List(&'static [(&'static str, char)]), + Static(&'static [(&'static str, char)]), + Runtime(Arc>), } impl Symbol { @@ -47,12 +49,22 @@ impl Symbol { Self { repr: Repr::Single(c), modifiers: EcoString::new() } } - /// Create a symbol with variants. + /// Create a symbol with a static variant list. #[track_caller] pub fn list(list: &'static [(&'static str, char)]) -> Self { debug_assert!(!list.is_empty()); Self { - repr: Repr::List(list), + repr: Repr::Static(list), + modifiers: EcoString::new(), + } + } + + /// Create a symbol with a runtime variant list. + #[track_caller] + pub fn runtime(list: Vec<(EcoString, char)>) -> Self { + debug_assert!(!list.is_empty()); + Self { + repr: Repr::Runtime(Arc::new(list)), modifiers: EcoString::new(), } } @@ -61,7 +73,7 @@ impl Symbol { pub fn get(&self) -> char { match self.repr { Repr::Single(c) => c, - Repr::List(list) => find(list, &self.modifiers).unwrap(), + _ => find(self.variants(), &self.modifiers).unwrap(), } } @@ -71,10 +83,7 @@ impl Symbol { self.modifiers.push('.'); } self.modifiers.push_str(modifier); - if match self.repr { - Repr::Single(_) => true, - Repr::List(list) => find(list, &self.modifiers).is_none(), - } { + if find(self.variants(), &self.modifiers).is_none() { Err("unknown modifier")? } Ok(self) @@ -82,21 +91,19 @@ impl Symbol { /// The characters that are covered by this symbol. pub fn variants(&self) -> impl Iterator { - let (first, slice) = match self.repr { - Repr::Single(c) => (Some(("", c)), [].as_slice()), - Repr::List(list) => (None, list), - }; - first.into_iter().chain(slice.iter().copied()) + match &self.repr { + Repr::Single(c) => Variants::Single(Some(*c).into_iter()), + Repr::Static(list) => Variants::Static(list.iter()), + Repr::Runtime(list) => Variants::Runtime(list.iter()), + } } /// Possible modifiers. pub fn modifiers(&self) -> impl Iterator + '_ { let mut set = BTreeSet::new(); - if let Repr::List(list) = self.repr { - for modifier in list.iter().flat_map(|(name, _)| name.split('.')) { - if !modifier.is_empty() && !contained(&self.modifiers, modifier) { - set.insert(modifier); - } + for modifier in self.variants().flat_map(|(name, _)| name.split('.')) { + if !modifier.is_empty() && !contained(&self.modifiers, modifier) { + set.insert(modifier); } } set.into_iter() @@ -115,13 +122,35 @@ impl Display for Symbol { } } +/// Iterator over variants. +enum Variants<'a> { + Single(std::option::IntoIter), + Static(std::slice::Iter<'static, (&'static str, char)>), + Runtime(std::slice::Iter<'a, (EcoString, char)>), +} + +impl<'a> Iterator for Variants<'a> { + type Item = (&'a str, char); + + fn next(&mut self) -> Option { + match self { + Self::Single(iter) => Some(("", iter.next()?)), + Self::Static(list) => list.next().copied(), + Self::Runtime(list) => list.next().map(|(s, c)| (s.as_str(), *c)), + } + } +} + /// Find the best symbol from the list. -fn find(list: &[(&str, char)], modifiers: &str) -> Option { +fn find<'a>( + variants: impl Iterator, + modifiers: &str, +) -> Option { let mut best = None; let mut best_score = None; // Find the best table entry with this name. - 'outer: for candidate in list { + 'outer: for candidate in variants { for modifier in parts(modifiers) { if !contained(candidate.0, modifier) { continue 'outer; diff --git a/tests/fonts/NotoSansSymbols2-Regular.ttf b/tests/fonts/NotoSansSymbols2-Regular.ttf new file mode 100644 index 000000000..429a51d56 Binary files /dev/null and b/tests/fonts/NotoSansSymbols2-Regular.ttf differ diff --git a/tests/ref/compute/construct.png b/tests/ref/compute/construct.png index 600e61741..6e637f342 100644 Binary files a/tests/ref/compute/construct.png and b/tests/ref/compute/construct.png differ diff --git a/tests/typ/compute/construct.typ b/tests/typ/compute/construct.typ index ccb7bd2e7..50fa4e3e1 100644 --- a/tests/typ/compute/construct.typ +++ b/tests/typ/compute/construct.typ @@ -41,6 +41,23 @@ // Error: 21-26 expected integer or ratio, found boolean #rgb(10%, 20%, 30%, false) +--- +// Ref: true +#let envelope = symbol( + "🖂", + ("stamped", "🖃"), + ("stamped.pen", "🖆"), + ("lightning", "🖄"), + ("fly", "🖅"), +) + +#envelope +#envelope.stamped +#envelope.pen +#envelope.stamped.pen +#envelope.lightning +#envelope.fly + --- // Test conversion to string. #test(str(123), "123")