use pcre2::bytes::{Regex as Pcre2Regex, RegexBuilder as Pcre2RegexBuilder};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, LazyLock, RwLock};
pub const DEFAULT_WORD_CHARS: &str = r"\w\-'";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WordDefinition {
chars: String,
}
impl WordDefinition {
#[must_use]
pub fn new(chars: impl Into<String>) -> Self {
let chars = chars.into();
let trimmed = chars.trim();
let inner = trimmed
.strip_prefix('[')
.and_then(|value| value.strip_suffix(']'))
.unwrap_or(trimmed);
Self {
chars: inner.to_string(),
}
}
#[must_use]
pub fn chars(&self) -> &str {
&self.chars
}
#[must_use]
pub fn char_class(&self) -> String {
format!("[{}]", self.chars)
}
#[must_use]
pub fn word_regex(&self) -> String {
format!("[{}]+", self.chars)
}
#[must_use]
pub fn boundary(&self) -> String {
let class = self.char_class();
format!("(?:(?={class})(?<!{class})|(?<={class})(?!{class}))")
}
#[must_use]
pub fn compiled_boundary(&self) -> Pcre2Regex {
compile_boundary(&self.boundary())
}
}
impl Default for WordDefinition {
fn default() -> Self {
Self::new(DEFAULT_WORD_CHARS)
}
}
struct WordState {
definition: WordDefinition,
boundary: Arc<Pcre2Regex>,
char_class: Arc<str>,
word_regex: Arc<str>,
boundary_str: Arc<str>,
}
impl WordState {
fn build(definition: WordDefinition) -> Self {
let boundary_str: Arc<str> = Arc::from(definition.boundary());
let boundary = Arc::new(compile_boundary(&boundary_str));
Self {
char_class: Arc::from(definition.char_class()),
word_regex: Arc::from(definition.word_regex()),
boundary_str,
definition,
boundary,
}
}
}
static STATE: LazyLock<RwLock<WordState>> =
LazyLock::new(|| RwLock::new(WordState::build(WordDefinition::default())));
pub fn configure_word_definition(definition: WordDefinition) {
let next = WordState::build(definition);
*STATE.write().expect("word definition lock poisoned") = next;
}
#[must_use]
pub fn word_definition() -> WordDefinition {
STATE
.read()
.expect("word definition lock poisoned")
.definition
.clone()
}
#[must_use]
pub fn tokenizer_boundary() -> Arc<Pcre2Regex> {
STATE
.read()
.expect("word definition lock poisoned")
.boundary
.clone()
}
#[must_use]
pub fn char_class() -> Arc<str> {
STATE
.read()
.expect("word definition lock poisoned")
.char_class
.clone()
}
#[must_use]
pub fn word_regex() -> Arc<str> {
STATE
.read()
.expect("word definition lock poisoned")
.word_regex
.clone()
}
#[must_use]
pub fn boundary_pattern() -> Arc<str> {
STATE
.read()
.expect("word definition lock poisoned")
.boundary_str
.clone()
}
fn compile_boundary(pattern: &str) -> Pcre2Regex {
Pcre2RegexBuilder::new()
.utf(true)
.ucp(true)
.jit_if_available(true)
.build(pattern)
.expect("valid word boundary regex")
}
#[cfg(test)]
pub(crate) static WORD_DEF_TEST_LOCK: RwLock<()> = RwLock::new(());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_matches_legacy_hardcoded_forms() {
let def = WordDefinition::default();
assert_eq!(def.char_class(), r"[\w\-']");
assert_eq!(def.word_regex(), r"[\w\-']+");
assert_eq!(
def.boundary(),
r"(?:(?=[\w\-'])(?<![\w\-'])|(?<=[\w\-'])(?![\w\-']))"
);
}
#[test]
fn new_strips_surrounding_brackets() {
assert_eq!(WordDefinition::new(r"[\w\-']").chars(), r"\w\-'");
assert_eq!(WordDefinition::new(r"\w\-'").chars(), r"\w\-'");
}
#[test]
fn configured_definition_changes_derived_forms_and_is_reset() {
let _guard = WORD_DEF_TEST_LOCK
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
configure_word_definition(WordDefinition::new(r"\w"));
assert_eq!(word_definition().word_regex(), r"[\w]+");
configure_word_definition(WordDefinition::default());
assert_eq!(word_definition().word_regex(), r"[\w\-']+");
}
}