use std::collections::HashSet;
pub const DEFAULT_DELIMITERS: &[char] = &[' ', '\n', '\t'];
fn is_word_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Segment {
bytes: Vec<u8>,
pub start: usize,
pub end: usize,
pub index: usize,
pub is_partial_special: bool,
}
impl Segment {
#[must_use]
pub fn new(bytes: Vec<u8>, start: usize, end: usize) -> Self {
Self {
bytes,
start,
end,
index: 0,
is_partial_special: false,
}
}
#[must_use]
pub fn with_index(bytes: Vec<u8>, start: usize, end: usize, index: usize) -> Self {
Self {
bytes,
start,
end,
index,
is_partial_special: false,
}
}
#[must_use]
pub fn from_str(text: &str, start: usize, end: usize) -> Self {
Self {
bytes: text.as_bytes().to_vec(),
start,
end,
index: 0,
is_partial_special: false,
}
}
#[must_use]
pub fn partial_special(bytes: Vec<u8>, start: usize, end: usize, index: usize) -> Self {
Self {
bytes,
start,
end,
index,
is_partial_special: true,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(&self.bytes) }
}
#[must_use]
pub fn text(&self) -> String {
self.as_str().to_owned()
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn len(&self) -> usize {
self.bytes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
impl std::fmt::Display for Segment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text())
}
}
#[derive(Debug, Clone)]
pub struct Tokenizer {
special_tokens: Vec<String>,
delimiters: HashSet<char>,
sorted_specials: Vec<String>,
needs_build: bool,
}
impl Default for Tokenizer {
fn default() -> Self {
Self {
special_tokens: Vec::new(),
delimiters: DEFAULT_DELIMITERS.iter().copied().collect(),
sorted_specials: Vec::new(),
needs_build: false,
}
}
}
impl Tokenizer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_specials_and_delimiters(
special_tokens: Vec<String>,
delimiters: Vec<char>,
) -> Self {
let mut tokenizer = Self {
special_tokens: Vec::new(),
delimiters: delimiters.into_iter().collect(),
sorted_specials: Vec::new(),
needs_build: false,
};
for token in special_tokens {
tokenizer.add_special(token);
}
tokenizer.build();
tokenizer
}
pub fn add_special(&mut self, token: String) {
if !self.special_tokens.contains(&token) {
self.special_tokens.push(token);
self.needs_build = true;
}
}
#[must_use]
pub fn specials(&self) -> &Vec<String> {
&self.special_tokens
}
pub fn build(&mut self) {
if !self.needs_build {
return;
}
self.sorted_specials = self.special_tokens.clone();
self.sorted_specials
.sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a.cmp(b)));
self.sorted_specials.dedup();
self.needs_build = false;
}
fn is_delimiter(&self, c: char) -> bool {
self.delimiters.contains(&c)
}
fn match_special(&self, text: &str) -> Option<usize> {
for token in &self.sorted_specials {
if token.is_empty() || !text.starts_with(token) {
continue;
}
let ends_word = token.chars().next_back().is_some_and(is_word_char);
let next_is_word = text[token.len()..].chars().next().is_some_and(is_word_char);
if ends_word && next_is_word {
continue;
}
return Some(token.len());
}
None
}
fn prefix_special(&self, text: &str) -> bool {
self.sorted_specials
.iter()
.any(|token| token.len() > text.len() && token.starts_with(text))
}
pub fn tokenize(&mut self, input: &str) -> Result<Vec<Segment>, String> {
self.build();
let mut segments = Vec::new();
let mut byte_pos = 0;
let bytes = input.as_bytes();
while byte_pos < bytes.len() {
let remaining = &input[byte_pos..];
if let Some(match_len) = self.match_special(remaining) {
let index = segments.len();
let matched_bytes = remaining.as_bytes()[..match_len].to_vec();
segments.push(Segment::with_index(
matched_bytes,
byte_pos,
byte_pos + match_len,
index,
));
byte_pos += match_len;
continue;
}
if self.prefix_special(remaining) {
let index = segments.len();
segments.push(Segment::partial_special(
remaining.as_bytes().to_vec(),
byte_pos,
bytes.len(),
index,
));
break;
}
let Some(c) = remaining.chars().next() else {
break;
};
if self.is_delimiter(c) {
byte_pos += c.len_utf8();
continue;
}
let start_pos = byte_pos;
let mut token_end = byte_pos;
let mut prev_char: Option<char> = None;
for (i, c) in remaining.char_indices() {
let abs_pos = byte_pos + i;
if self.is_delimiter(c) {
break;
}
let substr = &input[abs_pos..];
if self.match_special(substr).is_some() {
let prev_is_word = prev_char.is_some_and(is_word_char);
if !(is_word_char(c) && prev_is_word) {
break;
}
}
if i > 0 && self.prefix_special(substr) {
let prev_is_word = prev_char.is_some_and(|p| p.is_alphanumeric() || p == '_');
let curr_is_word = c.is_alphanumeric() || c == '_';
if !(prev_is_word && curr_is_word) {
break;
}
}
token_end = abs_pos + c.len_utf8();
prev_char = Some(c);
}
if token_end > start_pos {
let index = segments.len();
let token_bytes = input.as_bytes()[start_pos..token_end].to_vec();
segments.push(Segment::with_index(
token_bytes,
start_pos,
token_end,
index,
));
byte_pos = token_end;
} else {
return Err("Found weird unexpected char !".to_string());
}
}
Ok(segments)
}
}