use super::{tables::is_word_joiner, unicode::UnicodeBackend};
use unicode_normalization::char::canonical_combining_class;
pub const MAX_TOKEN_BYTES: usize = 64;
pub(super) fn emit_truncated(token: &str, sink: &mut dyn FnMut(&str)) {
let token = trim_leading_contextual_chars(token);
if token.is_empty() || !has_emit_lexical_content(token) {
return;
}
if let Some((start, end)) = invalid_apostrophe_joiner(token) {
emit_truncated(&token[..start], sink);
emit_truncated(&token[end..], sink);
return;
}
let mut end = token.len().min(MAX_TOKEN_BYTES);
while !token.is_char_boundary(end) {
end -= 1;
}
let original_end = end;
let mut saw_trailing_joiner = false;
while end > 0 {
let Some(c) = token[..end].chars().next_back() else {
break;
};
if is_word_joiner(c) {
saw_trailing_joiner = true;
end -= c.len_utf8();
} else if UnicodeBackend::is_mark(c) {
end -= c.len_utf8();
} else {
if !saw_trailing_joiner {
end = original_end;
}
break;
}
}
if end == 0 && !saw_trailing_joiner {
end = original_end;
}
if end == 0 {
return;
}
if !has_emit_lexical_content(&token[..end]) {
return;
}
sink(&token[..end]);
}
fn trim_leading_marks(token: &str) -> &str {
let mut prefix = 0usize;
for (offset, c) in token.char_indices() {
if UnicodeBackend::is_mark(c) {
prefix = offset + c.len_utf8();
} else {
break;
}
}
if prefix == token.len() {
token
} else {
&token[prefix..]
}
}
fn trim_leading_contextual_chars(mut token: &str) -> &str {
loop {
let next = trim_leading_marks(token).trim_start_matches(is_word_joiner);
if next.len() == token.len() {
return token;
}
token = next;
}
}
fn invalid_apostrophe_joiner(token: &str) -> Option<(usize, usize)> {
let mut chars = token.char_indices().peekable();
while let Some((offset, c)) = chars.next() {
if c != '\'' && c != '\u{2019}' {
continue;
}
let left = previous_non_mark(&token[..offset]).is_some_and(is_letter);
let right = next_non_mark(chars.clone()).is_some_and(is_letter);
if !left || !right {
return Some((offset, offset + c.len_utf8()));
}
}
None
}
fn previous_non_mark(text: &str) -> Option<char> {
text.chars().rev().find(|&c| !UnicodeBackend::is_mark(c))
}
fn next_non_mark<'a>(chars: impl Iterator<Item = (usize, char)> + 'a) -> Option<char> {
chars.map(|(_, c)| c).find(|&c| !UnicodeBackend::is_mark(c))
}
fn is_letter(c: char) -> bool {
c.is_alphabetic() || UnicodeBackend::is_alphabetic(c)
}
fn has_emit_lexical_content(token: &str) -> bool {
let mut chars = token.chars();
let Some(first) = chars.next() else {
return false;
};
let mut only_one = true;
let mut all_marks = UnicodeBackend::is_mark(first);
if !all_marks && is_lexical_base(first) {
return true;
}
for c in chars {
only_one = false;
let is_mark = UnicodeBackend::is_mark(c);
all_marks &= is_mark;
if !is_mark && is_lexical_base(c) {
return true;
}
}
all_marks && only_one && first.is_alphanumeric() && canonical_combining_class(first) != 0
}
fn is_lexical_base(c: char) -> bool {
(c.is_alphanumeric() || UnicodeBackend::is_alphabetic(c)) && !UnicodeBackend::is_mark(c)
}