use std::collections::HashMap;
use super::data::TokenizerData;
use super::lexeme::{Norms, is_space, prefix, suffix, word_shape};
use super::patterns::Patterns;
use super::specials::{RawToken, SpecialMatcher, Specials};
use super::token::SpacyToken;
use crate::error::Result;
use crate::g2p::pystr::is_py_space;
type Cache = HashMap<String, Vec<RawToken>>;
#[derive(Debug)]
pub struct SpacyTokenizer {
patterns: Patterns,
specials: Specials,
matcher: SpecialMatcher,
norms: Norms,
source: String,
load_cache: Cache,
}
impl SpacyTokenizer {
pub fn en_core_web_sm() -> Result<Self> {
Self::from_data(TokenizerData::en_core_web_sm()?)
}
fn from_data(data: TokenizerData) -> Result<Self> {
let mut tok = Self {
patterns: Patterns::new(&data)?,
specials: Specials::new(&data.rules),
matcher: SpecialMatcher::default(),
norms: Norms::new(data.lexeme_norm, data.base_norms),
source: data.source,
load_cache: Cache::new(),
};
let mut rules: Vec<&str> = data.rules.iter().map(|(chunk, _)| chunk.as_str()).collect();
rules.sort_unstable();
let mut cache = Cache::new();
for chunk in rules {
cache.clear();
let p = &tok.patterns;
if data.faster_heuristics
&& p.find_prefix(chunk) == 0
&& p.find_infix(chunk).is_empty()
&& p.find_suffix(chunk) == chunk.len()
&& !chunk.contains(' ')
{
continue;
}
let pattern = tok.tokenize_affixes(chunk, false, Some(&mut cache));
tok.matcher
.add(pattern.into_iter().map(|t| t.text).collect());
}
tok.load_cache = cache;
Ok(tok)
}
pub fn source(&self) -> &str {
&self.source
}
pub fn tokenize(&self, text: &str) -> Vec<SpacyToken> {
let doc = self.tokenize_affixes(text, true, None);
let doc = self.matcher.apply(doc, &self.specials);
doc.into_iter()
.map(|t| SpacyToken {
norm: t.norm.unwrap_or_else(|| self.norms.norm(&t.text)),
prefix: prefix(&t.text),
suffix: suffix(&t.text),
shape: word_shape(&t.text),
is_space: is_space(&t.text),
spacy: t.spacy,
text: t.text,
})
.collect()
}
fn tokenize_affixes(
&self,
text: &str,
with_specials: bool,
mut record: Option<&mut Cache>,
) -> Vec<RawToken> {
let mut doc = Vec::new();
let Some(first) = text.chars().next() else {
return doc;
};
let mut in_ws = is_py_space(first);
let mut start = 0usize;
for (i, c) in text.char_indices() {
if is_py_space(c) == in_ws {
continue;
}
if start < i {
self.tokenize_chunk(
&text[start..i],
with_specials,
&mut doc,
record.as_deref_mut(),
);
}
if c == ' ' {
if let Some(last) = doc.last_mut() {
last.spacy = true;
}
start = i + 1;
} else {
start = i;
}
in_ws = !in_ws;
}
if start < text.len() {
self.tokenize_chunk(&text[start..], with_specials, &mut doc, record);
if let Some(last) = doc.last_mut() {
last.spacy = text.ends_with(' ') && !in_ws;
}
}
doc
}
fn tokenize_chunk(
&self,
chunk: &str,
with_specials: bool,
doc: &mut Vec<RawToken>,
record: Option<&mut Cache>,
) {
if self.try_specials_and_cache(chunk, with_specials, doc) {
return;
}
let before = doc.len();
let mut prefixes = Vec::new();
let mut suffixes = Vec::new();
let rest = self.split_affixes(chunk, with_specials, &mut prefixes, &mut suffixes);
self.attach_tokens(doc, rest, &prefixes, &suffixes, with_specials);
if let Some(cache) = record
&& doc.len() > before
{
cache
.entry(chunk.to_string())
.or_insert_with(|| doc[before..].to_vec());
}
}
fn try_specials_and_cache(
&self,
chunk: &str,
with_specials: bool,
doc: &mut Vec<RawToken>,
) -> bool {
if with_specials && self.specials.push(chunk, doc) {
return true;
}
match self.load_cache.get(chunk) {
Some(tokens) => {
doc.extend(tokens.iter().cloned());
true
}
None => false,
}
}
fn split_affixes<'a>(
&self,
mut s: &'a str,
with_specials: bool,
prefixes: &mut Vec<&'a str>,
suffixes: &mut Vec<&'a str>,
) -> &'a str {
let p = &self.patterns;
let special = |x: &str| with_specials && self.specials.contains(x);
let mut last_size = 0usize;
while !s.is_empty() && s.len() != last_size {
if p.token_match(s) || special(s) {
break;
}
last_size = s.len();
let pre_end = p.find_prefix(s);
if pre_end != 0 {
let minus_pre = &s[pre_end..];
if !minus_pre.is_empty() && special(minus_pre) {
prefixes.push(&s[..pre_end]);
s = minus_pre;
break;
}
}
let suf_start = pre_end + p.find_suffix(&s[pre_end..]);
if suf_start != s.len() {
let minus_suf = &s[..suf_start];
if !minus_suf.is_empty() && special(minus_suf) {
suffixes.push(&s[suf_start..]);
s = minus_suf;
break;
}
}
let has_pre = pre_end != 0;
let has_suf = suf_start != s.len();
if has_pre && has_suf && pre_end <= suf_start {
prefixes.push(&s[..pre_end]);
suffixes.push(&s[suf_start..]);
s = &s[pre_end..suf_start];
} else if has_pre {
prefixes.push(&s[..pre_end]);
s = &s[pre_end..];
} else if has_suf {
suffixes.push(&s[suf_start..]);
s = &s[..suf_start];
}
}
s
}
fn attach_tokens(
&self,
doc: &mut Vec<RawToken>,
core: &str,
prefixes: &[&str],
suffixes: &[&str],
with_specials: bool,
) {
doc.extend(prefixes.iter().map(|p| RawToken::lex(p)));
if !core.is_empty() && !self.try_specials_and_cache(core, with_specials, doc) {
let p = &self.patterns;
let infixes = if p.token_match(core) || p.url_match(core) {
Vec::new()
} else {
p.find_infix(core)
};
if infixes.is_empty() {
doc.push(RawToken::lex(core));
} else {
let mut start = 0usize;
for (infix_start, infix_end) in infixes {
if infix_start == 0 {
continue;
}
if infix_start != start {
doc.push(RawToken::lex(&core[start..infix_start]));
}
if infix_start != infix_end {
doc.push(RawToken::lex(&core[infix_start..infix_end]));
}
start = infix_end;
}
if start < core.len() {
doc.push(RawToken::lex(&core[start..]));
}
}
}
doc.extend(suffixes.iter().rev().map(|s| RawToken::lex(s)));
}
}