use std::sync::OnceLock;
use harfrust::{
shape, Buffer, ClusterLevel, Direction, Font, Language, Script, ShapeOptions, ShapePlan,
ShaperFont,
};
use crate::duplicates::collapse;
use crate::generated::font_hudum::GLYPHS;
use crate::redundant_zwj::drop_redundant_zwj;
use crate::unicode::check_word_chars;
use crate::{Error, Position, WrittenUnit};
static FONT_DATA: &[u8] = include_bytes!("../fonts/hudum.otf");
static FONT: OnceLock<Font> = OnceLock::new();
fn font() -> &'static Font {
FONT.get_or_init(|| Font::new(FONT_DATA, 0).expect("fonts/hudum.otf is a valid font"))
}
fn is_skipped(c: char) -> bool {
matches!(
c as u32,
0x180B | 0x180C | 0x180D | 0x180F | 0x200D | 0x200C
)
}
fn language() -> Language {
Language::new("mn").expect("a non-empty language tag")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Kind {
Letters,
Mvs,
Nirugu,
Zwj,
Silent,
Unsupported,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Glyph {
pub(crate) kind: Kind,
pub(crate) units: &'static [WrittenUnit],
pub(crate) letters: u8,
pub(crate) form: Option<Position>,
pub(crate) joining: Option<Position>,
}
impl Glyph {
const fn control(kind: Kind, units: &'static [WrittenUnit]) -> Glyph {
Glyph {
kind,
units,
letters: 0,
form: None,
joining: None,
}
}
pub(crate) const fn letters(
units: &'static [WrittenUnit],
letters: u8,
form: Position,
joining: Position,
) -> Glyph {
Glyph {
kind: Kind::Letters,
units,
letters,
form: Some(form),
joining: Some(joining),
}
}
pub(crate) const fn mvs() -> Glyph {
Glyph::control(Kind::Mvs, &[WrittenUnit::Mvs])
}
pub(crate) const fn nirugu() -> Glyph {
Glyph::control(Kind::Nirugu, &[WrittenUnit::Nirugu])
}
pub(crate) const fn zwj() -> Glyph {
Glyph::control(Kind::Zwj, &[WrittenUnit::Zwj])
}
pub(crate) const fn silent() -> Glyph {
Glyph::control(Kind::Silent, &[])
}
pub(crate) const fn unsupported() -> Glyph {
Glyph::control(Kind::Unsupported, &[])
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ShapedGlyph {
pub glyph: u16,
pub index: usize,
pub units: &'static [WrittenUnit],
pub form: Option<Position>,
pub joining: Option<Position>,
pub skipped: bool,
}
pub struct FontShaper {
language: Language,
plan: ShapePlan,
}
impl Default for FontShaper {
fn default() -> FontShaper {
FontShaper::new()
}
}
impl FontShaper {
pub fn new() -> FontShaper {
let language = language();
let plan = ShapePlan::new(
font(),
Direction::LeftToRight,
Some(Script::MONGOLIAN),
Some(&language),
&[],
);
FontShaper { language, plan }
}
pub fn shape(&self, text: &str) -> Result<Vec<WrittenUnit>, Error> {
let glyphs = self.run(text)?;
let raw: Vec<WrittenUnit> = glyphs
.iter()
.filter(|glyph| !glyph.skipped)
.flat_map(|glyph| glyph.units.iter().copied())
.collect();
Ok(collapse(&drop_redundant_zwj(&raw)))
}
#[doc(hidden)]
pub fn shape_raw(&self, text: &str) -> Result<Vec<WrittenUnit>, Error> {
let glyphs = self.run(text)?;
Ok(glyphs
.iter()
.flat_map(|glyph| glyph.units.iter().copied())
.collect())
}
pub fn shape_str(&self, text: &str) -> Result<String, Error> {
let units = self.shape(text)?;
let mut out = String::new();
for unit in units {
if !out.is_empty() {
out.push('+');
}
out.push_str(unit.as_str());
}
Ok(out)
}
pub fn same_shape(&self, a: &str, b: &str) -> Result<bool, Error> {
Ok(self.shape(a)? == self.shape(b)?)
}
pub fn shape_glyphs(&self, text: &str) -> Result<Vec<ShapedGlyph>, Error> {
self.run(text)
}
fn run(&self, text: &str) -> Result<Vec<ShapedGlyph>, Error> {
check_word_chars(text)?;
let chars: Vec<char> = text.chars().collect();
let mut buffer = Buffer::new();
buffer.set_cluster_level(ClusterLevel::Characters);
for (index, &ch) in chars.iter().enumerate() {
buffer.push(ch as u32, index as u32);
}
buffer.set_direction(Direction::LeftToRight);
buffer.set_script(Some(Script::MONGOLIAN));
buffer.set_language(Some(self.language.clone()));
let shaper_font = ShaperFont::new(font());
shape(
&shaper_font,
&mut buffer,
ShapeOptions::new().plan(Some(&self.plan)),
)
.expect("the plan was built for this direction and script");
let mut out = Vec::with_capacity(buffer.len());
let mut span: Option<(usize, usize)> = None;
for info in buffer.glyph_infos() {
let id = info.glyph_id as usize;
let index = info.cluster as usize;
let glyph = GLYPHS.get(id).copied().unwrap_or(Glyph::unsupported());
match glyph.kind {
Kind::Unsupported => {
return Err(Error::UnsupportedChar {
ch: chars[index],
index,
})
}
Kind::Letters => span = Some((index, span_end(&chars, index, glyph.letters))),
_ => {}
}
let skipped = glyph.kind == Kind::Zwj
&& span.is_some_and(|(start, end)| start < index && index < end);
out.push(ShapedGlyph {
glyph: id as u16,
index,
units: glyph.units,
form: glyph.form,
joining: glyph.joining,
skipped,
});
}
Ok(out)
}
}
fn span_end(chars: &[char], start: usize, letters: u8) -> usize {
let mut remaining = letters.max(1);
let mut end = start;
for (offset, &ch) in chars[start..].iter().enumerate() {
if is_skipped(ch) {
continue;
}
remaining -= 1;
end = start + offset;
if remaining == 0 {
break;
}
}
end
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generated::font_hudum::GLYPH_COUNT;
#[test]
fn the_table_covers_the_font() {
assert_eq!(font().num_glyphs() as usize, GLYPH_COUNT);
assert_eq!(GLYPHS.len(), GLYPH_COUNT);
}
#[test]
fn every_letter_glyph_shows_at_least_one_unit() {
for (id, glyph) in GLYPHS.iter().enumerate() {
match glyph.kind {
Kind::Letters => {
assert!(!glyph.units.is_empty(), "glyph {id} shows nothing");
assert!(glyph.letters >= 1, "glyph {id} draws no letter");
}
Kind::Mvs | Kind::Nirugu | Kind::Zwj => assert_eq!(glyph.units.len(), 1),
Kind::Silent | Kind::Unsupported => assert!(glyph.units.is_empty()),
}
}
}
#[test]
fn shapers_are_shareable_between_threads() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<FontShaper>();
}
#[test]
fn span_end_skips_fvs_and_joiners() {
let chars: Vec<char> = "ᠪ\u{200D}\u{180B}ᠣᠯ".chars().collect();
assert_eq!(span_end(&chars, 0, 1), 0);
assert_eq!(span_end(&chars, 0, 2), 3);
assert_eq!(span_end(&chars, 0, 3), 4);
assert_eq!(span_end(&chars, 3, 1), 3);
assert_eq!(span_end(&chars, 3, 5), 4);
}
}