mod rules;
#[cfg(test)]
mod tests;
use crate::dictionary::{self, Dictionaries, DictionaryScript, Run, RunScript};
use crate::{GraphemeProps, GraphemeState, grapheme_props};
const CLASS_MASK: u8 = 0x1F;
const PICTOGRAPHIC: u8 = 0x20;
const RUN_SHIFT: u32 = 6;
const RUN_SOUTHEAST_ASIAN: u8 = 1;
const RUN_CHINESE_JAPANESE: u8 = 2;
const END: u8 = u8::MAX;
const _: () = assert!(
rules::CHINESE_JAPANESE_LETTER == rules::FULL_STOP + 1
&& rules::CLASSES == rules::CHINESE_JAPANESE_LETTER as usize + 1
);
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub(crate) struct WordProps(u8);
impl WordProps {
#[inline]
pub(crate) const fn from_bits(bits: u8) -> Self {
Self(bits)
}
#[inline]
const fn class(self) -> u8 {
self.0 & CLASS_MASK
}
#[inline]
const fn is_pictographic(self) -> bool {
self.0 & PICTOGRAPHIC != 0
}
#[inline]
const fn run(self) -> u8 {
self.0 >> RUN_SHIFT
}
}
#[inline]
pub(crate) fn run_script(ch: char) -> Option<RunScript> {
match crate::props::word_props(ch).run() {
RUN_SOUTHEAST_ASIAN => Some(RunScript::from_sa_char(ch)),
RUN_CHINESE_JAPANESE => Some(RunScript::ChineseJapanese),
_ => None,
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub struct WordOptions {
pub posix_full_stop: bool,
}
impl WordOptions {
#[inline]
pub const fn new() -> Self {
Self {
posix_full_stop: false,
}
}
}
#[derive(Clone, Debug)]
pub struct WordBreaks<'d, I> {
scalars: I,
state: u8,
after_zwj: bool,
resolved: [u8; 2],
least: Option<usize>,
started: bool,
dictionaries: Option<&'d Dictionaries>,
cj_runs: bool,
run: Run,
after_katakana: bool,
#[cfg(test)]
test: tests::TestState,
}
impl<'d, I: Iterator<Item = (usize, char)> + Clone> WordBreaks<'d, I> {
#[inline]
pub fn new(scalars: I, options: WordOptions) -> Self {
Self {
scalars,
state: rules::START,
after_zwj: false,
resolved: [
if options.posix_full_stop {
rules::MID_NUMBER
} else {
rules::MID_NUM_LETTER
},
rules::LETTER,
],
least: Some(0),
started: false,
dictionaries: None,
cj_runs: false,
run: Run::default(),
after_katakana: false,
#[cfg(test)]
test: tests::TestState::default(),
}
}
#[inline]
pub fn with_dictionaries(mut self, dictionaries: &'d Dictionaries) -> Self {
self.dictionaries = Some(dictionaries);
self.cj_runs = dictionaries
.get(DictionaryScript::ChineseJapanese)
.is_some();
if self.cj_runs {
self.resolved[1] = rules::OTHER;
}
self
}
#[inline(always)]
fn props(&self, ch: char) -> WordProps {
#[cfg(test)]
if let Some(classes) = self.test.classes {
return WordProps(classes.get(ch as usize).copied().unwrap_or_default());
}
crate::props::word_props(ch)
}
#[inline(always)]
fn class(&self, props: WordProps) -> u8 {
#[cfg(test)]
if self.test.legacy & tests::Legacy::COMPLEX_CLASS != 0 && self.is_complex_props(props) {
return rules::OTHER;
}
let class = props.class();
if class < rules::FULL_STOP {
class
} else {
self.resolved
.get(usize::from(class - rules::FULL_STOP))
.copied()
.unwrap_or(rules::OTHER)
}
}
#[inline(always)]
fn step(&mut self, offset: usize, ch: char, decides: bool) -> bool {
let props = self.props(ch);
let class = self.class(props);
let run = if props.run() != 0 {
self.run_step(offset, ch, props.run(), class)
} else {
self.run.end();
None
};
#[cfg(test)]
let run = if self.test.legacy != 0 {
self.model_step(offset, ch, run)
} else {
run
};
let (answer, next) = rules::pair(self.state, class);
#[cfg(test)]
let answer = self.icu_answer(answer, class);
let breaks = decides
&& match run {
Some(breaks) => breaks,
None => self.answer(answer, props),
};
self.state = next;
self.after_zwj = class == rules::ZWJ;
breaks
}
#[inline(always)]
fn answer(&self, answer: u8, props: WordProps) -> bool {
if self.after_zwj && props.is_pictographic() {
return false;
}
match answer {
rules::KEEP => false,
rules::BREAK => true,
waits => !rules::holds(waits, self.next_class()),
}
}
#[cold]
#[inline(never)]
fn next_class(&self) -> u8 {
let mut least = self.least.unwrap_or(usize::MAX);
for (offset, ch) in self.scalars.clone() {
if offset < least {
break;
}
least = offset.saturating_add(1);
let class = self.class(self.props(ch));
if !rules::is_attached(class) {
return class;
}
}
END
}
#[inline(never)]
fn run_step(&mut self, offset: usize, ch: char, bits: u8, class: u8) -> Option<bool> {
let script = if bits == RUN_SOUTHEAST_ASIAN {
RunScript::from_sa_char(ch)
} else if self.cj_runs {
RunScript::ChineseJapanese
} else {
self.run.end();
return None;
};
let mut run = self.run;
let katakana_start = !(self.after_katakana && run.script() == Some(script));
let cj = script == RunScript::ChineseJapanese;
let breaks = run.step(
offset,
script,
rules::is_attached(class),
self.dictionaries,
|dictionary| {
let continues = |next| self.continues_run(next, script);
let props = |ch| self.grapheme_props(ch);
let rest = self.scalars.clone();
let start = self.grapheme_start();
if cj {
dictionary::find_next_cj_word(
dictionary,
offset,
ch,
rest,
continues,
props,
start,
katakana_start,
)
} else {
dictionary::find_next_word(
dictionary, offset, ch, rest, continues, props, start,
)
}
},
);
self.run = run;
self.after_katakana = cj && dictionary::is_katakana(ch);
breaks
}
#[inline]
fn continues_run(&self, ch: char, script: RunScript) -> bool {
let bits = self.props(ch).run();
if script == RunScript::ChineseJapanese {
bits == RUN_CHINESE_JAPANESE
} else {
bits == RUN_SOUTHEAST_ASIAN && RunScript::from_sa_char(ch) == script
}
}
#[inline(always)]
fn grapheme_props(&self, ch: char) -> GraphemeProps {
#[cfg(test)]
if let Some(classes) = self.test.graphemes {
return classes.get(ch as usize).copied().unwrap_or_default();
}
grapheme_props(ch)
}
#[inline(always)]
fn grapheme_start(&self) -> GraphemeState {
#[cfg(test)]
if self.test.graphemes_17 {
return GraphemeState::with_unicode_17_conjuncts();
}
GraphemeState::new()
}
}
impl<I: Iterator<Item = (usize, char)> + Clone> Iterator for WordBreaks<'_, I> {
type Item = usize;
#[inline]
fn next(&mut self) -> Option<usize> {
if !self.started {
self.started = true;
match self.scalars.next() {
Some((offset, ch)) => {
self.least = Some(offset.saturating_add(1));
self.step(offset, ch, false);
}
None => self.least = None,
}
}
let mut least = self.least?;
while let Some((offset, ch)) = self.scalars.next() {
if offset < least {
break;
}
least = offset.saturating_add(1);
self.least = Some(least);
if self.step(offset, ch, true) {
return Some(offset);
}
}
self.least = None;
None
}
}
impl<I: Iterator<Item = (usize, char)> + Clone> core::iter::FusedIterator for WordBreaks<'_, I> {}