Compare commits

..

7 Commits

Author SHA1 Message Date
T0mstone
0dd19b5f6c
Merge 6594a0f530d42209c3c6f5f6a7e56fbff93b62e5 into 78355421ad73fdcbe93b4acca890b439c4b6f98d 2025-07-23 14:22:22 +02:00
T0mstone
6594a0f530 Make docs align with actual behavior 2025-07-23 14:22:16 +02:00
T0mstone
70f619e896 Add tests, fix bug, and improve symbol constructor errors 2025-07-23 14:04:17 +02:00
T0mstone
91189f4061 Error for empty symbol variant values 2025-07-23 13:16:41 +02:00
T0mstone
e48fe5e301 docs: Ignore variation selectors for math class and accent 2025-07-23 13:06:09 +02:00
T0mstone
5e202843c1 Add basic multi-char symbol layout
Not perfect, but should handle most cases

Co-authored-by: Max <max@mkor.je>
2025-07-23 12:52:12 +02:00
T0mstone
632186f446 Merge branch 'main' into multi-char-symbols 2025-07-23 12:22:15 +02:00
6 changed files with 79 additions and 46 deletions

View File

@ -300,6 +300,7 @@ impl GlyphFragment {
); );
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
// TODO: deal with multiple glyphs.
if buffer.len() != 1 { if buffer.len() != 1 {
bail!(span, "did not get a single glyph after shaping {}", text); bail!(span, "did not get a single glyph after shaping {}", text);
} }

View File

@ -129,44 +129,41 @@ pub fn layout_symbol(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
assert!(
elem.text.len() <= 4 && elem.text.chars().count() == 1,
"TODO: layout multi-char symbol"
);
let elem_char = elem
.text
.chars()
.next()
.expect("TODO: should an empty symbol value forbidden?");
// 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_char) {
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
_ => (elem_char, 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); let fragment: MathFragment =
let text: EcoString = to_style(unstyled_c, style).collect(); match GlyphFragment::new(ctx.font, styles, &text, elem.span()) {
Ok(mut glyph) => {
let fragment: MathFragment = adjust_glyph_layout(&mut glyph, ctx, styles);
match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) { glyph.into()
Ok(mut glyph) => { }
adjust_glyph_layout(&mut glyph, ctx, styles); Err(_) => {
glyph.into() // Not in the math font, fallback to normal inline text layout.
} // TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
Err(_) => { layout_inline_text(&text, elem.span(), ctx, styles)?.into()
// Not in the math font, fallback to normal inline text layout. }
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`]. };
layout_inline_text(&text, elem.span(), ctx, styles)?.into() ctx.push(fragment);
} }
};
ctx.push(fragment);
Ok(()) Ok(())
} }

View File

@ -8,7 +8,7 @@ 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 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,
@ -231,15 +231,30 @@ 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() {
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() { 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);
} }
@ -250,29 +265,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()
@ -386,7 +406,6 @@ pub struct SymbolVariant(EcoString, EcoString);
cast! { cast! {
SymbolVariant, SymbolVariant,
c: char => Self(EcoString::new(), c.into()),
s: EcoString => Self(EcoString::new(), s), s: EcoString => Self(EcoString::new(), s),
array: Array => { array: Array => {
let mut iter = array.into_iter(); let mut iter = array.into_iter();

View File

@ -733,10 +733,12 @@ 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: value_char.and_then(|c| { // Matches `typst_layout::math::GlyphFragment::new`
math_class: value.chars().next().and_then(|c| {
typst_utils::default_math_class(c).map(math_class_name) typst_utils::default_math_class(c).map(math_class_name)
}), }),
value: value.into(), value: value.into(),
// Matches casting `Symbol` to `Accent`
accent: value_char accent: value_char
.is_some_and(|c| typst::math::Accent::combine(c).is_some()), .is_some_and(|c| typst::math::Accent::combine(c).is_some()),
alternates: symbol alternates: symbol

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,14 @@
("variant.duplicate", "y"), ("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 --- --- symbol-unknown-modifier ---
// Error: 13-20 unknown symbol modifier // Error: 13-20 unknown symbol modifier
#emoji.face.garbage #emoji.face.garbage