mod rules;
#[cfg(test)]
mod tests;
use crate::dictionary::{self, Dictionaries, Run, RunScript};
use crate::{GraphemeProps, GraphemeState, LineBreak, grapheme_props};
use rules::{
AFTER_ZWJ, AL, BEFORE_EAST_ASIAN, BREAK, CL, CM, EOT, KEEP, LEFT_EAST_ASIAN, NEEDS_NEXT,
NEXT_EAST_ASIAN, OP, Pair, QU_PF, QU_PI, RESOLVE, RI, RI_EVEN, RIGHT_EAST_ASIAN, Rules, SA,
SA_MARK, SOT, Tailoring, UNREAD, ZWJ,
};
const CLASS_MASK: u8 = 0x3F;
const EAST_ASIAN: u8 = 0x40;
const UNASSIGNED_PICTOGRAPHIC: u8 = 0x80;
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub(crate) struct LineProps(u8);
impl LineProps {
#[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 flags(self) -> u8 {
self.0 & (EAST_ASIAN | UNASSIGNED_PICTOGRAPHIC)
}
#[inline]
pub(crate) const fn line_break(self) -> LineBreak {
let class = self.class() as usize;
LineBreak::new(if class < rules::DATA_CLASS_COUNT {
rules::LINE_BREAK[class]
} else {
0
})
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub enum LineStrictness {
Loose,
Normal,
#[default]
Strict,
Anywhere,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub enum LineWordOption {
#[default]
Normal,
BreakAll,
KeepAll,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub struct LineOptions {
pub strictness: LineStrictness,
pub word: LineWordOption,
pub ja_zh: bool,
pub cj_quotes: bool,
pub loose_hyphens: bool,
}
impl LineOptions {
#[inline]
pub const fn new() -> Self {
Self {
strictness: LineStrictness::Strict,
word: LineWordOption::Normal,
ja_zh: false,
cj_quotes: false,
loose_hyphens: false,
}
}
const fn rules(self) -> Rules {
let tailoring = match self.strictness {
LineStrictness::Loose => Tailoring::Loose,
LineStrictness::Normal if self.ja_zh => Tailoring::NormalJapaneseChinese,
_ => Tailoring::None,
};
Rules {
tailoring,
ja_zh: self.ja_zh,
keep_all: matches!(self.word, LineWordOption::KeepAll),
loose_hyphens: matches!(self.strictness, LineStrictness::Loose)
&& (self.ja_zh
|| self.loose_hyphens
|| matches!(self.word, LineWordOption::BreakAll)),
#[cfg(test)]
legacy: 0,
}
}
const fn resolve(self) -> &'static [u8; 64] {
&RESOLVE[self.resolve_index()]
}
const fn resolve_index(self) -> usize {
let strictness = match self.strictness {
LineStrictness::Strict | LineStrictness::Anywhere => 0,
LineStrictness::Normal => 1 + self.ja_zh as usize,
LineStrictness::Loose => 3 + self.ja_zh as usize,
};
let break_all = matches!(self.word, LineWordOption::BreakAll);
strictness * 2 + break_all as usize
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
struct ScanState {
left: u8,
flags: u8,
}
#[derive(Clone, Debug)]
pub struct LineBreaks<'d, I> {
scalars: I,
state: ScanState,
graphemes: GraphemeState,
anywhere: bool,
cj_quotes: bool,
rules: Rules,
resolve: &'static [u8; 64],
least: Option<usize>,
started: bool,
reads_runs: bool,
dictionaries: Option<&'d Dictionaries>,
run: Run,
#[cfg(test)]
test: TestState,
}
#[cfg(test)]
#[derive(Clone, Debug, Default)]
struct TestState {
classes: Option<&'static [u8]>,
grapheme_classes: Option<&'static [crate::GraphemeProps]>,
graphemes_17: bool,
model: Option<(std::vec::Vec<usize>, Option<usize>)>,
last: Option<(usize, char, bool)>,
}
impl<'d, I: Iterator<Item = (usize, char)> + Clone> LineBreaks<'d, I> {
#[inline]
pub fn new(scalars: I, options: LineOptions) -> Self {
Self::from_options(scalars, options)
}
#[inline]
pub fn with_dictionaries(mut self, dictionaries: &'d Dictionaries) -> Self {
self.dictionaries = Some(dictionaries);
self
}
pub fn with_preceding(
scalars: I,
options: LineOptions,
preceding: impl IntoIterator<Item = char>,
) -> Self {
let mut breaks = Self::from_options(scalars, options);
let mut marked = false;
for ch in preceding {
let (class, bits) = breaks.classify(ch);
if class == CM || class == ZWJ {
marked = true;
continue;
}
breaks.state = if marked && rules::is_unmarkable(class) {
ScanState { left: AL, flags: 0 }
} else {
ScanState {
left: if class == RI { RI_EVEN } else { class },
flags: rules::left_flags(bits),
}
};
marked = false;
break;
}
if marked {
breaks.state.left = AL;
}
breaks
}
#[inline]
fn from_options(scalars: I, options: LineOptions) -> Self {
Self {
scalars,
state: ScanState {
left: SOT,
flags: 0,
},
graphemes: GraphemeState::new(),
anywhere: matches!(options.strictness, LineStrictness::Anywhere),
cj_quotes: options.cj_quotes,
rules: options.rules(),
resolve: options.resolve(),
least: Some(0),
started: false,
reads_runs: !matches!(options.word, LineWordOption::BreakAll),
dictionaries: None,
run: Run::default(),
#[cfg(test)]
test: TestState::default(),
}
}
#[inline]
fn start(&mut self) {
self.started = true;
if let Some((offset, ch)) = self.scalars.next() {
self.least = Some(offset.saturating_add(1));
self.step(offset, ch, false);
} else {
self.least = None;
}
}
#[inline(always)]
fn classify(&self, ch: char) -> (u8, u8) {
self.classify_props(ch, self.props(ch))
}
#[inline(always)]
fn classify_props(&self, ch: char, props: LineProps) -> (u8, u8) {
let class = self.resolve[usize::from(props.class())];
if self.cj_quotes && (class == QU_PI || class == QU_PF) {
match ch {
'\u{201C}' => return (OP, 0),
'\u{201D}' => return (CL, 0),
_ => {}
}
}
(class, props.flags())
}
#[inline(always)]
fn props(&self, ch: char) -> LineProps {
#[cfg(test)]
if let Some(classes) = self.test.classes {
return LineProps(classes.get(ch as usize).copied().unwrap_or_default());
}
crate::props::line_props(ch)
}
#[inline(always)]
fn step(&mut self, offset: usize, ch: char, decides: bool) -> bool {
if self.anywhere {
let props = self.grapheme_props(ch);
return self.graphemes.advance(props) && decides;
}
let props = self.props(ch);
let raw = props.class();
let sa = (raw == SA || raw == SA_MARK) && self.reads_runs;
let run = if sa {
self.run_step(offset, ch, raw == SA_MARK)
} else {
self.run.end();
None
};
#[cfg(test)]
let run = if self.rules.legacy & rules::Legacy::DICTIONARY_RUNS != 0 {
self.model_run_step(offset, ch, sa)
} else {
run
};
let (mut class, mut bits) = self.classify_props(ch, props);
let ScanState { left, flags } = self.state;
let joiner = if class == ZWJ { AFTER_ZWJ } else { 0 };
if class == CM || class == ZWJ {
if !rules::is_unmarkable(left) {
self.state.flags = (flags & !AFTER_ZWJ) | joiner;
return decides && run == Some(true);
}
class = AL;
#[cfg(test)]
if self.rules.legacy & rules::Legacy::MARKS_APART != 0 {
class = rules::MARK_AL;
}
bits = 0;
}
let breaks = decides
&& match run {
Some(breaks) => breaks,
None => self.breaks(left, class, flags, bits, ch),
};
let mut next = joiner | rules::left_flags(bits);
if flags & LEFT_EAST_ASIAN != 0 {
next |= BEFORE_EAST_ASIAN;
}
#[cfg(test)]
let left = if decides && self.icu_forgets(left, class, flags, bits, ch, breaks) {
SOT
} else {
left
};
let left_next = rules::next_left(left, class, ch, &self.rules);
#[cfg(test)]
if self.rules.legacy & rules::Legacy::TAILORING_OUTSIDE_RULES != 0 {
next |= rules::icu_inner_flags(left, class, left_next);
}
self.state = ScanState {
left: left_next,
flags: next,
};
breaks
}
#[inline(never)]
fn run_step(&mut self, offset: usize, ch: char, mark: bool) -> Option<bool> {
let script = RunScript::from_sa_char(ch);
let mut run = self.run;
let breaks = run.step(offset, script, mark, self.dictionaries, |dictionary| {
dictionary::find_next_word(
dictionary,
offset,
ch,
self.scalars.clone(),
|next| self.continues_run(next, script),
|ch| self.grapheme_props(ch),
self.grapheme_start(),
)
});
self.run = run;
breaks
}
#[inline]
fn continues_run(&self, ch: char, script: RunScript) -> bool {
let raw = self.props(ch).class();
(raw == SA || raw == SA_MARK) && RunScript::from_sa_char(ch) == script
}
#[inline(always)]
fn grapheme_props(&self, ch: char) -> GraphemeProps {
#[cfg(test)]
if let Some(classes) = self.test.grapheme_classes {
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()
}
#[cfg(test)]
fn model_run_step(&mut self, offset: usize, ch: char, sa: bool) -> Option<bool> {
let previous = self.test.last.replace((offset, ch, sa));
if let Some((breaks, end)) = &mut self.test.model {
let found = breaks.first() == Some(&offset);
breaks.retain(|&at| at > offset);
let covered = end.is_some_and(|end| offset < end);
if breaks.is_empty() && !covered {
self.test.model = None;
}
if found {
return Some(true);
}
if !sa {
self.test.model = None;
return None;
}
if self.test.model.is_some() {
return Some(false);
}
}
let (start, first, true) = previous? else {
return None;
};
if !sa {
return None;
}
let mut run = std::vec![(start, first), (offset, ch)];
let mut least = offset.saturating_add(1);
for (at, next) in self.scalars.clone() {
let raw = self.props(next).class();
if at < least || !(raw == SA || raw == SA_MARK) {
break;
}
run.push((at, next));
least = at.saturating_add(1);
}
let legacy = self.rules.legacy;
let lao = legacy & rules::Legacy::LAO_LO_LING != 0;
let quirks = crate::tests::RunQuirks {
slice_ends: legacy & rules::Legacy::RUN_END_BREAKS != 0,
leaves: legacy & rules::Legacy::DICTIONARY_LEAVES != 0,
misses: legacy & rules::Legacy::DICTIONARY_MISSES != 0,
rest: legacy & rules::Legacy::DICTIONARY_REST != 0,
};
let (mut breaks, covers) = crate::tests::icu_run_breaks(
&run,
self.dictionaries,
|ch| {
if lao && ch == '\u{EA3}' {
RunScript::Other
} else {
RunScript::from_sa_char(ch)
}
},
quirks,
|ch| self.grapheme_props(ch),
self.grapheme_start(),
|ch| self.props(ch).class() == SA_MARK,
);
let found = breaks.first() == Some(&offset);
breaks.retain(|&at| at > offset);
let end = run
.last()
.map(|&(at, ch)| at + ch.len_utf8())
.filter(|_| covers);
if !breaks.is_empty() || end.is_some() {
self.test.model = Some((breaks, end));
}
Some(found)
}
#[cfg(test)]
fn icu_forgets(
&self,
left: u8,
right: u8,
flags: u8,
bits: u8,
ch: char,
breaks: bool,
) -> bool {
if self.rules.legacy & rules::Legacy::CONTEXT_FORGOTTEN == 0 {
return false;
}
let pair = Pair {
left,
right,
flags: flags
| if bits & EAST_ASIAN != 0 {
RIGHT_EAST_ASIAN
} else {
0
},
flags_unread: false,
ch,
next: UNREAD,
second: UNREAD,
};
breaks || rules::icu_tailors(&pair, &self.rules)
}
#[inline(always)]
fn breaks(&self, left: u8, right: u8, flags: u8, bits: u8, ch: char) -> bool {
let tailors = matches!(self.rules.tailoring, Tailoring::Loose);
#[cfg(test)]
if self.rules.legacy != 0 {
return self.decide(left, right, flags, bits, ch);
}
if flags & AFTER_ZWJ == 0 && !(tailors && rules::may_tailor(left, right)) {
let entry = rules::PAIRS
.get(usize::from(left))
.and_then(|row| row.get(usize::from(right)));
if let Some(&entry) = entry
&& entry & rules::PAIR_READS_MORE == 0
{
let answer = if self.rules.keep_all {
rules::PAIR_BREAKS_KEEP_ALL
} else {
rules::PAIR_BREAKS
};
return entry & answer != 0;
}
}
self.decide(left, right, flags, bits, ch)
}
#[inline(never)]
fn decide(&self, left: u8, right: u8, flags: u8, bits: u8, ch: char) -> bool {
let mut pair = Pair {
left,
right,
flags: flags
| if bits & EAST_ASIAN != 0 {
RIGHT_EAST_ASIAN
} else {
0
},
flags_unread: false,
ch,
next: UNREAD,
second: UNREAD,
};
loop {
match rules::decide(&pair, &self.rules) {
KEEP => return false,
BREAK => return true,
NEEDS_NEXT => self.read_ahead(&mut pair, false),
_ => self.read_ahead(&mut pair, true),
}
}
}
#[cold]
#[inline(never)]
fn read_ahead(&self, pair: &mut Pair, second: bool) {
let mut scalars = self.scalars.clone();
let mut least = self.least.unwrap_or(usize::MAX);
let mut found = [EOT; 2];
let mut east_asian = false;
let wanted = if second { 2 } else { 1 };
let mut count = 0;
while count < wanted {
let Some((offset, ch)) = scalars.next() else {
break;
};
if offset < least {
break;
}
least = offset.saturating_add(1);
let (class, bits) = self.classify(ch);
if class == CM || class == ZWJ {
continue;
}
if count == 0 {
east_asian = bits & EAST_ASIAN != 0;
}
found[count] = class;
count += 1;
}
pair.next = found[0];
pair.second = if second { found[1] } else { UNREAD };
if east_asian {
pair.flags |= NEXT_EAST_ASIAN;
}
}
}
#[cfg(test)]
#[derive(Copy, Clone, Debug, Default)]
pub(super) struct Config {
pub(super) classes: Option<&'static [u8]>,
pub(super) legacy: u32,
pub(super) graphemes_17: bool,
pub(super) graphemes: Option<&'static [crate::GraphemeProps]>,
}
#[cfg(test)]
impl<I: Iterator<Item = (usize, char)> + Clone> LineBreaks<'_, I> {
pub(super) fn with_config(scalars: I, options: LineOptions, config: Config) -> Self {
let mut breaks = Self::from_options(scalars, options);
breaks.test.classes = config.classes;
breaks.rules.legacy = config.legacy;
let sa_marks = config.legacy & rules::Legacy::SA_MARKS != 0;
let icu = config.legacy & rules::Legacy::ICU_TAILORING != 0;
if sa_marks || icu {
let tables = &rules::RESOLVE_ICU[usize::from(sa_marks) | usize::from(icu) << 1];
breaks.resolve = &tables[options.resolve_index()];
}
if config.graphemes_17 {
breaks.graphemes = GraphemeState::with_unicode_17_conjuncts();
breaks.test.graphemes_17 = true;
}
breaks.test.grapheme_classes = config.graphemes;
breaks
}
}
impl<I: Iterator<Item = (usize, char)> + Clone> Iterator for LineBreaks<'_, I> {
type Item = usize;
#[inline]
fn next(&mut self) -> Option<usize> {
if !self.started {
self.start();
}
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 LineBreaks<'_, I> {}