summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-01-30 19:45:10 +0100
committerLaurenz <laurmaedje@gmail.com>2023-01-30 19:45:10 +0100
commit1ea0a933254d866e00acb9034bba39a5f4790682 (patch)
treed224b24f44d43795fabf2a35d0bb161b9d0d0938
parent3a4c5ae4b96ff5c2cd17a2f41a67398f21da0373 (diff)
User-defined symbols
-rw-r--r--library/src/compute/construct.rs61
-rw-r--r--library/src/lib.rs1
-rw-r--r--src/model/symbol.rs69
-rw-r--r--tests/fonts/NotoSansSymbols2-Regular.ttfbin0 -> 656828 bytes
-rw-r--r--tests/ref/compute/construct.pngbin155 -> 1308 bytes
-rw-r--r--tests/typ/compute/construct.typ17
6 files changed, 128 insertions, 20 deletions
diff --git a/library/src/compute/construct.rs b/library/src/compute/construct.rs
index 784513f6..176409dd 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<Value> {
+ let mut list: Vec<(EcoString, char)> = vec![];
+ for Spanned { v, span } in args.all::<Spanned<Variant>>()? {
+ 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 41c621cb..08ff171a 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::<compute::LumaFunc>("luma");
global.def_func::<compute::RgbFunc>("rgb");
global.def_func::<compute::CmykFunc>("cmyk");
+ global.def_func::<compute::SymbolFunc>("symbol");
global.def_func::<compute::StrFunc>("str");
global.def_func::<compute::LabelFunc>("label");
global.def_func::<compute::RegexFunc>("regex");
diff --git a/src/model/symbol.rs b/src/model/symbol.rs
index 146f7502..435048ac 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<Vec<(EcoString, char)>>),
}
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<Item = (&str, char)> {
- 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<Item = &str> + '_ {
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<char>),
+ 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<Self::Item> {
+ 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<char> {
+fn find<'a>(
+ variants: impl Iterator<Item = (&'a str, char)>,
+ modifiers: &str,
+) -> Option<char> {
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 00000000..429a51d5
--- /dev/null
+++ b/tests/fonts/NotoSansSymbols2-Regular.ttf
Binary files differ
diff --git a/tests/ref/compute/construct.png b/tests/ref/compute/construct.png
index 600e6174..6e637f34 100644
--- a/tests/ref/compute/construct.png
+++ b/tests/ref/compute/construct.png
Binary files differ
diff --git a/tests/typ/compute/construct.typ b/tests/typ/compute/construct.typ
index ccb7bd2e..50fa4e3e 100644
--- a/tests/typ/compute/construct.typ
+++ b/tests/typ/compute/construct.typ
@@ -42,6 +42,23 @@
#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")
#test(str(50.14), "50.14")