use super::abbrev;
use super::chars::{
codepoints, is_alpha, is_digit, is_inner_uk_apostrophe, is_latin, is_space, is_uk,
is_word_mark, lower_ascii_ukrainian, smile_at, Cp,
};
use super::{push_substring, Substring};
const TOKEN_PUNCT: &str = "\\/!#$%&*+,.:;<=>?@^_`|~№…‑–—−-«“‘»”’\"„'()[]{}";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum AtomType {
Uk,
Lat,
Int,
Punct,
Other,
}
#[derive(Clone, Copy, Debug)]
struct Atom<'a> {
start: usize,
stop: usize,
kind: AtomType,
text: &'a str,
}
fn is_token_punct(cp: char) -> bool {
TOKEN_PUNCT.contains(cp)
}
fn is_web_stop(cp: char) -> bool {
is_space(cp) || "<>\"«»“”()[]{}".contains(cp)
}
fn is_trailing_url_punct(cp: char) -> bool {
matches!(cp, '.' | ',' | ';' | ':' | '!' | '?')
}
fn legal_number_atom_stop(cps: &[Cp], index: usize) -> Option<usize> {
if cps[index].value != '№' {
return None;
}
let mut i = index + 1;
if cps.get(i).is_some_and(|c| c.value == '-') {
i += 1;
}
let body_begin = i;
while i < cps.len()
&& (is_alpha(cps[i].value) || is_digit(cps[i].value) || matches!(cps[i].value, '/' | '-'))
{
i += 1;
}
(i > body_begin).then_some(i)
}
fn web_atom_stop(text: &str, cps: &[Cp], index: usize) -> Option<usize> {
let rest = &text[cps[index].start..];
if rest.starts_with("http://") || rest.starts_with("https://") {
let mut i = index;
while i < cps.len() && !is_web_stop(cps[i].value) {
i += 1;
}
while i > index && is_trailing_url_punct(cps[i - 1].value) {
i -= 1;
}
return (i > index).then_some(i);
}
if matches!(cps[index].value, '@' | '#') {
let mut i = index + 1;
while i < cps.len()
&& (is_alpha(cps[i].value) || is_digit(cps[i].value) || is_word_mark(cps[i].value))
{
i += 1;
}
return (i > index + 1).then_some(i);
}
if !is_alpha(cps[index].value) && !is_digit(cps[index].value) {
return None;
}
let mut i = index;
let mut saw_at = false;
let mut saw_dot_after_at = false;
while i < cps.len() {
let cp = cps[i].value;
if is_alpha(cp) || is_digit(cp) || matches!(cp, '_' | '-' | '.') {
saw_dot_after_at |= saw_at && cp == '.';
i += 1;
} else if cp == '@' && !saw_at && i > index {
saw_at = true;
i += 1;
} else {
break;
}
}
while i > index && is_trailing_url_punct(cps[i - 1].value) {
i -= 1;
}
(saw_at && saw_dot_after_at && i > index).then_some(i)
}
fn atoms(text: &str) -> Vec<Atom<'_>> {
let cps = codepoints(text);
let mut out = Vec::new();
let mut i = 0;
while i < cps.len() {
if is_space(cps[i].value) {
i += 1;
continue;
}
let begin = i;
let kind;
if let Some(stop) = legal_number_atom_stop(&cps, i).or_else(|| web_atom_stop(text, &cps, i))
{
kind = AtomType::Other;
i = stop;
} else if is_uk(cps[i].value) {
kind = AtomType::Uk;
while i < cps.len() && (is_uk(cps[i].value) || is_inner_uk_apostrophe(&cps, i)) {
i += 1;
}
} else if is_latin(cps[i].value) {
kind = AtomType::Lat;
while i < cps.len() && is_latin(cps[i].value) {
i += 1;
}
} else if is_digit(cps[i].value) {
kind = AtomType::Int;
while i < cps.len() && is_digit(cps[i].value) {
i += 1;
}
} else {
kind = if is_token_punct(cps[i].value) { AtomType::Punct } else { AtomType::Other };
i += 1;
}
let (start, stop) = (cps[begin].start, cps[i - 1].stop);
out.push(Atom { start, stop, kind, text: &text[start..stop] });
}
out
}
fn is_smile(text: &str) -> bool {
smile_at(text, 0) == Some(text.len())
}
fn token_join(
left_1: &Atom<'_>,
left_2: Option<&Atom<'_>>,
delimiter: &str,
right_1: &Atom<'_>,
right_2: Option<&Atom<'_>>,
buffer: &str,
) -> bool {
let around = |delim: char, pred: &dyn Fn(&Atom<'_>, &Atom<'_>) -> bool| -> bool {
if delimiter.starts_with(delim) {
return pred(left_1, right_1);
}
if !delimiter.is_empty() {
return false;
}
if let Some(left_2) = left_2.filter(|_| left_1.text.starts_with(delim)) {
return pred(left_2, right_1);
}
if let Some(right_2) = right_2.filter(|_| right_1.text.starts_with(delim)) {
return pred(left_1, right_2);
}
false
};
let not_punct =
|l: &Atom<'_>, r: &Atom<'_>| l.kind != AtomType::Punct && r.kind != AtomType::Punct;
let both_int = |l: &Atom<'_>, r: &Atom<'_>| l.kind == AtomType::Int && r.kind == AtomType::Int;
if "‑–—−-_".chars().any(|d| around(d, ¬_punct)) {
return true;
}
if ".,/\\".chars().any(|d| around(d, &both_int)) {
return true;
}
if left_1.kind == AtomType::Punct && right_1.kind == AtomType::Punct {
if is_smile(&format!("{buffer}{}", right_1.text)) {
return true;
}
if ".?!…".contains(left_1.text) && ".?!…".contains(right_1.text) {
return true;
}
if left_1.text == right_1.text && (left_1.text == "-" || left_1.text == "*") {
return true;
}
}
if left_1.kind == AtomType::Other
&& matches!(right_1.kind, AtomType::Other | AtomType::Uk | AtomType::Lat)
{
return true;
}
if matches!(left_1.kind, AtomType::Other | AtomType::Uk | AtomType::Lat)
&& right_1.kind == AtomType::Other
{
return true;
}
if delimiter.is_empty()
&& left_1.kind == AtomType::Int
&& matches!(right_1.kind, AtomType::Uk | AtomType::Lat)
&& abbrev::is_known(&lower_ascii_ukrainian(right_1.text))
{
return true;
}
right_1.text == "!" && lower_ascii_ukrainian(left_1.text) == "yahoo"
}
#[must_use]
pub fn tokenize(text: &str) -> Vec<Substring<'_>> {
let atoms = atoms(text);
let Some(first) = atoms.first() else {
return Vec::new();
};
let mut out = Vec::new();
let mut start = first.start;
let mut stop = first.stop;
for i in 1..atoms.len() {
let delimiter = &text[atoms[i - 1].stop..atoms[i].start];
let buffer = &text[start..stop];
let join = delimiter.is_empty()
&& token_join(
&atoms[i - 1],
atoms.get(i.wrapping_sub(2)),
delimiter,
&atoms[i],
atoms.get(i + 1),
buffer,
);
if !join {
push_substring(&mut out, text, start, stop, false);
start = atoms[i].start;
}
stop = atoms[i].stop;
}
push_substring(&mut out, text, start, stop, false);
out
}