#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod contractions;
mod emoji;
mod patterns;
use regex::Regex;
use std::collections::HashMap;
use patterns::Rule;
pub mod tags {
use crate::patterns;
pub const WORD: &str = patterns::WORD_CAT;
pub const NUMBER: &str = patterns::NUMBER_CAT;
pub const ORDINAL: &str = patterns::ORDINAL_CAT;
pub const EMAIL: &str = patterns::EMAIL_CAT;
pub const URL: &str = patterns::URL_CAT;
pub const MENTION: &str = patterns::MENTION_CAT;
pub const HASHTAG: &str = patterns::HASHTAG_CAT;
pub const EMOJI: &str = patterns::EMOJI_CAT;
pub const EMOTICON: &str = patterns::EMOTICON_CAT;
pub const TIME: &str = patterns::TIME_CAT;
pub const CURRENCY: &str = patterns::CURRENCY_CAT;
pub const QUOTED_PHRASE: &str = patterns::QUOTED_PHRASE_CAT;
pub const PUNCTUATION: &str = patterns::PUNCTUATION_CAT;
pub const SYMBOL: &str = patterns::SYMBOL_CAT;
pub const ALIEN: &str = "alien";
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
pub value: String,
pub tag: String,
}
impl Token {
fn new(value: impl Into<String>, tag: impl Into<String>) -> Self {
Token {
value: value.into(),
tag: tag.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AddError {
MissingFingerprint {
tag: String,
},
TagExists {
tag: String,
},
}
impl std::fmt::Display for AddError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AddError::MissingFingerprint { tag } => write!(
f,
"Tag {tag} doesn't exist; Provide a 'fingerprintCode' to add it as a tag."
),
AddError::TagExists { tag } => write!(f, "Tag {tag} already exists"),
}
}
}
impl std::error::Error for AddError {}
fn default_fingerprint_codes() -> HashMap<String, String> {
let pairs = [
("emoticon", "c"),
("email", "e"),
("emoji", "j"),
("hashtag", "h"),
("mention", "m"),
("number", "n"),
("ordinal", "o"),
("quoted_phrase", "q"),
("currency", "r"),
("time", "t"),
("url", "u"),
("word", "w"),
("alien", "z"),
];
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect()
}
enum Unit {
Token(Token),
Fragment(String),
}
#[derive(Debug)]
pub struct Tokenizer {
master: Vec<Rule>,
rgxs: Vec<Rule>,
final_tokens: Vec<Token>,
fingerprint_codes: HashMap<String, String>,
contraction: Regex,
pos_singular: Regex,
pos_plural: Regex,
spaces: Regex,
}
impl Default for Tokenizer {
fn default() -> Self {
Self::new()
}
}
impl Tokenizer {
pub fn new() -> Self {
let master = patterns::master();
let mut tk = Tokenizer {
rgxs: Vec::new(),
master,
final_tokens: Vec::new(),
fingerprint_codes: default_fingerprint_codes(),
contraction: Regex::new(r"'").expect("contraction pattern compiles"),
pos_singular: Regex::new(patterns::POS_SINGULAR).expect("possessive pattern compiles"),
pos_plural: Regex::new(patterns::POS_PLURAL).expect("possessive pattern compiles"),
spaces: Regex::new(patterns::SPACES).expect("spaces pattern compiles"),
};
tk.define_config(&[("quoted_phrase", false)]);
tk
}
pub fn tokenize(&mut self, sentence: &str) -> Vec<Token> {
self.final_tokens.clear();
let rules = std::mem::take(&mut self.rgxs);
self.tokenize_recursive(sentence, &rules);
self.rgxs = rules;
self.final_tokens.clone()
}
pub fn define_config(&mut self, config: &[(&str, bool)]) -> usize {
if config.is_empty() {
self.rgxs = Vec::new();
} else {
let map: HashMap<&str, bool> = config.iter().copied().collect();
self.rgxs = self
.master
.iter()
.filter(|rule| match map.get(rule.category.as_str()) {
Some(flag) => *flag,
None => true,
})
.cloned()
.collect();
}
let mut unique: Vec<&str> = Vec::new();
for rule in &self.rgxs {
if !unique.contains(&rule.category.as_str()) {
unique.push(rule.category.as_str());
}
}
self.fingerprint_codes = default_fingerprint_codes();
unique.len()
}
pub fn get_tokens_fp(&self) -> String {
let mut fp = String::new();
for token in &self.final_tokens {
match self.fingerprint_codes.get(&token.tag) {
Some(code) if !code.is_empty() => fp.push_str(code),
_ => fp.push_str(&token.value),
}
}
fp
}
pub fn add_regex(
&mut self,
regex: Regex,
tag: &str,
fingerprint_code: Option<&str>,
) -> Result<(), AddError> {
let known = self.has_code(tag) || self.has_category(tag);
let code_present = fingerprint_code.is_some_and(|c| !c.is_empty());
if !known && !code_present {
return Err(AddError::MissingFingerprint {
tag: tag.to_string(),
});
}
if !known {
self.add_tag(tag, fingerprint_code.expect("checked above"))?;
}
self.rgxs.insert(
0,
Rule {
regex,
category: tag.to_string(),
},
);
Ok(())
}
pub fn add_tag(&mut self, name: &str, fingerprint_code: &str) -> Result<(), AddError> {
if fingerprint_code.is_empty() {
return Err(AddError::MissingFingerprint {
tag: name.to_string(),
});
}
if self.fingerprint_codes.contains_key(name) {
return Err(AddError::TagExists {
tag: name.to_string(),
});
}
self.fingerprint_codes
.insert(name.to_string(), fingerprint_code.to_string());
Ok(())
}
fn has_code(&self, tag: &str) -> bool {
self.fingerprint_codes
.get(tag)
.is_some_and(|c| !c.is_empty())
}
fn has_category(&self, tag: &str) -> bool {
self.master.iter().any(|rule| rule.category == tag)
}
fn tokenize_recursive(&mut self, text: &str, rules: &[Rule]) {
if rules.is_empty() {
for piece in self.spaces.split(text) {
self.final_tokens.push(Token::new(piece, "alien"));
}
return;
}
let sentence = text.trim();
let units = self.tokenize_unit(sentence, &rules[0]);
for unit in units {
match unit {
Unit::Fragment(frag) => self.tokenize_recursive(&frag, &rules[1..]),
Unit::Token(token) => self.final_tokens.push(token),
}
}
}
fn tokenize_unit(&self, text: &str, rule: &Rule) -> Vec<Unit> {
let mut units = Vec::new();
let is_word = rule.category == "word";
let mut last = 0;
for m in rule.regex.find_iter(text) {
let gap = &text[last..m.start()];
let trimmed = gap.trim();
if !trimmed.is_empty() {
units.push(Unit::Fragment(trimmed.to_string()));
}
let matched = m.as_str();
if is_word && self.contraction.is_match(matched) {
self.manage_contraction(matched, &mut units);
} else {
units.push(Unit::Token(Token::new(matched, rule.category.clone())));
}
last = m.end();
}
let tail = text[last..].trim();
if !tail.is_empty() {
units.push(Unit::Fragment(tail.to_string()));
}
units
}
fn manage_contraction(&self, word: &str, units: &mut Vec<Unit>) {
if let Some(parts) = contractions::lookup(word) {
for part in parts {
units.push(Unit::Token(Token::new(*part, "word")));
}
return;
}
if let Some(caps) = self.pos_singular.captures(word) {
units.push(Unit::Token(Token::new(&caps[1], "word")));
units.push(Unit::Token(Token::new(&caps[2], "word")));
return;
}
if let Some(caps) = self.pos_plural.captures(word) {
units.push(Unit::Token(Token::new(&caps[1], "word")));
units.push(Unit::Token(Token::new(&caps[2], "word")));
return;
}
units.push(Unit::Token(Token::new(word, "word")));
}
}
pub const CONTRACTION_COUNT: usize = contractions::COUNT;