#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Fallback {
Cjk,
Emoji,
}
pub fn classify(c: char) -> Option<Fallback> {
let cp = c as u32;
if cp <= 0x00FF {
return None; }
if is_emoji(cp) {
return Some(Fallback::Emoji);
}
Some(Fallback::Cjk)
}
fn is_emoji(cp: u32) -> bool {
matches!(
cp,
0x1F000..=0x1FAFF | 0x2600..=0x26FF | 0x2700..=0x27BF
)
}
pub struct Run {
pub fallback: Option<Fallback>,
pub text: String,
}
pub fn split_runs(text: &str) -> Vec<Run> {
let mut runs: Vec<Run> = Vec::new();
for c in text.chars() {
let cp = c as u32;
let attach = matches!(cp, 0xFE0E | 0xFE0F | 0x200D);
let class = if attach {
runs.last().map(|r| r.fallback).unwrap_or(None)
} else {
classify(c)
};
match runs.last_mut() {
Some(r) if r.fallback == class => r.text.push(c),
_ => runs.push(Run {
fallback: class,
text: c.to_string(),
}),
}
}
runs
}
#[cfg(any(feature = "cjk-form-fonts", feature = "cjk-render-fallback"))]
pub fn font_bytes(kind: Fallback) -> &'static [u8] {
match kind {
Fallback::Cjk => include_bytes!("assets/DroidSansFallbackFull.ttf"),
Fallback::Emoji => include_bytes!("assets/NotoEmoji-Regular.ttf"),
}
}
#[cfg(feature = "cjk-render-fallback")]
pub fn render_cjk_fallback_bytes() -> &'static [u8] {
include_bytes!("assets/DroidSansFallbackFull.ttf")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn latin_needs_no_fallback() {
assert_eq!(classify('A'), None);
assert_eq!(classify('ñ'), None); }
#[test]
fn cjk_and_emoji_classified() {
assert_eq!(classify('や'), Some(Fallback::Cjk));
assert_eq!(classify('東'), Some(Fallback::Cjk));
assert_eq!(classify('🍺'), Some(Fallback::Emoji));
}
#[test]
fn runs_split_mixed_cjk_emoji() {
let runs = split_runs("やまだ🍺A");
let classes: Vec<_> = runs.iter().map(|r| r.fallback).collect();
assert_eq!(classes, vec![Some(Fallback::Cjk), Some(Fallback::Emoji), None]);
assert_eq!(runs[0].text, "やまだ");
assert_eq!(runs[1].text, "🍺");
assert_eq!(runs[2].text, "A");
}
}