From 0a168b687e60948b0e89559e83713ef618a9d6a4 Mon Sep 17 00:00:00 2001 From: T0mstone <39707032+T0mstone@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:30:19 +0200 Subject: [PATCH] Allow multi-character symbols (#6489) Co-authored-by: Max Co-authored-by: Laurenz --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/typst-eval/src/markup.rs | 4 +- crates/typst-eval/src/math.rs | 2 +- crates/typst-ide/src/complete.rs | 6 +- crates/typst-layout/src/math/fragment.rs | 36 +++-- crates/typst-layout/src/math/mat.rs | 2 +- crates/typst-layout/src/math/text.rs | 66 ++++---- .../typst-library/src/foundations/symbol.rs | 143 ++++++++++-------- crates/typst-library/src/math/accent.rs | 6 +- crates/typst-library/src/math/matrix.rs | 2 +- crates/typst-library/src/symbols.rs | 2 +- crates/typst-realize/src/lib.rs | 8 +- docs/src/lib.rs | 19 ++- docs/src/model.rs | 2 +- tests/ref/symbol-constructor.png | Bin 511 -> 558 bytes tests/suite/symbols/symbol.typ | 26 ++++ 17 files changed, 200 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99253a7d..7c826e78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "codex" version = "0.1.1" -source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00" +source = "git+https://github.com/typst/codex?rev=775d828#775d82873c3f74ce95ec2621f8541de1b48778a7" [[package]] name = "color-print" diff --git a/Cargo.toml b/Cargo.toml index 0214e0bc..b855820d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" } +codex = { git = "https://github.com/typst/codex", rev = "775d828" } color-print = "0.3.6" comemo = "0.5.0" csv = "1" diff --git a/crates/typst-eval/src/markup.rs b/crates/typst-eval/src/markup.rs index 772494b6..48946f44 100644 --- a/crates/typst-eval/src/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -123,7 +123,7 @@ impl Eval for ast::Escape<'_> { type Output = Value; fn eval(self, _: &mut Vm) -> SourceResult { - Ok(Value::Symbol(Symbol::single(self.get()))) + Ok(Value::Symbol(Symbol::runtime_char(self.get()))) } } @@ -131,7 +131,7 @@ impl Eval for ast::Shorthand<'_> { type Output = Value; fn eval(self, _: &mut Vm) -> SourceResult { - Ok(Value::Symbol(Symbol::single(self.get()))) + Ok(Value::Symbol(Symbol::runtime_char(self.get()))) } } diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index a2979e28..06677677 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -49,7 +49,7 @@ impl Eval for ast::MathShorthand<'_> { type Output = Value; fn eval(self, _: &mut Vm) -> SourceResult { - Ok(Value::Symbol(Symbol::single(self.get()))) + Ok(Value::Symbol(Symbol::runtime_char(self.get()))) } } diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index ab376e0a..7fcb3371 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -98,7 +98,7 @@ pub enum CompletionKind { /// A font family. Font, /// A symbol. - Symbol(char), + Symbol(EcoString), } /// Complete in comments. Or rather, don't! @@ -450,7 +450,7 @@ fn field_access_completions( for modifier in symbol.modifiers() { if let Ok(modified) = symbol.clone().modified((), modifier) { ctx.completions.push(Completion { - kind: CompletionKind::Symbol(modified.get()), + kind: CompletionKind::Symbol(modified.get().into()), label: modifier.into(), apply: None, detail: None, @@ -1381,7 +1381,7 @@ impl<'a> CompletionContext<'a> { kind: kind.unwrap_or_else(|| match value { Value::Func(_) => CompletionKind::Func, Value::Type(_) => CompletionKind::Type, - Value::Symbol(s) => CompletionKind::Symbol(s.get()), + Value::Symbol(s) => CompletionKind::Symbol(s.get().into()), _ => CompletionKind::Constant, }), label, diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 610e2905..768eb092 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -6,8 +6,8 @@ use rustybuzz::{BufferFlags, UnicodeBuffer}; use ttf_parser::GlyphId; use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use typst_library::World; -use typst_library::diag::warning; -use typst_library::foundations::StyleChain; +use typst_library::diag::{At, HintedStrResult, SourceResult, bail, warning}; +use typst_library::foundations::{Repr, StyleChain}; use typst_library::introspection::Tag; use typst_library::layout::{ Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, @@ -307,7 +307,7 @@ impl GlyphFragment { styles: StyleChain, c: char, span: Span, - ) -> Option { + ) -> SourceResult> { Self::new(ctx.engine.world, styles, c.encode_utf8(&mut [0; 4]), span) } @@ -318,10 +318,10 @@ impl GlyphFragment { styles: StyleChain, text: &str, span: Span, - ) -> Option { + ) -> SourceResult> { assert!(text.graphemes(true).count() == 1); - let (c, font, mut glyph) = shape( + let Some((c, font, mut glyph)) = shape( world, variant(styles), features(styles), @@ -329,7 +329,11 @@ impl GlyphFragment { styles.get(TextElem::fallback), text, families(styles).collect(), - )?; + ) + .at(span)? + else { + return Ok(None); + }; glyph.span.0 = span; let limits = Limits::for_char(c); @@ -369,7 +373,7 @@ impl GlyphFragment { modifiers: FrameModifiers::get_in(styles), }; fragment.update_glyph(); - Some(fragment) + Ok(Some(fragment)) } /// Sets element id and boxes in appropriate way without changing other @@ -847,7 +851,7 @@ fn shape( fallback: bool, text: &str, families: Vec<&FontFamily>, -) -> Option<(char, Font, Glyph)> { +) -> HintedStrResult> { let mut used = vec![]; let buffer = UnicodeBuffer::new(); shape_glyph( @@ -874,7 +878,7 @@ fn shape_glyph<'a>( fallback: bool, text: &str, mut families: impl Iterator + Clone, -) -> Option<(char, Font, Glyph)> { +) -> HintedStrResult> { // Find the next available family. let book = world.book(); let mut selection = None; @@ -913,9 +917,9 @@ fn shape_glyph<'a>( span: (Span::detached(), 0), }; let c = text.chars().next().unwrap(); - return Some((c, font, glyph)); + return Ok(Some((c, font, glyph))); } - return None; + return Ok(None); }; // This font has been exhausted and will not be used again. @@ -944,9 +948,13 @@ fn shape_glyph<'a>( let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); match buffer.len() { - 0 => return None, + 0 => return Ok(None), 1 => {} - _ => unreachable!(), + // TODO: Deal with multiple glyphs. + _ => bail!( + "shaping the text `{}` yielded more than one glyph", text.repr(); + hint: "please report this as a bug", + ), } let info = buffer.glyph_infos()[0]; @@ -969,7 +977,7 @@ fn shape_glyph<'a>( span: (Span::detached(), 0), }; let c = text[cluster..].chars().next().unwrap(); - Some((c, font, glyph)) + Ok(Some((c, font, glyph))) } else { shape_glyph( world, diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index a1ba5fc2..2004d013 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -187,7 +187,7 @@ fn layout_body( // way too big. // This will never panic as a paren will never shape into nothing. let paren = - GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached()) + GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())? .unwrap(); for (column, col) in columns.iter().zip(&mut cols) { diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index ced257ae..4140d8b3 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -80,7 +80,7 @@ fn layout_inline_text( // This won't panic as ASCII digits and '.' will never end up as // nothing after shaping. - let glyph = GlyphFragment::new_char(ctx, styles, c, span).unwrap(); + let glyph = GlyphFragment::new_char(ctx, styles, c, span)?.unwrap(); fragments.push(glyph.into()); } let frame = MathRun::new(fragments).into_frame(styles); @@ -129,41 +129,47 @@ pub fn layout_symbol( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - // Switch dotless char to normal when we have the dtls OpenType feature. - // This should happen before the main styling pass. - let dtls = style_dtls(); - let (unstyled_c, symbol_styles) = match (try_dotless(elem.text), ctx.font().clone()) { - (Some(c), font) if has_dtls_feat(&font) => (c, styles.chain(&dtls)), - _ => (elem.text, styles), - }; - let variant = styles.get(EquationElem::variant); let bold = styles.get(EquationElem::bold); let italic = styles.get(EquationElem::italic); + let dtls = style_dtls(); + let has_dtls_feat = has_dtls_feat(ctx.font()); + for cluster in elem.text.graphemes(true) { + // Switch dotless char to normal when we have the dtls OpenType feature. + // This should happen before the main styling pass. + let mut enable_dtls = false; + let text: EcoString = cluster + .chars() + .flat_map(|mut c| { + if has_dtls_feat && let Some(d) = try_dotless(c) { + enable_dtls = true; + c = d; + } + to_style(c, MathStyle::select(c, variant, bold, italic)) + }) + .collect(); + let styles = if enable_dtls { styles.chain(&dtls) } else { styles }; - let style = MathStyle::select(unstyled_c, variant, bold, italic); - let text: EcoString = to_style(unstyled_c, style).collect(); - - if let Some(mut glyph) = - GlyphFragment::new(ctx.engine.world, symbol_styles, &text, elem.span()) - { - if glyph.class == MathClass::Large { - if styles.get(EquationElem::size) == MathSize::Display { - let height = glyph - .item - .font - .math() - .display_operator_min_height - .at(glyph.item.size); - glyph.stretch_vertical(ctx, height); - }; - // TeXbook p 155. Large operators are always vertically centered on - // the axis. - glyph.center_on_axis(); + if let Some(mut glyph) = + GlyphFragment::new(ctx.engine.world, styles, &text, elem.span())? + { + if glyph.class == MathClass::Large { + if styles.get(EquationElem::size) == MathSize::Display { + let height = glyph + .item + .font + .math() + .display_operator_min_height + .at(glyph.item.size); + glyph.stretch_vertical(ctx, height); + }; + // TeXbook p 155. Large operators are always vertically centered on + // the axis. + glyph.center_on_axis(); + } + ctx.push(glyph); } - ctx.push(glyph); } - Ok(()) } diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 1f55db59..b535f7ff 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -1,5 +1,5 @@ use std::collections::BTreeSet; -use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::fmt::{self, Debug, Display, Formatter}; use std::sync::Arc; use codex::ModifierSet; @@ -8,8 +8,9 @@ use rustc_hash::FxHashMap; use serde::{Serialize, Serializer}; use typst_syntax::{Span, Spanned, is_ident}; use typst_utils::hash128; +use unicode_segmentation::UnicodeSegmentation; -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, @@ -53,7 +54,7 @@ pub struct Symbol(Repr); #[derive(Clone, Eq, PartialEq, Hash)] enum Repr { /// A native symbol that has no named variant. - Single(char), + Single(&'static str), /// A native symbol with multiple named variants. Complex(&'static [Variant<&'static str>]), /// A symbol with multiple named variants, where some modifiers may have @@ -62,9 +63,9 @@ enum Repr { Modified(Arc<(List, ModifierSet)>), } -/// A symbol variant, consisting of a set of modifiers, a character, and an +/// A symbol variant, consisting of a set of modifiers, the variant's value, and an /// optional deprecation message. -type Variant = (ModifierSet, char, Option); +type Variant = (ModifierSet, S, Option); /// A collection of symbols. #[derive(Clone, Eq, PartialEq, Hash)] @@ -74,9 +75,9 @@ enum List { } impl Symbol { - /// Create a new symbol from a single character. - pub const fn single(c: char) -> Self { - Self(Repr::Single(c)) + /// Create a new symbol from a single value. + pub const fn single(value: &'static str) -> Self { + Self(Repr::Single(value)) } /// Create a symbol with a static variant list. @@ -86,6 +87,11 @@ impl Symbol { Self(Repr::Complex(list)) } + /// Create a symbol from a runtime char. + pub fn runtime_char(c: char) -> Self { + Self::runtime(Box::new([(ModifierSet::default(), c.into(), None)])) + } + /// Create a symbol with a runtime variant list. #[track_caller] pub fn runtime(list: Box<[Variant]>) -> Self { @@ -93,15 +99,15 @@ impl Symbol { Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default())))) } - /// Get the symbol's character. - pub fn get(&self) -> char { + /// Get the symbol's value. + pub fn get(&self) -> &str { match &self.0 { - Repr::Single(c) => *c, + Repr::Single(value) => value, Repr::Complex(_) => ModifierSet::<&'static str>::default() - .best_match_in(self.variants().map(|(m, c, _)| (m, c))) + .best_match_in(self.variants().map(|(m, v, _)| (m, v))) .unwrap(), Repr::Modified(arc) => { - arc.1.best_match_in(self.variants().map(|(m, c, _)| (m, c))).unwrap() + arc.1.best_match_in(self.variants().map(|(m, v, _)| (m, v))).unwrap() } } } @@ -109,27 +115,27 @@ impl Symbol { /// Try to get the function associated with the symbol, if any. pub fn func(&self) -> StrResult { match self.get() { - '⌈' => Ok(crate::math::ceil::func()), - '⌊' => Ok(crate::math::floor::func()), - '–' => Ok(crate::math::accent::dash::func()), - '⋅' | '\u{0307}' => Ok(crate::math::accent::dot::func()), - '¨' => Ok(crate::math::accent::dot_double::func()), - '\u{20db}' => Ok(crate::math::accent::dot_triple::func()), - '\u{20dc}' => Ok(crate::math::accent::dot_quad::func()), - '∼' => Ok(crate::math::accent::tilde::func()), - '´' => Ok(crate::math::accent::acute::func()), - '˝' => Ok(crate::math::accent::acute_double::func()), - '˘' => Ok(crate::math::accent::breve::func()), - 'ˇ' => Ok(crate::math::accent::caron::func()), - '^' => Ok(crate::math::accent::hat::func()), - '`' => Ok(crate::math::accent::grave::func()), - '¯' => Ok(crate::math::accent::macron::func()), - '○' => Ok(crate::math::accent::circle::func()), - '→' => Ok(crate::math::accent::arrow::func()), - '←' => Ok(crate::math::accent::arrow_l::func()), - '↔' => Ok(crate::math::accent::arrow_l_r::func()), - '⇀' => Ok(crate::math::accent::harpoon::func()), - '↼' => Ok(crate::math::accent::harpoon_lt::func()), + "⌈" => Ok(crate::math::ceil::func()), + "⌊" => Ok(crate::math::floor::func()), + "–" => Ok(crate::math::accent::dash::func()), + "⋅" | "\u{0307}" => Ok(crate::math::accent::dot::func()), + "¨" => Ok(crate::math::accent::dot_double::func()), + "\u{20db}" => Ok(crate::math::accent::dot_triple::func()), + "\u{20dc}" => Ok(crate::math::accent::dot_quad::func()), + "∼" => Ok(crate::math::accent::tilde::func()), + "´" => Ok(crate::math::accent::acute::func()), + "˝" => Ok(crate::math::accent::acute_double::func()), + "˘" => Ok(crate::math::accent::breve::func()), + "ˇ" => Ok(crate::math::accent::caron::func()), + "^" => Ok(crate::math::accent::hat::func()), + "`" => Ok(crate::math::accent::grave::func()), + "¯" => Ok(crate::math::accent::macron::func()), + "○" => Ok(crate::math::accent::circle::func()), + "→" => Ok(crate::math::accent::arrow::func()), + "←" => Ok(crate::math::accent::arrow_l::func()), + "↔" => Ok(crate::math::accent::arrow_l_r::func()), + "⇀" => Ok(crate::math::accent::harpoon::func()), + "↼" => Ok(crate::math::accent::harpoon_lt::func()), _ => bail!("symbol {self} is not callable"), } } @@ -164,7 +170,7 @@ impl Symbol { /// The characters that are covered by this symbol. pub fn variants(&self) -> impl Iterator> { match &self.0 { - Repr::Single(c) => Variants::Single(Some(*c).into_iter()), + Repr::Single(value) => Variants::Single(std::iter::once(*value)), Repr::Complex(list) => Variants::Static(list.iter()), Repr::Modified(arc) => arc.0.variants(), } @@ -227,15 +233,29 @@ 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() || v.1.graphemes(true).nth(1).is_some() { + errors.push(error!( + span, "invalid variant value: {}", v.1.repr(); + hint: "variant value must be exactly one grapheme cluster" + )); + } + 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); } @@ -246,29 +266,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" ) - } + }); + continue 'variants; } seen.insert(hash, i); } + if !errors.is_empty() { + return Err(errors); + } let list = variants .into_iter() @@ -280,14 +305,14 @@ impl Symbol { impl Display for Symbol { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_char(self.get()) + f.write_str(self.get()) } } impl Debug for Repr { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Single(c) => Debug::fmt(c, f), + Self::Single(value) => Debug::fmt(value, f), Self::Complex(list) => list.fmt(f), Self::Modified(lists) => lists.fmt(f), } @@ -306,7 +331,7 @@ impl Debug for List { impl crate::foundations::Repr for Symbol { fn repr(&self) -> EcoString { match &self.0 { - Repr::Single(c) => eco_format!("symbol(\"{}\")", *c), + Repr::Single(value) => eco_format!("symbol({})", value.repr()), Repr::Complex(variants) => { eco_format!( "symbol{}", @@ -342,15 +367,15 @@ fn repr_variants<'a>( // that contain all applied modifiers. applied_modifiers.iter().all(|am| modifiers.contains(am)) }) - .map(|(modifiers, c, _)| { + .map(|(modifiers, value, _)| { let trimmed_modifiers = modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m)); if trimmed_modifiers.clone().all(|m| m.is_empty()) { - eco_format!("\"{c}\"") + value.repr() } else { let trimmed_modifiers = trimmed_modifiers.collect::>().join("."); - eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c) + eco_format!("({}, {})", trimmed_modifiers.repr(), value.repr()) } }) .collect::>(), @@ -363,7 +388,7 @@ impl Serialize for Symbol { where S: Serializer, { - serializer.serialize_char(self.get()) + serializer.serialize_str(self.get()) } } @@ -378,11 +403,11 @@ impl List { } /// A value that can be cast to a symbol. -pub struct SymbolVariant(EcoString, char); +pub struct SymbolVariant(EcoString, EcoString); cast! { SymbolVariant, - c: char => Self(EcoString::new(), c), + s: EcoString => Self(EcoString::new(), s), array: Array => { let mut iter = array.into_iter(); match (iter.next(), iter.next(), iter.next()) { @@ -394,7 +419,7 @@ cast! { /// Iterator over variants. enum Variants<'a> { - Single(std::option::IntoIter), + Single(std::iter::Once<&'static str>), Static(std::slice::Iter<'static, Variant<&'static str>>), Runtime(std::slice::Iter<'a, Variant>), } @@ -407,7 +432,7 @@ impl<'a> Iterator for Variants<'a> { Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)), Self::Static(list) => list.next().copied(), Self::Runtime(list) => { - list.next().map(|(m, c, d)| (m.as_deref(), *c, d.as_deref())) + list.next().map(|(m, s, d)| (m.as_deref(), s.as_str(), d.as_deref())) } } } @@ -416,21 +441,21 @@ impl<'a> Iterator for Variants<'a> { /// A single character. #[elem(Repr, PlainText)] pub struct SymbolElem { - /// The symbol's character. + /// The symbol's value. #[required] - pub text: char, // This is called `text` for consistency with `TextElem`. + pub text: EcoString, // This is called `text` for consistency with `TextElem`. } impl SymbolElem { /// Create a new packed symbol element. - pub fn packed(text: impl Into) -> Content { + pub fn packed(text: impl Into) -> Content { Self::new(text.into()).pack() } } impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push(self.text); + text.push_str(&self.text); } } diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index 79e22304..90a1e311 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -187,8 +187,8 @@ cast! { Accent, self => self.0.into_value(), v: char => Self::new(v), - v: Content => match v.to_packed::() { - Some(elem) => Self::new(elem.text), - None => bail!("expected a symbol"), + v: Content => match v.to_packed::().and_then(|elem| elem.text.parse::().ok()) { + Some(c) => Self::new(c), + _ => bail!("expected a single-codepoint symbol"), }, } diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs index 440f62aa..bd645ccc 100644 --- a/crates/typst-library/src/math/matrix.rs +++ b/crates/typst-library/src/math/matrix.rs @@ -274,7 +274,7 @@ cast! { Delimiter, self => self.0.into_value(), _: NoneValue => Self::none(), - v: Symbol => Self::char(v.get())?, + v: Symbol => Self::char(v.get().parse::().map_err(|_| "expected a single-codepoint symbol")?)?, v: char => Self::char(v)?, } diff --git a/crates/typst-library/src/symbols.rs b/crates/typst-library/src/symbols.rs index 8f26d74a..3cb0c15e 100644 --- a/crates/typst-library/src/symbols.rs +++ b/crates/typst-library/src/symbols.rs @@ -39,7 +39,7 @@ impl From for Scope { impl From for Symbol { fn from(symbol: codex::Symbol) -> Self { match symbol { - codex::Symbol::Single(c) => Symbol::single(c), + codex::Symbol::Single(value) => Symbol::single(value), codex::Symbol::Multi(list) => Symbol::list(list), } } diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index abdecb07..050f1605 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -301,9 +301,7 @@ fn visit_kind_rules<'a>( // textual elements via `TEXTUAL` grouping. However, in math, this is // not desirable, so we just do it on a per-element basis. if let Some(elem) = content.to_packed::() { - if let Some(m) = - find_regex_match_in_str(elem.text.encode_utf8(&mut [0; 4]), styles) - { + if let Some(m) = find_regex_match_in_str(elem.text.as_str(), styles) { visit_regex_match(s, &[(content, styles)], m)?; return Ok(true); } @@ -324,7 +322,7 @@ fn visit_kind_rules<'a>( // Symbols in non-math content transparently convert to `TextElem` so we // don't have to handle them in non-math layout. if let Some(elem) = content.to_packed::() { - let mut text = TextElem::packed(elem.text).spanned(elem.span()); + let mut text = TextElem::packed(elem.text.clone()).spanned(elem.span()); if let Some(label) = elem.label() { text.set_label(label); } @@ -1238,7 +1236,7 @@ fn visit_regex_match<'a>( let len = if let Some(elem) = content.to_packed::() { elem.text.len() } else if let Some(elem) = content.to_packed::() { - elem.text.len_utf8() + elem.text.len() } else { 1 // The rest are Ascii, so just one byte. }; diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 340b4146..13a94e28 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -719,9 +719,13 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { } }; - for (variant, c, deprecation_message) in symbol.variants() { + for (variant, value, deprecation_message) in symbol.variants() { + let value_char = value.parse::().ok(); + let shorthand = |list: &[(&'static str, char)]| { - list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) + value_char.and_then(|c| { + list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) + }) }; let name = complete(variant); @@ -730,9 +734,14 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { name, markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST), math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST), - math_class: typst_utils::default_math_class(c).map(math_class_name), - codepoint: c as _, - accent: typst::math::Accent::combine(c).is_some(), + // Matches `typst_layout::math::GlyphFragment::new` + math_class: value.chars().next().and_then(|c| { + typst_utils::default_math_class(c).map(math_class_name) + }), + value: value.into(), + // Matches casting `Symbol` to `Accent` + accent: value_char + .is_some_and(|c| typst::math::Accent::combine(c).is_some()), alternates: symbol .variants() .filter(|(other, _, _)| other != &variant) diff --git a/docs/src/model.rs b/docs/src/model.rs index 86d46827..f93a47cc 100644 --- a/docs/src/model.rs +++ b/docs/src/model.rs @@ -161,7 +161,7 @@ pub struct SymbolsModel { #[serde(rename_all = "camelCase")] pub struct SymbolModel { pub name: EcoString, - pub codepoint: u32, + pub value: EcoString, pub accent: bool, pub alternates: Vec, pub markup_shorthand: Option<&'static str>, diff --git a/tests/ref/symbol-constructor.png b/tests/ref/symbol-constructor.png index e6db9491d1e7a8bef885085f0935ae853b6f1c31..0cccd70f17361c75db741c0d14b8d7f0ef9ca884 100644 GIT binary patch delta 533 zcmV+w0_y$$1Fi&+B!BfuL_t(|+U=D&Pa8oLfJv7+RqC|qQbqiVB+3s+sx+yRBBDvd z&0-{sEF)}-Ls;fqFsvDj!4mgjzyi!=4q9G_zOzj9H#nmBvV^!k{#+m~ zZ!fjcKg4IZF1L}|YM=~Btv-S{y)9HVPYRI0cFwTBD~U4_RB$N3@P5)QW*-AMc(W1Z zyRrcd;C)EJ@PCf1g3|}%VKlf2_W_c4yAr{N{BVNi|2&y9JPLtzfRR-&hPN7ZOC8d2 zQMt*(*-fC-#aTG5%8}A+PaM!xkCh+Yb@|x*ddkMXsYk-#3^=Aigx5Tj9bw04A&Bzu zHJTrQhe4|jR>ir^{*NwfnIR?#-}a3}!CYLFV+X6P~oi$_cgx7hfh z7)u!mwpt*%L?%}QP7^XH?tV@#}5R0x3Wff1p*HR<&x=rgu|8 zh8U^zAn842jor!;dO!~=sqCR)D48ZCVu>Y2o{q3S=Ntztl zUt4)kt|yezlk|l%4H79Z(~2rE5vRJ*CI~TA4P~(*_91>-)x!}-; z2H(DQKoV6qet&)8eQ@6c?A?Mk{5Yvv`h|{TUekZc=!|TiicB6UvKPFA)AmN6@+uV&=+15WnP@IH783wA5zjhoI;E8u#EPg zhP`e;ZarvtFH4sZxQyuwXImtO*nw)6R7TJo+$O_#bdg