use crate::generated::enums::{Alias, Condition, WrittenUnit};
use crate::tables::{Fvs, Position};
use crate::unicode::{is_mongolian_letter_cp, MVS, NIRUGU, NNBSP, ZWJ};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TokenKind {
Letter,
Mvs,
Nirugu,
Zwj,
}
#[derive(Clone, Debug)]
pub(crate) struct Token {
pub kind: TokenKind,
pub cp: u32,
pub alias: Option<Alias>,
pub fvs: Vec<Fvs>,
pub position: Position,
pub condition: Option<Condition>,
pub written: Option<&'static [WrittenUnit]>,
}
impl Token {
fn new(kind: TokenKind, cp: u32, alias: Option<Alias>) -> Token {
Token {
kind,
cp,
alias,
fvs: Vec::new(),
position: Position::Isol,
condition: None,
written: None,
}
}
pub fn is_letter(&self) -> bool {
self.kind == TokenKind::Letter
}
pub fn is_mvs(&self) -> bool {
self.kind == TokenKind::Mvs
}
pub fn is_nirugu(&self) -> bool {
matches!(self.kind, TokenKind::Nirugu | TokenKind::Zwj)
}
pub fn has_fvs(&self) -> bool {
!self.fvs.is_empty()
}
pub fn first_fvs(&self) -> Option<Fvs> {
self.fvs.first().copied()
}
pub fn written_ends_with(&self, unit: WrittenUnit) -> bool {
self.written.and_then(<[WrittenUnit]>::last) == Some(&unit)
}
}
pub(crate) fn tokenize(text: &str, alias_of: impl Fn(u32) -> Option<Alias>) -> Vec<Token> {
let cps: Vec<u32> = text.chars().map(|c| c as u32).collect();
let mut tokens = Vec::with_capacity(cps.len());
let mut i = 0;
while i < cps.len() {
let cp = cps[i];
if is_mongolian_letter_cp(cp) {
let mut token = Token::new(TokenKind::Letter, cp, alias_of(cp));
let mut j = i + 1;
while let Some(fvs) = cps.get(j).copied().and_then(Fvs::from_cp) {
token.fvs.push(fvs);
j += 1;
}
tokens.push(token);
i = j;
} else if cp == MVS || cp == NNBSP {
tokens.push(Token::new(TokenKind::Mvs, MVS, None));
i += 1;
} else if cp == NIRUGU {
tokens.push(Token::new(TokenKind::Nirugu, NIRUGU, None));
i += 1;
} else if cp == ZWJ {
tokens.push(Token::new(TokenKind::Zwj, ZWJ, None));
i += 1;
} else {
i += 1;
}
}
tokens
}
pub(crate) fn assign_positions(tokens: &mut [Token]) {
let mut segments: Vec<Vec<usize>> = Vec::new();
let mut current: Vec<usize> = Vec::new();
for (i, token) in tokens.iter().enumerate() {
if token.is_letter() || token.is_nirugu() {
current.push(i);
} else if token.is_mvs() && !current.is_empty() {
segments.push(std::mem::take(&mut current));
}
}
if !current.is_empty() {
segments.push(current);
}
for segment in segments {
let n = segment.len();
for (k, &i) in segment.iter().enumerate() {
if !tokens[i].is_letter() {
continue;
}
tokens[i].position = if n == 1 {
Position::Isol
} else if k == 0 {
Position::Init
} else if k == n - 1 {
Position::Fina
} else {
Position::Medi
};
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn no_alias(_: u32) -> Option<Alias> {
None
}
fn positions(text: &str) -> Vec<(TokenKind, Position)> {
let mut tokens = tokenize(text, no_alias);
assign_positions(&mut tokens);
tokens.iter().map(|t| (t.kind, t.position)).collect()
}
#[test]
fn letters_absorb_their_fvs_run() {
let tokens = tokenize("\u{1820}\u{180B}\u{180C}\u{1828}", no_alias);
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].fvs, vec![Fvs::Fvs1, Fvs::Fvs2]);
assert_eq!(tokens[0].first_fvs(), Some(Fvs::Fvs1));
assert!(tokens[0].has_fvs());
assert!(!tokens[1].has_fvs());
}
#[test]
fn nnbsp_becomes_mvs_and_joiners_keep_their_kind() {
let tokens = tokenize("\u{202F}\u{180E}\u{180A}\u{200D}x \u{180B}", no_alias);
let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind).collect();
assert_eq!(
kinds,
vec![
TokenKind::Mvs,
TokenKind::Mvs,
TokenKind::Nirugu,
TokenKind::Zwj
]
);
assert_eq!(tokens[0].cp, MVS);
assert!(tokens[2].is_nirugu() && tokens[3].is_nirugu());
}
#[test]
fn structural_positions() {
use Position::*;
use TokenKind::*;
assert_eq!(positions("\u{1828}"), vec![(Letter, Isol)]);
assert_eq!(
positions("\u{1820}\u{182A}"),
vec![(Letter, Init), (Letter, Fina)]
);
assert_eq!(
positions("\u{1832}\u{1820}\u{182F}"),
vec![(Letter, Init), (Letter, Medi), (Letter, Fina)]
);
assert_eq!(
positions("\u{1832}\u{1820}\u{182F}\u{180E}\u{1820}"),
vec![
(Letter, Init),
(Letter, Medi),
(Letter, Fina),
(Mvs, Isol),
(Letter, Isol)
]
);
assert_eq!(
positions("\u{180A}\u{1820}"),
vec![(Nirugu, Isol), (Letter, Fina)]
);
assert_eq!(
positions("\u{1820}\u{180A}"),
vec![(Letter, Init), (Nirugu, Isol)]
);
assert_eq!(
positions("\u{180A}\u{1820}\u{180A}"),
vec![(Nirugu, Isol), (Letter, Medi), (Nirugu, Isol)]
);
assert_eq!(
positions("\u{200D}\u{1833}"),
vec![(Zwj, Isol), (Letter, Fina)]
);
}
#[test]
fn written_ends_with_requires_resolution() {
let mut token = tokenize("\u{1820}", no_alias).remove(0);
assert!(!token.written_ends_with(WrittenUnit::A));
token.written = Some(&[WrittenUnit::A, WrittenUnit::A]);
assert!(token.written_ends_with(WrittenUnit::A));
assert!(!token.written_ends_with(WrittenUnit::I));
}
#[test]
fn alias_of_is_threaded_onto_letters_only() {
let tokens = tokenize("\u{1820}\u{180B}\u{180E}", |cp| {
(cp == 0x1820).then_some(Alias::A)
});
assert_eq!(tokens[0].alias, Some(Alias::A)); assert_eq!(tokens[1].alias, None); }
#[test]
fn empty_and_mvs_edge_cases() {
use Position::*;
use TokenKind::*;
assert!(tokenize("", no_alias).is_empty());
let mut empty: Vec<Token> = Vec::new();
assign_positions(&mut empty);
assert!(empty.is_empty());
assert_eq!(
positions("\u{180E}\u{1820}"),
vec![(Mvs, Isol), (Letter, Isol)]
);
assert_eq!(
positions("\u{1820}\u{180E}\u{180E}\u{1820}"),
vec![(Letter, Isol), (Mvs, Isol), (Mvs, Isol), (Letter, Isol)]
);
let tokens = tokenize("\u{180B}\u{1820}", no_alias);
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].kind, Letter);
assert!(!tokens[0].has_fvs());
}
}