use std::borrow::Cow;
use unicode_categories::UnicodeCategories;
use unicode_normalization::UnicodeNormalization;
use unicode_segmentation::UnicodeSegmentation;
use crate::config::CanonicalizeConfig;
use crate::document::CanonicalizedDocument;
use crate::error::CanonicalError;
use crate::hash::{hash_canonical_bytes, hash_token_bytes};
use crate::token::Token;
pub fn canonicalize(
doc_id: impl Into<String>,
input: &str,
cfg: &CanonicalizeConfig,
) -> Result<CanonicalizedDocument, CanonicalError> {
if cfg.version == 0 {
return Err(CanonicalError::InvalidConfig(
"config version must be >= 1".into(),
));
}
let doc_id: String = doc_id.into();
let trimmed = doc_id.trim();
if trimmed.is_empty() {
return Err(CanonicalError::MissingDocId);
}
let doc_id = if doc_id.len() == trimmed.len() {
doc_id
} else {
trimmed.to_string()
};
let normalized_text: Cow<str> = if cfg.normalize_unicode {
Cow::Owned(input.nfkc().collect::<String>())
} else {
Cow::Borrowed(input)
};
let mut canonical_text = String::with_capacity(normalized_text.len());
let mut tokens: Vec<Token> = Vec::with_capacity((normalized_text.len() / 4).saturating_add(1));
let mut pending_space = false;
let mut current_token_start: Option<usize> = None;
process_chars(
normalized_text.as_ref(),
cfg,
&mut canonical_text,
&mut tokens,
&mut pending_space,
&mut current_token_start,
);
finalize_token(&mut tokens, &canonical_text, &mut current_token_start);
if canonical_text.is_empty() {
return Err(CanonicalError::EmptyInput);
}
let canonical_version = cfg.version;
let mut token_hashes: Vec<String> = Vec::with_capacity(tokens.len());
token_hashes.extend(
tokens
.iter()
.map(|t| hash_token_bytes(canonical_version, t.text.as_bytes())),
);
let sha256_hex = hash_canonical_bytes(canonical_version, canonical_text.as_bytes());
Ok(CanonicalizedDocument {
doc_id,
canonical_text,
tokens,
token_hashes,
sha256_hex,
canonical_version,
config: cfg.clone(),
})
}
fn process_chars(
text: &str,
cfg: &CanonicalizeConfig,
canonical_text: &mut String,
tokens: &mut Vec<Token>,
pending_space: &mut bool,
current_token_start: &mut Option<usize>,
) {
for grapheme in text.graphemes(true) {
if cfg.lowercase {
for lower in grapheme.to_lowercase().chars() {
dispatch_char(
lower,
cfg,
canonical_text,
tokens,
pending_space,
current_token_start,
);
}
} else {
for ch in grapheme.chars() {
dispatch_char(
ch,
cfg,
canonical_text,
tokens,
pending_space,
current_token_start,
);
}
}
}
}
fn dispatch_char(
ch: char,
cfg: &CanonicalizeConfig,
canonical_text: &mut String,
tokens: &mut Vec<Token>,
pending_space: &mut bool,
current_token_start: &mut Option<usize>,
) {
let is_delim = ch.is_whitespace() || (cfg.strip_punctuation && ch.is_punctuation());
if is_delim {
finalize_token(tokens, canonical_text, current_token_start);
if !canonical_text.is_empty() {
*pending_space = true;
}
} else {
append_char(ch, canonical_text, current_token_start, pending_space);
}
}
fn append_char(
ch: char,
canonical_text: &mut String,
current_token_start: &mut Option<usize>,
pending_space: &mut bool,
) {
if *pending_space {
canonical_text.push(' ');
*pending_space = false;
*current_token_start = Some(canonical_text.len());
} else if current_token_start.is_none() {
*current_token_start = Some(canonical_text.len());
}
canonical_text.push(ch);
}
fn finalize_token(
tokens: &mut Vec<Token>,
canonical_text: &str,
current_token_start: &mut Option<usize>,
) {
if let Some(start) = current_token_start.take() {
if start < canonical_text.len() {
let end = canonical_text.len();
tokens.push(Token {
text: canonical_text[start..end].to_string(),
start,
end,
});
}
}
}