use az::SaturatingAs;
use comemo::Tracked;
use rustybuzz::{
BufferFlags, Direction, Feature, Language, Script, UnicodeBuffer, shape_with_plan,
};
use ttf_parser::Tag;
use typst_library::World;
use typst_library::foundations::StyleChain;
use typst_library::layout::{Abs, Em};
use typst_library::math::families;
use typst_library::text::{
FontFamily, FontInstance, FontVariant, FontVariations, Glyph, TextElem, language,
variant,
};
use typst_syntax::Span;
use crate::inline::{SharedShapingContext, create_shape_plan, get_font_and_covers};
pub fn shape(
world: Tracked<dyn World + '_>,
styles: StyleChain,
features: &[Feature],
text: &str,
) -> Option<(FontInstance, Vec<Glyph>)> {
shape_impl(
world,
variant(styles),
features,
language(styles),
styles.get(TextElem::fallback),
text,
families(styles).collect(),
styles.resolve(TextElem::size),
&styles.get_cloned(TextElem::variations),
)
}
#[expect(clippy::too_many_arguments)]
fn shape_impl(
world: Tracked<dyn World + '_>,
variant: FontVariant,
features: &[Feature],
language: Language,
fallback: bool,
text: &str,
families: Vec<&FontFamily>,
size: Abs,
variations: &FontVariations,
) -> Option<(FontInstance, Vec<Glyph>)> {
let mut ctx = ShapingContext {
world,
used: vec![],
variant,
features,
language,
fallback,
glyphs: vec![],
font: None,
size,
variations,
};
shape_text(&mut ctx, text, families.into_iter());
Some((ctx.font?, ctx.glyphs))
}
pub fn feat_fallback<F>(mut features: Vec<Feature>, mut retry: F)
where
F: FnMut(&[Feature]) -> bool,
{
const FLAC: Tag = Tag::from_bytes(b"flac");
const SSTY: Tag = Tag::from_bytes(b"ssty");
const OPTIONS: [(bool, u32); 6] =
[(true, 2), (true, 1), (true, 0), (false, 2), (false, 1), (false, 0)];
let had_flac = features
.iter()
.rev()
.find(|f| f.tag == FLAC)
.is_some_and(|f| f.value != 0);
let prev_ssty = features
.iter()
.rev()
.find(|f| f.tag == SSTY)
.map(|f| f.value)
.unwrap_or(0);
features.retain(|f| f.tag != FLAC && f.tag != SSTY);
let base_len = features.len();
for (flac, ssty) in OPTIONS {
if (flac && !had_flac) || ssty > prev_ssty {
continue;
}
if (flac, ssty) == (had_flac, prev_ssty) {
continue;
}
features.truncate(base_len);
if flac {
features.push(Feature::new(FLAC, 1, ..));
}
if ssty > 0 {
features.push(Feature::new(SSTY, ssty, ..));
}
if retry(&features) {
break;
}
}
}
struct ShapingContext<'a, 'b> {
world: Tracked<'a, dyn World + 'a>,
used: Vec<FontInstance>,
variant: FontVariant,
features: &'b [Feature],
variations: &'b FontVariations,
language: Language,
fallback: bool,
glyphs: Vec<Glyph>,
font: Option<FontInstance>,
size: Abs,
}
impl<'a, 'b> SharedShapingContext<'a> for ShapingContext<'a, 'b> {
fn world(&self) -> Tracked<'a, dyn World + 'a> {
self.world
}
fn used(&mut self) -> &mut Vec<FontInstance> {
&mut self.used
}
fn first(&self) -> Option<&FontInstance> {
self.used.first()
}
fn variant(&self) -> FontVariant {
self.variant
}
fn fallback(&self) -> bool {
self.fallback
}
fn size(&self) -> Abs {
self.size
}
fn variations(&self) -> &FontVariations {
self.variations
}
}
fn shape_text<'a, 'b>(
ctx: &mut ShapingContext<'a, 'b>,
text: &str,
mut families: impl Iterator<Item = &'a FontFamily> + Clone,
) {
let shape_tofus = |ctx: &mut ShapingContext, text: &str, font: FontInstance| {
for _ in text.chars() {
ctx.glyphs.push(Glyph {
id: 0,
x_advance: font.x_advance(0).unwrap_or_default(),
x_offset: Em::zero(),
y_advance: Em::zero(),
y_offset: Em::zero(),
range: 0..text.len().saturating_as(),
span: (Span::detached(), 0),
});
}
ctx.font = Some(font);
};
let Some((font, covers)) =
get_font_and_covers(ctx, text, families.by_ref(), shape_tofus)
else {
return;
};
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_language(ctx.language.clone());
buffer.set_script(Script::from_iso15924_tag(Tag::from_bytes(b"math")).unwrap());
buffer.set_direction(Direction::LeftToRight);
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
let plan = create_shape_plan(
&font,
buffer.direction(),
buffer.script(),
buffer.language().as_ref(),
ctx.features,
);
let buffer = shape_with_plan(font.rusty(), &plan, buffer);
if buffer.glyph_infos().iter().any(|i| i.glyph_id == 0)
|| !covers.is_none_or(|cov| cov.is_match(text))
{
shape_text(ctx, text, families);
} else {
for i in 0..buffer.len() {
let info = buffer.glyph_infos()[i];
let pos = buffer.glyph_positions()[i];
ctx.glyphs.push(Glyph {
id: info.glyph_id as u16,
x_advance: font.to_em(pos.x_advance),
x_offset: font.to_em(pos.x_offset),
y_advance: font.to_em(pos.y_advance),
y_offset: font.to_em(pos.y_offset),
range: 0..text.len().saturating_as(),
span: (Span::detached(), 0),
});
}
if !buffer.is_empty() {
ctx.font = Some(font);
}
}
}