Allow multi-character symbols (#6489)

Co-authored-by: Max <max@mkor.je>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
T0mstone 2025-09-04 11:30:19 +02:00 committed by GitHub
parent c340c27924
commit 0a168b687e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 200 additions and 128 deletions

2
Cargo.lock generated
View File

@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.1.1" 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]] [[package]]
name = "color-print" name = "color-print"

View File

@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1" clap_complete = "4.2.1"
clap_mangen = "0.2.10" clap_mangen = "0.2.10"
codespan-reporting = "0.11" 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" color-print = "0.3.6"
comemo = "0.5.0" comemo = "0.5.0"
csv = "1" csv = "1"

View File

@ -123,7 +123,7 @@ impl Eval for ast::Escape<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
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; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get()))) Ok(Value::Symbol(Symbol::runtime_char(self.get())))
} }
} }

View File

@ -49,7 +49,7 @@ impl Eval for ast::MathShorthand<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get()))) Ok(Value::Symbol(Symbol::runtime_char(self.get())))
} }
} }

View File

@ -98,7 +98,7 @@ pub enum CompletionKind {
/// A font family. /// A font family.
Font, Font,
/// A symbol. /// A symbol.
Symbol(char), Symbol(EcoString),
} }
/// Complete in comments. Or rather, don't! /// Complete in comments. Or rather, don't!
@ -450,7 +450,7 @@ fn field_access_completions(
for modifier in symbol.modifiers() { for modifier in symbol.modifiers() {
if let Ok(modified) = symbol.clone().modified((), modifier) { if let Ok(modified) = symbol.clone().modified((), modifier) {
ctx.completions.push(Completion { ctx.completions.push(Completion {
kind: CompletionKind::Symbol(modified.get()), kind: CompletionKind::Symbol(modified.get().into()),
label: modifier.into(), label: modifier.into(),
apply: None, apply: None,
detail: None, detail: None,
@ -1381,7 +1381,7 @@ impl<'a> CompletionContext<'a> {
kind: kind.unwrap_or_else(|| match value { kind: kind.unwrap_or_else(|| match value {
Value::Func(_) => CompletionKind::Func, Value::Func(_) => CompletionKind::Func,
Value::Type(_) => CompletionKind::Type, Value::Type(_) => CompletionKind::Type,
Value::Symbol(s) => CompletionKind::Symbol(s.get()), Value::Symbol(s) => CompletionKind::Symbol(s.get().into()),
_ => CompletionKind::Constant, _ => CompletionKind::Constant,
}), }),
label, label,

View File

@ -6,8 +6,8 @@ use rustybuzz::{BufferFlags, UnicodeBuffer};
use ttf_parser::GlyphId; use ttf_parser::GlyphId;
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use typst_library::World; use typst_library::World;
use typst_library::diag::warning; use typst_library::diag::{At, HintedStrResult, SourceResult, bail, warning};
use typst_library::foundations::StyleChain; use typst_library::foundations::{Repr, StyleChain};
use typst_library::introspection::Tag; use typst_library::introspection::Tag;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
@ -307,7 +307,7 @@ impl GlyphFragment {
styles: StyleChain, styles: StyleChain,
c: char, c: char,
span: Span, span: Span,
) -> Option<Self> { ) -> SourceResult<Option<Self>> {
Self::new(ctx.engine.world, styles, c.encode_utf8(&mut [0; 4]), span) Self::new(ctx.engine.world, styles, c.encode_utf8(&mut [0; 4]), span)
} }
@ -318,10 +318,10 @@ impl GlyphFragment {
styles: StyleChain, styles: StyleChain,
text: &str, text: &str,
span: Span, span: Span,
) -> Option<GlyphFragment> { ) -> SourceResult<Option<GlyphFragment>> {
assert!(text.graphemes(true).count() == 1); assert!(text.graphemes(true).count() == 1);
let (c, font, mut glyph) = shape( let Some((c, font, mut glyph)) = shape(
world, world,
variant(styles), variant(styles),
features(styles), features(styles),
@ -329,7 +329,11 @@ impl GlyphFragment {
styles.get(TextElem::fallback), styles.get(TextElem::fallback),
text, text,
families(styles).collect(), families(styles).collect(),
)?; )
.at(span)?
else {
return Ok(None);
};
glyph.span.0 = span; glyph.span.0 = span;
let limits = Limits::for_char(c); let limits = Limits::for_char(c);
@ -369,7 +373,7 @@ impl GlyphFragment {
modifiers: FrameModifiers::get_in(styles), modifiers: FrameModifiers::get_in(styles),
}; };
fragment.update_glyph(); fragment.update_glyph();
Some(fragment) Ok(Some(fragment))
} }
/// Sets element id and boxes in appropriate way without changing other /// Sets element id and boxes in appropriate way without changing other
@ -847,7 +851,7 @@ fn shape(
fallback: bool, fallback: bool,
text: &str, text: &str,
families: Vec<&FontFamily>, families: Vec<&FontFamily>,
) -> Option<(char, Font, Glyph)> { ) -> HintedStrResult<Option<(char, Font, Glyph)>> {
let mut used = vec![]; let mut used = vec![];
let buffer = UnicodeBuffer::new(); let buffer = UnicodeBuffer::new();
shape_glyph( shape_glyph(
@ -874,7 +878,7 @@ fn shape_glyph<'a>(
fallback: bool, fallback: bool,
text: &str, text: &str,
mut families: impl Iterator<Item = &'a FontFamily> + Clone, mut families: impl Iterator<Item = &'a FontFamily> + Clone,
) -> Option<(char, Font, Glyph)> { ) -> HintedStrResult<Option<(char, Font, Glyph)>> {
// Find the next available family. // Find the next available family.
let book = world.book(); let book = world.book();
let mut selection = None; let mut selection = None;
@ -913,9 +917,9 @@ fn shape_glyph<'a>(
span: (Span::detached(), 0), span: (Span::detached(), 0),
}; };
let c = text.chars().next().unwrap(); 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. // 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); let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
match buffer.len() { match buffer.len() {
0 => return None, 0 => return Ok(None),
1 => {} 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]; let info = buffer.glyph_infos()[0];
@ -969,7 +977,7 @@ fn shape_glyph<'a>(
span: (Span::detached(), 0), span: (Span::detached(), 0),
}; };
let c = text[cluster..].chars().next().unwrap(); let c = text[cluster..].chars().next().unwrap();
Some((c, font, glyph)) Ok(Some((c, font, glyph)))
} else { } else {
shape_glyph( shape_glyph(
world, world,

View File

@ -187,7 +187,7 @@ fn layout_body(
// way too big. // way too big.
// This will never panic as a paren will never shape into nothing. // This will never panic as a paren will never shape into nothing.
let paren = let paren =
GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached()) GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())?
.unwrap(); .unwrap();
for (column, col) in columns.iter().zip(&mut cols) { for (column, col) in columns.iter().zip(&mut cols) {

View File

@ -80,7 +80,7 @@ fn layout_inline_text(
// This won't panic as ASCII digits and '.' will never end up as // This won't panic as ASCII digits and '.' will never end up as
// nothing after shaping. // 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()); fragments.push(glyph.into());
} }
let frame = MathRun::new(fragments).into_frame(styles); let frame = MathRun::new(fragments).into_frame(styles);
@ -129,41 +129,47 @@ pub fn layout_symbol(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> 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 variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold); let bold = styles.get(EquationElem::bold);
let italic = styles.get(EquationElem::italic); 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); if let Some(mut glyph) =
let text: EcoString = to_style(unstyled_c, style).collect(); GlyphFragment::new(ctx.engine.world, styles, &text, elem.span())?
{
if let Some(mut glyph) = if glyph.class == MathClass::Large {
GlyphFragment::new(ctx.engine.world, symbol_styles, &text, elem.span()) if styles.get(EquationElem::size) == MathSize::Display {
{ let height = glyph
if glyph.class == MathClass::Large { .item
if styles.get(EquationElem::size) == MathSize::Display { .font
let height = glyph .math()
.item .display_operator_min_height
.font .at(glyph.item.size);
.math() glyph.stretch_vertical(ctx, height);
.display_operator_min_height };
.at(glyph.item.size); // TeXbook p 155. Large operators are always vertically centered on
glyph.stretch_vertical(ctx, height); // the axis.
}; glyph.center_on_axis();
// TeXbook p 155. Large operators are always vertically centered on }
// the axis. ctx.push(glyph);
glyph.center_on_axis();
} }
ctx.push(glyph);
} }
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fmt::{self, Debug, Display, Formatter, Write}; use std::fmt::{self, Debug, Display, Formatter};
use std::sync::Arc; use std::sync::Arc;
use codex::ModifierSet; use codex::ModifierSet;
@ -8,8 +8,9 @@ use rustc_hash::FxHashMap;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use typst_syntax::{Span, Spanned, is_ident}; use typst_syntax::{Span, Spanned, is_ident};
use typst_utils::hash128; 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::{ use crate::foundations::{
Array, Content, Func, NativeElement, NativeFunc, Packed, PlainText, Repr as _, cast, Array, Content, Func, NativeElement, NativeFunc, Packed, PlainText, Repr as _, cast,
elem, func, scope, ty, elem, func, scope, ty,
@ -53,7 +54,7 @@ pub struct Symbol(Repr);
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
enum Repr { enum Repr {
/// A native symbol that has no named variant. /// A native symbol that has no named variant.
Single(char), Single(&'static str),
/// A native symbol with multiple named variants. /// A native symbol with multiple named variants.
Complex(&'static [Variant<&'static str>]), Complex(&'static [Variant<&'static str>]),
/// A symbol with multiple named variants, where some modifiers may have /// A symbol with multiple named variants, where some modifiers may have
@ -62,9 +63,9 @@ enum Repr {
Modified(Arc<(List, ModifierSet<EcoString>)>), Modified(Arc<(List, ModifierSet<EcoString>)>),
} }
/// 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. /// optional deprecation message.
type Variant<S> = (ModifierSet<S>, char, Option<S>); type Variant<S> = (ModifierSet<S>, S, Option<S>);
/// A collection of symbols. /// A collection of symbols.
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
@ -74,9 +75,9 @@ enum List {
} }
impl Symbol { impl Symbol {
/// Create a new symbol from a single character. /// Create a new symbol from a single value.
pub const fn single(c: char) -> Self { pub const fn single(value: &'static str) -> Self {
Self(Repr::Single(c)) Self(Repr::Single(value))
} }
/// Create a symbol with a static variant list. /// Create a symbol with a static variant list.
@ -86,6 +87,11 @@ impl Symbol {
Self(Repr::Complex(list)) 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. /// Create a symbol with a runtime variant list.
#[track_caller] #[track_caller]
pub fn runtime(list: Box<[Variant<EcoString>]>) -> Self { pub fn runtime(list: Box<[Variant<EcoString>]>) -> Self {
@ -93,15 +99,15 @@ impl Symbol {
Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default())))) Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default()))))
} }
/// Get the symbol's character. /// Get the symbol's value.
pub fn get(&self) -> char { pub fn get(&self) -> &str {
match &self.0 { match &self.0 {
Repr::Single(c) => *c, Repr::Single(value) => value,
Repr::Complex(_) => ModifierSet::<&'static str>::default() 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(), .unwrap(),
Repr::Modified(arc) => { 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. /// Try to get the function associated with the symbol, if any.
pub fn func(&self) -> StrResult<Func> { pub fn func(&self) -> StrResult<Func> {
match self.get() { match self.get() {
'⌈' => Ok(crate::math::ceil::func()), "" => Ok(crate::math::ceil::func()),
'⌊' => Ok(crate::math::floor::func()), "" => Ok(crate::math::floor::func()),
'' => Ok(crate::math::accent::dash::func()), "" => Ok(crate::math::accent::dash::func()),
'⋅' | '\u{0307}' => Ok(crate::math::accent::dot::func()), "" | "\u{0307}" => Ok(crate::math::accent::dot::func()),
'¨' => Ok(crate::math::accent::dot_double::func()), "¨" => Ok(crate::math::accent::dot_double::func()),
'\u{20db}' => Ok(crate::math::accent::dot_triple::func()), "\u{20db}" => Ok(crate::math::accent::dot_triple::func()),
'\u{20dc}' => Ok(crate::math::accent::dot_quad::func()), "\u{20dc}" => Ok(crate::math::accent::dot_quad::func()),
'' => Ok(crate::math::accent::tilde::func()), "" => Ok(crate::math::accent::tilde::func()),
'´' => Ok(crate::math::accent::acute::func()), "´" => Ok(crate::math::accent::acute::func()),
'˝' => Ok(crate::math::accent::acute_double::func()), "˝" => Ok(crate::math::accent::acute_double::func()),
'˘' => Ok(crate::math::accent::breve::func()), "˘" => Ok(crate::math::accent::breve::func()),
'ˇ' => Ok(crate::math::accent::caron::func()), "ˇ" => Ok(crate::math::accent::caron::func()),
'^' => Ok(crate::math::accent::hat::func()), "^" => Ok(crate::math::accent::hat::func()),
'`' => Ok(crate::math::accent::grave::func()), "`" => Ok(crate::math::accent::grave::func()),
'¯' => Ok(crate::math::accent::macron::func()), "¯" => Ok(crate::math::accent::macron::func()),
'○' => Ok(crate::math::accent::circle::func()), "" => Ok(crate::math::accent::circle::func()),
'→' => Ok(crate::math::accent::arrow::func()), "" => Ok(crate::math::accent::arrow::func()),
'←' => Ok(crate::math::accent::arrow_l::func()), "" => Ok(crate::math::accent::arrow_l::func()),
'↔' => Ok(crate::math::accent::arrow_l_r::func()), "" => Ok(crate::math::accent::arrow_l_r::func()),
'⇀' => Ok(crate::math::accent::harpoon::func()), "" => Ok(crate::math::accent::harpoon::func()),
'↼' => Ok(crate::math::accent::harpoon_lt::func()), "" => Ok(crate::math::accent::harpoon_lt::func()),
_ => bail!("symbol {self} is not callable"), _ => bail!("symbol {self} is not callable"),
} }
} }
@ -164,7 +170,7 @@ impl Symbol {
/// The characters that are covered by this symbol. /// The characters that are covered by this symbol.
pub fn variants(&self) -> impl Iterator<Item = Variant<&str>> { pub fn variants(&self) -> impl Iterator<Item = Variant<&str>> {
match &self.0 { 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::Complex(list) => Variants::Static(list.iter()),
Repr::Modified(arc) => arc.0.variants(), Repr::Modified(arc) => arc.0.variants(),
} }
@ -227,15 +233,29 @@ impl Symbol {
// A list of modifiers, cleared & reused in each iteration. // A list of modifiers, cleared & reused in each iteration.
let mut modifiers = Vec::new(); let mut modifiers = Vec::new();
let mut errors = ecow::eco_vec![];
// Validate the variants. // 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(); 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() { if !v.0.is_empty() {
// Collect all modifiers. // Collect all modifiers.
for modifier in v.0.split('.') { for modifier in v.0.split('.') {
if !is_ident(modifier) { 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); modifiers.push(modifier);
} }
@ -246,29 +266,34 @@ impl Symbol {
// Ensure that there are no duplicate modifiers. // Ensure that there are no duplicate modifiers.
if let Some(ms) = modifiers.windows(2).find(|ms| ms[0] == ms[1]) { 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(); span, "duplicate modifier within variant: {}", ms[0].repr();
hint: "modifiers are not ordered, so each one may appear only once" hint: "modifiers are not ordered, so each one may appear only once"
) ));
continue 'variants;
} }
// Check whether we had this set of modifiers before. // Check whether we had this set of modifiers before.
let hash = hash128(&modifiers); let hash = hash128(&modifiers);
if let Some(&i) = seen.get(&hash) { if let Some(&i) = seen.get(&hash) {
if v.0.is_empty() { errors.push(if v.0.is_empty() {
bail!(span, "duplicate default variant"); error!(span, "duplicate default variant")
} else if v.0 == variants[i].v.0 { } else if v.0 == variants[i].v.0 {
bail!(span, "duplicate variant: {}", v.0.repr()); error!(span, "duplicate variant: {}", v.0.repr())
} else { } else {
bail!( error!(
span, "duplicate variant: {}", v.0.repr(); span, "duplicate variant: {}", v.0.repr();
hint: "variants with the same modifiers are identical, regardless of their order" hint: "variants with the same modifiers are identical, regardless of their order"
) )
} });
continue 'variants;
} }
seen.insert(hash, i); seen.insert(hash, i);
} }
if !errors.is_empty() {
return Err(errors);
}
let list = variants let list = variants
.into_iter() .into_iter()
@ -280,14 +305,14 @@ impl Symbol {
impl Display for Symbol { impl Display for Symbol {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_char(self.get()) f.write_str(self.get())
} }
} }
impl Debug for Repr { impl Debug for Repr {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::Single(c) => Debug::fmt(c, f), Self::Single(value) => Debug::fmt(value, f),
Self::Complex(list) => list.fmt(f), Self::Complex(list) => list.fmt(f),
Self::Modified(lists) => lists.fmt(f), Self::Modified(lists) => lists.fmt(f),
} }
@ -306,7 +331,7 @@ impl Debug for List {
impl crate::foundations::Repr for Symbol { impl crate::foundations::Repr for Symbol {
fn repr(&self) -> EcoString { fn repr(&self) -> EcoString {
match &self.0 { match &self.0 {
Repr::Single(c) => eco_format!("symbol(\"{}\")", *c), Repr::Single(value) => eco_format!("symbol({})", value.repr()),
Repr::Complex(variants) => { Repr::Complex(variants) => {
eco_format!( eco_format!(
"symbol{}", "symbol{}",
@ -342,15 +367,15 @@ fn repr_variants<'a>(
// that contain all applied modifiers. // that contain all applied modifiers.
applied_modifiers.iter().all(|am| modifiers.contains(am)) applied_modifiers.iter().all(|am| modifiers.contains(am))
}) })
.map(|(modifiers, c, _)| { .map(|(modifiers, value, _)| {
let trimmed_modifiers = let trimmed_modifiers =
modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m)); modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m));
if trimmed_modifiers.clone().all(|m| m.is_empty()) { if trimmed_modifiers.clone().all(|m| m.is_empty()) {
eco_format!("\"{c}\"") value.repr()
} else { } else {
let trimmed_modifiers = let trimmed_modifiers =
trimmed_modifiers.collect::<Vec<_>>().join("."); trimmed_modifiers.collect::<Vec<_>>().join(".");
eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c) eco_format!("({}, {})", trimmed_modifiers.repr(), value.repr())
} }
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
@ -363,7 +388,7 @@ impl Serialize for Symbol {
where where
S: Serializer, 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. /// A value that can be cast to a symbol.
pub struct SymbolVariant(EcoString, char); pub struct SymbolVariant(EcoString, EcoString);
cast! { cast! {
SymbolVariant, SymbolVariant,
c: char => Self(EcoString::new(), c), s: EcoString => Self(EcoString::new(), s),
array: Array => { array: Array => {
let mut iter = array.into_iter(); let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) { match (iter.next(), iter.next(), iter.next()) {
@ -394,7 +419,7 @@ cast! {
/// Iterator over variants. /// Iterator over variants.
enum Variants<'a> { enum Variants<'a> {
Single(std::option::IntoIter<char>), Single(std::iter::Once<&'static str>),
Static(std::slice::Iter<'static, Variant<&'static str>>), Static(std::slice::Iter<'static, Variant<&'static str>>),
Runtime(std::slice::Iter<'a, Variant<EcoString>>), Runtime(std::slice::Iter<'a, Variant<EcoString>>),
} }
@ -407,7 +432,7 @@ impl<'a> Iterator for Variants<'a> {
Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)), Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)),
Self::Static(list) => list.next().copied(), Self::Static(list) => list.next().copied(),
Self::Runtime(list) => { 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. /// A single character.
#[elem(Repr, PlainText)] #[elem(Repr, PlainText)]
pub struct SymbolElem { pub struct SymbolElem {
/// The symbol's character. /// The symbol's value.
#[required] #[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 { impl SymbolElem {
/// Create a new packed symbol element. /// Create a new packed symbol element.
pub fn packed(text: impl Into<char>) -> Content { pub fn packed(text: impl Into<EcoString>) -> Content {
Self::new(text.into()).pack() Self::new(text.into()).pack()
} }
} }
impl PlainText for Packed<SymbolElem> { impl PlainText for Packed<SymbolElem> {
fn plain_text(&self, text: &mut EcoString) { fn plain_text(&self, text: &mut EcoString) {
text.push(self.text); text.push_str(&self.text);
} }
} }

View File

@ -187,8 +187,8 @@ cast! {
Accent, Accent,
self => self.0.into_value(), self => self.0.into_value(),
v: char => Self::new(v), v: char => Self::new(v),
v: Content => match v.to_packed::<SymbolElem>() { v: Content => match v.to_packed::<SymbolElem>().and_then(|elem| elem.text.parse::<char>().ok()) {
Some(elem) => Self::new(elem.text), Some(c) => Self::new(c),
None => bail!("expected a symbol"), _ => bail!("expected a single-codepoint symbol"),
}, },
} }

View File

@ -274,7 +274,7 @@ cast! {
Delimiter, Delimiter,
self => self.0.into_value(), self => self.0.into_value(),
_: NoneValue => Self::none(), _: NoneValue => Self::none(),
v: Symbol => Self::char(v.get())?, v: Symbol => Self::char(v.get().parse::<char>().map_err(|_| "expected a single-codepoint symbol")?)?,
v: char => Self::char(v)?, v: char => Self::char(v)?,
} }

View File

@ -39,7 +39,7 @@ impl From<codex::Module> for Scope {
impl From<codex::Symbol> for Symbol { impl From<codex::Symbol> for Symbol {
fn from(symbol: codex::Symbol) -> Self { fn from(symbol: codex::Symbol) -> Self {
match symbol { match symbol {
codex::Symbol::Single(c) => Symbol::single(c), codex::Symbol::Single(value) => Symbol::single(value),
codex::Symbol::Multi(list) => Symbol::list(list), codex::Symbol::Multi(list) => Symbol::list(list),
} }
} }

View File

@ -301,9 +301,7 @@ fn visit_kind_rules<'a>(
// textual elements via `TEXTUAL` grouping. However, in math, this is // textual elements via `TEXTUAL` grouping. However, in math, this is
// not desirable, so we just do it on a per-element basis. // not desirable, so we just do it on a per-element basis.
if let Some(elem) = content.to_packed::<SymbolElem>() { if let Some(elem) = content.to_packed::<SymbolElem>() {
if let Some(m) = if let Some(m) = find_regex_match_in_str(elem.text.as_str(), styles) {
find_regex_match_in_str(elem.text.encode_utf8(&mut [0; 4]), styles)
{
visit_regex_match(s, &[(content, styles)], m)?; visit_regex_match(s, &[(content, styles)], m)?;
return Ok(true); return Ok(true);
} }
@ -324,7 +322,7 @@ fn visit_kind_rules<'a>(
// Symbols in non-math content transparently convert to `TextElem` so we // Symbols in non-math content transparently convert to `TextElem` so we
// don't have to handle them in non-math layout. // don't have to handle them in non-math layout.
if let Some(elem) = content.to_packed::<SymbolElem>() { if let Some(elem) = content.to_packed::<SymbolElem>() {
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() { if let Some(label) = elem.label() {
text.set_label(label); text.set_label(label);
} }
@ -1238,7 +1236,7 @@ fn visit_regex_match<'a>(
let len = if let Some(elem) = content.to_packed::<TextElem>() { let len = if let Some(elem) = content.to_packed::<TextElem>() {
elem.text.len() elem.text.len()
} else if let Some(elem) = content.to_packed::<SymbolElem>() { } else if let Some(elem) = content.to_packed::<SymbolElem>() {
elem.text.len_utf8() elem.text.len()
} else { } else {
1 // The rest are Ascii, so just one byte. 1 // The rest are Ascii, so just one byte.
}; };

View File

@ -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::<char>().ok();
let shorthand = |list: &[(&'static str, char)]| { 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); let name = complete(variant);
@ -730,9 +734,14 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
name, name,
markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST), markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST),
math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST), math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST),
math_class: typst_utils::default_math_class(c).map(math_class_name), // Matches `typst_layout::math::GlyphFragment::new`
codepoint: c as _, math_class: value.chars().next().and_then(|c| {
accent: typst::math::Accent::combine(c).is_some(), 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 alternates: symbol
.variants() .variants()
.filter(|(other, _, _)| other != &variant) .filter(|(other, _, _)| other != &variant)

View File

@ -161,7 +161,7 @@ pub struct SymbolsModel {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SymbolModel { pub struct SymbolModel {
pub name: EcoString, pub name: EcoString,
pub codepoint: u32, pub value: EcoString,
pub accent: bool, pub accent: bool,
pub alternates: Vec<EcoString>, pub alternates: Vec<EcoString>,
pub markup_shorthand: Option<&'static str>, pub markup_shorthand: Option<&'static str>,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 558 B

View File

@ -21,6 +21,10 @@
("lightning", "🖄"), ("lightning", "🖄"),
("fly", "🖅"), ("fly", "🖅"),
) )
#let one = symbol(
"1",
("emoji", "1")
)
#envelope #envelope
#envelope.stamped #envelope.stamped
@ -28,6 +32,8 @@
#envelope.stamped.pen #envelope.stamped.pen
#envelope.lightning #envelope.lightning
#envelope.fly #envelope.fly
#one
#one.emoji
--- symbol-constructor-empty --- --- symbol-constructor-empty ---
// Error: 2-10 expected at least one variant // Error: 2-10 expected at least one variant
@ -82,6 +88,26 @@
("variant.duplicate", "y"), ("variant.duplicate", "y"),
) )
--- symbol-constructor-empty-variant-value ---
// Error: 2:3-2:5 invalid variant value: ""
// Hint: 2:3-2:5 variant value must be exactly one grapheme cluster
// Error: 3:3-3:16 invalid variant value: ""
// Hint: 3:3-3:16 variant value must be exactly one grapheme cluster
#symbol(
"",
("empty", "")
)
--- symbol-constructor-multi-cluster-variant-value ---
// Error: 2:3-2:7 invalid variant value: "aa"
// Hint: 2:3-2:7 variant value must be exactly one grapheme cluster
// Error: 3:3-3:14 invalid variant value: "bb"
// Hint: 3:3-3:14 variant value must be exactly one grapheme cluster
#symbol(
"aa",
("b", "bb")
)
--- symbol-unknown-modifier --- --- symbol-unknown-modifier ---
// Error: 13-20 unknown symbol modifier // Error: 13-20 unknown symbol modifier
#emoji.face.garbage #emoji.face.garbage