use std::sync::LazyLock;
use super::fronting::{prefix_is_purely_fronting, word_before_marker_is_capitalised};
use super::language::Language;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MarkerPolicy {
pub digit_breaks: bool,
pub uppercase_breaks: bool,
}
pub struct MarkerDef {
pub matcher: SuffixMatcher,
pub policy: MarkerPolicy,
}
pub struct SuffixMatcher {
pub suffix: &'static str,
pub ignore_case: bool,
pub digit_only: bool,
}
impl SuffixMatcher {
#[inline]
fn strip<'h>(&self, head: &'h str) -> Option<&'h str> {
if self.ignore_case {
let idx = head.len().checked_sub(self.suffix.len())?;
head.as_bytes()[idx..]
.eq_ignore_ascii_case(self.suffix.as_bytes())
.then(|| &head[..idx])
} else {
head.strip_suffix(self.suffix)
}
}
}
pub struct MarkerTable {
markers: Box<[MarkerDef]>,
two_byte_filter: TwoByteFilter,
}
impl MarkerTable {
pub fn empty() -> &'static Self {
static EMPTY: LazyLock<MarkerTable> = LazyLock::new(|| MarkerTable {
markers: Box::from([]),
two_byte_filter: TwoByteFilter::EMPTY,
});
&EMPTY
}
pub fn build(markers: Vec<MarkerDef>) -> Self {
Self {
two_byte_filter: build_two_byte_filter(&markers),
markers: markers.into_boxed_slice(),
}
}
}
#[inline]
fn two_byte_key(second_last: u8, last: u8) -> usize {
((second_last.to_ascii_lowercase() as usize) << 8) | last.to_ascii_lowercase() as usize
}
struct TwoByteFilter([u64; 1024]);
impl TwoByteFilter {
const EMPTY: Self = Self([0; 1024]);
#[inline]
fn insert(&mut self, key: usize) {
self.0[key >> 6] |= 1u64 << (key & 63);
}
#[inline]
fn contains(&self, key: usize) -> bool {
self.0[key >> 6] & (1u64 << (key & 63)) != 0
}
}
fn build_two_byte_filter(markers: &[MarkerDef]) -> TwoByteFilter {
let mut filter = TwoByteFilter::EMPTY;
for marker in markers {
let suffix = marker.matcher.suffix.as_bytes();
let last = *suffix.last().expect("marker suffix must be non-empty");
match suffix {
[.., second_last, _] => filter.insert(two_byte_key(*second_last, last)),
_ => (0u8..=255).for_each(|hi| filter.insert(two_byte_key(hi, last))),
}
}
filter
}
pub(crate) struct MarkerMatch<'a> {
pub(crate) prefix: &'a str,
pub(crate) def: &'static MarkerDef,
}
fn strip_marker_suffix<'h>(
head_trimmed: &'h str,
markers: &'static [MarkerDef],
) -> Option<MarkerMatch<'h>> {
markers.iter().find_map(|marker| {
let prefix = marker.matcher.strip(head_trimmed)?;
let predecessor_ok = if marker.matcher.digit_only {
prefix
.trim_end()
.chars()
.next_back()
.is_some_and(|c| c.is_ascii_digit())
} else {
prefix
.chars()
.next_back()
.is_none_or(|c| c.is_whitespace() || c.is_ascii_digit())
};
predecessor_ok.then_some(MarkerMatch {
prefix,
def: marker,
})
})
}
#[inline]
pub(crate) fn classify_trailing_marker<'h>(
head: &'h str,
table: &'static MarkerTable,
) -> Option<MarkerMatch<'h>> {
let trimmed = head.trim_end();
let [.., second_last, last] = trimmed.as_bytes() else {
return None;
};
let key = two_byte_key(*second_last, *last);
if !table.two_byte_filter.contains(key) {
return None;
}
strip_marker_suffix(trimmed, &table.markers)
}
pub(crate) fn marker_bypasses_suppression<L: Language + ?Sized>(
marker: &MarkerMatch<'_>,
next_word: &str,
next_is_starter: bool,
lang: &L,
) -> bool {
let policy = marker.def.policy;
let mut chars = next_word.trim_start().chars();
let first = chars.next();
if first.is_some_and(|c| c.is_ascii_uppercase()) && chars.next() == Some('.') {
return false;
}
match first {
Some(c) if c.is_ascii_digit() => policy.digit_breaks,
Some(c) if c.is_uppercase() => {
next_is_starter
|| (policy.uppercase_breaks
&& !word_before_marker_is_capitalised(marker.prefix)
&& !prefix_is_purely_fronting(marker.prefix, lang))
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::super::language::Language;
use super::{MarkerDef, MarkerMatch, MarkerPolicy, SuffixMatcher, marker_bypasses_suppression};
struct StubLang;
impl Language for StubLang {}
const fn marker(uppercase_breaks: bool) -> MarkerDef {
MarkerDef {
matcher: SuffixMatcher {
suffix: "p.m",
ignore_case: true,
digit_only: false,
},
policy: MarkerPolicy {
digit_breaks: true,
uppercase_breaks,
},
}
}
const PREFIX: &str = "the sun sets at 7 ";
static CONT_MARKER: MarkerDef = marker(false);
static BREAK_MARKER: MarkerDef = marker(true);
#[test]
fn uppercase_breaks_false_keeps_suppression() {
let m = MarkerMatch {
prefix: PREFIX,
def: &CONT_MARKER,
};
assert!(!marker_bypasses_suppression(
&m,
"Tom wakes up",
false,
&StubLang
));
}
#[test]
fn starter_override_wins_over_uppercase_breaks_false() {
let m = MarkerMatch {
prefix: PREFIX,
def: &CONT_MARKER,
};
assert!(marker_bypasses_suppression(
&m,
"Tom wakes up",
true,
&StubLang
));
}
#[test]
fn uppercase_breaks_true_breaks_on_uppercase_follower() {
let m = MarkerMatch {
prefix: PREFIX,
def: &BREAK_MARKER,
};
assert!(marker_bypasses_suppression(
&m,
"Tom wakes up",
false,
&StubLang
));
}
}