use std::{
fmt,
hash::Hash,
iter::FusedIterator,
str::{self, CharIndices},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LineBreak {
Soft(usize),
Hard(usize),
}
impl LineBreak {
#[inline]
pub fn offset(&self) -> usize {
match *self {
LineBreak::Soft(offset) | LineBreak::Hard(offset) => offset,
}
}
}
pub trait LineBreaker: fmt::Debug + Copy + Hash {
fn line_breaks<'a>(&self, glyph_info: &'a str) -> Box<dyn Iterator<Item = LineBreak> + 'a>;
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum BuiltInLineBreaker {
UnicodeLineBreaker,
AnyCharLineBreaker,
}
impl Default for BuiltInLineBreaker {
#[inline]
fn default() -> Self {
BuiltInLineBreaker::UnicodeLineBreaker
}
}
struct AnyCharLineBreakerIter<'a> {
chars: CharIndices<'a>,
breaks: xi_unicode::LineBreakIterator<'a>,
current_break: Option<(usize, bool)>,
}
impl Iterator for AnyCharLineBreakerIter<'_> {
type Item = LineBreak;
#[inline]
fn next(&mut self) -> Option<LineBreak> {
let (b_index, c) = self.chars.next()?;
let c_len = c.len_utf8();
while self.current_break.is_some() {
if self.current_break.as_ref().unwrap().0 < b_index + c_len {
self.current_break = self.breaks.next();
} else {
break;
}
}
if let Some((break_index, true)) = self.current_break {
if break_index == b_index + c_len {
return Some(LineBreak::Hard(break_index));
}
}
Some(LineBreak::Soft(b_index + c_len))
}
}
impl FusedIterator for AnyCharLineBreakerIter<'_> {}
impl LineBreaker for BuiltInLineBreaker {
#[inline]
fn line_breaks<'a>(&self, text: &'a str) -> Box<dyn Iterator<Item = LineBreak> + 'a> {
match *self {
BuiltInLineBreaker::UnicodeLineBreaker => Box::new(
xi_unicode::LineBreakIterator::new(text).map(|(offset, hard)| {
if hard {
LineBreak::Hard(offset)
} else {
LineBreak::Soft(offset)
}
}),
),
BuiltInLineBreaker::AnyCharLineBreaker => {
let mut unicode_breaker = xi_unicode::LineBreakIterator::new(text);
let current_break = unicode_breaker.next();
Box::new(AnyCharLineBreakerIter {
chars: text.char_indices(),
breaks: unicode_breaker,
current_break,
})
}
}
}
}
pub(crate) trait EolLineBreak<B: LineBreaker> {
fn eol_line_break(&self, line_breaker: &B) -> Option<LineBreak>;
}
impl<B: LineBreaker> EolLineBreak<B> for char {
#[inline]
fn eol_line_break(&self, line_breaker: &B) -> Option<LineBreak> {
let mut last_end_bytes: [u8; 5] = [b' '; 5];
self.encode_utf8(&mut last_end_bytes);
let len_utf8 = self.len_utf8();
if let Ok(last_end_padded) = str::from_utf8(&last_end_bytes[0..=len_utf8]) {
match line_breaker.line_breaks(last_end_padded).next() {
l @ Some(LineBreak::Soft(1)) | l @ Some(LineBreak::Hard(1)) => return l,
_ => {}
}
}
last_end_bytes[len_utf8] = b'a';
if let Ok(last_end_padded) = str::from_utf8(&last_end_bytes[0..=len_utf8]) {
match line_breaker.line_breaks(last_end_padded).next() {
l @ Some(LineBreak::Soft(1)) | l @ Some(LineBreak::Hard(1)) => return l,
_ => {}
}
}
None
}
}
#[cfg(test)]
mod eol_line_break {
use super::*;
#[test]
fn hard_break_char() {
assert_eq!(
'\n'.eol_line_break(&BuiltInLineBreaker::default()),
Some(LineBreak::Hard(1))
);
}
#[test]
fn soft_break_char() {
assert_eq!(
' '.eol_line_break(&BuiltInLineBreaker::default()),
Some(LineBreak::Soft(1))
);
}
}