diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index e4b12766d..c2e0a6ec7 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -8,7 +8,7 @@ use serde::{Serialize, Serializer}; use typst_syntax::{Span, Spanned, is_ident}; use typst_utils::hash128; -use crate::diag::{DeprecationSink, SourceResult, StrResult, bail}; +use crate::diag::{DeprecationSink, SourceResult, StrResult, bail, error}; use crate::foundations::{ Array, Content, Func, NativeElement, NativeFunc, Packed, PlainText, Repr as _, cast, elem, func, scope, ty, @@ -231,15 +231,30 @@ impl Symbol { // A list of modifiers, cleared & reused in each iteration. let mut modifiers = Vec::new(); + let mut errors = ecow::eco_vec![]; + // Validate the variants. - for (i, &Spanned { ref v, span }) in variants.iter().enumerate() { + 'variants: for (i, &Spanned { ref v, span }) in variants.iter().enumerate() { modifiers.clear(); + if v.1.is_empty() { + errors.push(if v.0.is_empty() { + error!(span, "empty default variant") + } else { + error!(span, "empty variant: {}", v.0.repr()) + }); + } + if !v.0.is_empty() { // Collect all modifiers. for modifier in v.0.split('.') { if !is_ident(modifier) { - bail!(span, "invalid symbol modifier: {}", modifier.repr()); + errors.push(error!( + span, + "invalid symbol modifier: {}", + modifier.repr() + )); + continue 'variants; } modifiers.push(modifier); } @@ -250,37 +265,34 @@ impl Symbol { // Ensure that there are no duplicate modifiers. if let Some(ms) = modifiers.windows(2).find(|ms| ms[0] == ms[1]) { - bail!( + errors.push(error!( span, "duplicate modifier within variant: {}", ms[0].repr(); hint: "modifiers are not ordered, so each one may appear only once" - ) + )); + continue 'variants; } // Check whether we had this set of modifiers before. let hash = hash128(&modifiers); if let Some(&i) = seen.get(&hash) { - if v.0.is_empty() { - bail!(span, "duplicate default variant"); + errors.push(if v.0.is_empty() { + error!(span, "duplicate default variant") } else if v.0 == variants[i].v.0 { - bail!(span, "duplicate variant: {}", v.0.repr()); + error!(span, "duplicate variant: {}", v.0.repr()) } else { - bail!( + error!( span, "duplicate variant: {}", v.0.repr(); hint: "variants with the same modifiers are identical, regardless of their order" ) - } - } - - if v.1.is_empty() { - if v.0.is_empty() { - bail!(span, "default variant is empty"); - } else { - bail!(span, "variant is empty: {}", v.0.repr()); - } + }); + continue 'variants; } seen.insert(hash, i); } + if !errors.is_empty() { + return Err(errors); + } let list = variants .into_iter() @@ -394,7 +406,6 @@ pub struct SymbolVariant(EcoString, EcoString); cast! { SymbolVariant, - c: char => Self(EcoString::new(), c.into()), s: EcoString => Self(EcoString::new(), s), array: Array => { let mut iter = array.into_iter(); diff --git a/tests/ref/symbol-constructor.png b/tests/ref/symbol-constructor.png index e6db9491d..0cccd70f1 100644 Binary files a/tests/ref/symbol-constructor.png and b/tests/ref/symbol-constructor.png differ diff --git a/tests/suite/symbols/symbol.typ b/tests/suite/symbols/symbol.typ index 9fdda9296..071bbc1a7 100644 --- a/tests/suite/symbols/symbol.typ +++ b/tests/suite/symbols/symbol.typ @@ -21,6 +21,10 @@ ("lightning", "🖄"), ("fly", "🖅"), ) +#let one = symbol( + "1", + ("emoji", "1️") +) #envelope #envelope.stamped @@ -28,6 +32,8 @@ #envelope.stamped.pen #envelope.lightning #envelope.fly +#one +#one.emoji --- symbol-constructor-empty --- // Error: 2-10 expected at least one variant @@ -82,6 +88,14 @@ ("variant.duplicate", "y"), ) +--- symbol-constructor-empty-variant --- +// Error: 2:3-2:5 empty default variant +// Error: 3:3-3:16 empty variant: "empty" +#symbol( + "", + ("empty", "") +) + --- symbol-unknown-modifier --- // Error: 13-20 unknown symbol modifier #emoji.face.garbage