use unicode_linebreak::{BreakClass, break_property};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::_log::log;
pub const PROHIBITED_START: &str = ")]}〕〉》」』】〙〗〟'\"⦆»\
ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻\
‐゠–〜\
? ! ‼ ⁇ ⁈ ⁉\
・、:;,\
。.";
pub const PROHIBITED_END: &str = "([{〔〈《「『【〘〖〝'\"⦅«";
#[derive(Debug, PartialEq, Eq)]
pub enum BreakPoint {
EndOfLine(usize),
WrapPoint(usize),
EndOfText(usize),
}
#[derive(Debug)]
pub struct LineBreaker {
max_width: usize,
prohibited_start: Vec<String>,
prohibited_end: Vec<String>,
}
pub struct LineBreakerBuilder {
line_breaker: LineBreaker,
}
impl LineBreakerBuilder {
pub fn max_width(mut self, max_width: usize) -> Self {
self.line_breaker.max_width = max_width;
self
}
fn prohibited_start<S: AsRef<str>>(mut self, graphemes: S) -> Self {
self.line_breaker.prohibited_start = graphemes
.as_ref()
.graphemes(true)
.map(|s| s.to_owned())
.collect();
self
}
fn prohibited_end<S: AsRef<str>>(mut self, graphemes: S) -> Self {
self.line_breaker.prohibited_end = graphemes
.as_ref()
.graphemes(true)
.map(|s| s.to_string())
.collect();
self
}
pub fn build(self) -> anyhow::Result<LineBreaker> {
if self.line_breaker.max_width < 2 {
anyhow::bail!(
"max_width out of range: {} (cannot be below 2)",
self.line_breaker.max_width
)
} else {
Ok(self.line_breaker)
}
}
}
impl LineBreaker {
pub fn builder() -> LineBreakerBuilder {
LineBreakerBuilder {
line_breaker: LineBreaker {
max_width: 80,
prohibited_start: Vec::new(),
prohibited_end: Vec::new(),
},
}
.prohibited_start(PROHIBITED_START)
.prohibited_end(PROHIBITED_END)
}
fn prohibited_start<'a>(&'a self) -> Vec<&'a str> {
self.prohibited_start
.iter()
.map(|s| s.as_ref())
.collect::<Vec<&'a str>>()
}
fn prohibited_end<'a>(&'a self) -> Vec<&'a str> {
self.prohibited_end
.iter()
.map(|s| s.as_ref())
.collect::<Vec<&'a str>>()
}
pub fn next_line_break(&self, line: &str) -> BreakPoint {
log!("next_line_break() {:?}", line);
let mut graphemes: Vec<&str> = Vec::with_capacity(128);
let mut acc_width: usize = 0;
for (i, grapheme) in line.grapheme_indices(true) {
if grapheme == "\r" || grapheme == "\n" {
log!(" {i:02} {:?} eol", grapheme);
return BreakPoint::EndOfLine(i + 1);
} else if grapheme == "\r\n" {
log!(" {i:02} {:?} eol", grapheme);
return BreakPoint::EndOfLine(i + 2);
}
let width = grapheme.width_cjk();
if self.max_width < acc_width + width {
log!(" {i:02} {:?} !!", grapheme);
if let Some(nbytes_seek_back) =
self.num_bytes_to_seek_back(graphemes.as_slice(), grapheme)
{
log!(
" split at {:02} --> {:?}",
i - nbytes_seek_back,
line.split_at_checked(i - nbytes_seek_back)
.expect("invalid split point")
);
return BreakPoint::WrapPoint(i - nbytes_seek_back);
}
}
log!(" {i:02} {:?}", grapheme);
graphemes.push(grapheme);
acc_width += width;
}
BreakPoint::EndOfText(line.len())
}
fn num_bytes_to_seek_back(
&self,
preceding_graphemes: &[&str],
following_grapheme: &str,
) -> Option<usize> {
debug_assert!(!preceding_graphemes.is_empty());
let mut nbytes_to_rewind = 0;
let mut following = following_grapheme;
for grapheme in preceding_graphemes.iter().rev() {
if !is_breakable(grapheme, following) {
let grapheme_len = grapheme.len();
nbytes_to_rewind += grapheme_len;
log!(" rewind {grapheme_len}: {grapheme:?} {following:?} (not breakable)",);
following = grapheme;
continue;
}
if self.prohibited_end().contains(grapheme) {
let grapheme_len = grapheme.len();
nbytes_to_rewind += grapheme_len;
log!(" rewind {grapheme_len}: {grapheme:?} {following:?} (prohibited_end)");
following = grapheme;
continue;
}
if self.prohibited_start().contains(&following) {
let grapheme_len = grapheme.len();
nbytes_to_rewind += grapheme_len;
log!(" rewind {grapheme_len}: {grapheme:?} {following:?} (prohibited_start)");
following = grapheme;
continue;
}
log!(" break: {grapheme:?} {following:?}");
return Some(nbytes_to_rewind);
}
log!(" cannot rewind anymore");
None
}
}
#[allow(clippy::let_and_return)]
fn is_breakable(preceding: &str, following: &str) -> bool {
debug_assert!(!preceding.is_empty());
debug_assert!(!following.is_empty());
let preceding_break_property = preceding
.chars()
.last()
.map(|c| break_property(c as u32))
.expect("`preceding` must be non-empty");
let following_break_property = following
.chars()
.next()
.map(|c| break_property(c as u32))
.expect("`following` must be non-empty");
let breakable = match (preceding_break_property, following_break_property) {
(BreakClass::After, _) => false,
(_, BreakClass::Before) => false,
(BreakClass::BeforeAndAfter, _) => false,
(_, BreakClass::BeforeAndAfter) => false,
(BreakClass::Alphabetic, BreakClass::Alphabetic) => false,
(_, BreakClass::Space) => false,
(_, _) => true,
};
breakable
}
#[cfg(test)]
mod test {
use super::*;
use rstest::rstest;
#[rstest]
#[case("a", "a", false)]
#[case("a", "あ", true)]
#[case("a", " ", false)]
#[case(" ", "a", true)]
fn is_breakable_normal(
#[case] preceding: &str,
#[case] following: &str,
#[case] expected: bool,
) {
assert_eq!(is_breakable(preceding, following), expected);
}
#[test]
fn max_width() {
assert!(LineBreaker::builder().max_width(0).build().is_err());
assert!(LineBreaker::builder().max_width(1).build().is_err());
assert!(LineBreaker::builder().max_width(2).build().is_ok());
assert!(LineBreaker::builder().max_width(3).build().is_ok());
}
#[rstest]
#[case(10, "あ「い」う", BreakPoint::EndOfText(15))]
#[case(9, "あ「い」う", BreakPoint::WrapPoint(12))]
#[case(8, "あ「い」う", BreakPoint::WrapPoint(12))]
#[case(7, "あ「い」う", BreakPoint::WrapPoint(3))]
#[case(6, "あ「い」う", BreakPoint::WrapPoint(3))]
#[case(5, "あ「い」う", BreakPoint::WrapPoint(3))]
#[case(4, "あ「い」う", BreakPoint::WrapPoint(3))]
#[case(3, "あ「い」う", BreakPoint::WrapPoint(3))]
#[case(2, "あ「い」う", BreakPoint::WrapPoint(3))]
fn next_line_break(
#[case] max_width: usize,
#[case] line: &str,
#[case] expected: BreakPoint,
) -> anyhow::Result<()> {
let line_breaker = LineBreaker::builder().max_width(max_width).build()?;
let actual = line_breaker.next_line_break(line);
assert_eq!(expected, actual);
Ok(())
}
#[rstest]
#[case(2, "foo\rbar", BreakPoint::EndOfLine(4))]
#[case(3, "foo\rbar", BreakPoint::EndOfLine(4))]
#[case(4, "foo\rbar", BreakPoint::EndOfLine(4))]
#[case(5, "foo\rbar", BreakPoint::EndOfLine(4))]
#[case(5, "foo\nbar", BreakPoint::EndOfLine(4))]
#[case(5, "foo\r\nbar", BreakPoint::EndOfLine(5))]
fn next_line_break_eol(
#[case] max_width: usize,
#[case] line: &str,
#[case] expected: BreakPoint,
) -> anyhow::Result<()> {
let line_breaker = LineBreaker::builder().max_width(max_width).build()?;
let actual = line_breaker.next_line_break(line);
assert_eq!(expected, actual);
Ok(())
}
#[rstest]
#[case(8, "あ「🐈⬛」う", BreakPoint::WrapPoint(19))]
#[case(7, "あ「🐈⬛」う", BreakPoint::WrapPoint(3))]
fn next_line_break_composite(
#[case] max_width: usize,
#[case] line: &str,
#[case] expected: BreakPoint,
) -> anyhow::Result<()> {
let line_breaker = LineBreaker::builder().max_width(max_width).build()?;
let actual = line_breaker.next_line_break(line);
assert_eq!(expected, actual);
Ok(())
}
#[rstest]
#[case(11, "あfoo barい", BreakPoint::EndOfText(13))]
#[case(10, "あfoo barい", BreakPoint::WrapPoint(10))]
#[case(9, "あfoo barい", BreakPoint::WrapPoint(10))]
#[case(8, "あfoo barい", BreakPoint::WrapPoint(7))]
#[case(7, "あfoo barい", BreakPoint::WrapPoint(7))]
#[case(6, "あfoo barい", BreakPoint::WrapPoint(7))]
#[case(5, "あfoo barい", BreakPoint::WrapPoint(3))]
#[case(4, "あfoo barい", BreakPoint::WrapPoint(3))]
#[case(3, "あfoo barい", BreakPoint::WrapPoint(3))]
#[case(2, "あfoo barい", BreakPoint::WrapPoint(3))]
fn next_line_break_western_word_wrap(
#[case] max_width: usize,
#[case] line: &str,
#[case] expected: BreakPoint,
) -> anyhow::Result<()> {
let line_breaker = LineBreaker::builder().max_width(max_width).build()?;
let actual = line_breaker.next_line_break(line);
assert_eq!(expected, actual);
Ok(())
}
#[rstest]
#[case(8, "あ「い」", "う", Some(0))]
#[case(6, "あ「い", "」", Some(6))]
#[case(3, "あ「", "い", Some(3))]
#[case(2, "あ", "「", Some(0))]
#[case(2, "」", "「", Some(0))]
#[case(2, "「", "「", None)]
fn num_bytes_to_seek_back(
#[case] max_width: usize,
#[case] preceding_graphemes: &str,
#[case] following_grapheme: &str,
#[case] expected: Option<usize>,
) -> anyhow::Result<()> {
let preceding_graphemes: Vec<&str> = preceding_graphemes.graphemes(true).collect();
let line_breaker = LineBreaker::builder().max_width(max_width).build()?;
let actual = line_breaker.num_bytes_to_seek_back(&preceding_graphemes, following_grapheme);
assert_eq!(actual, expected);
Ok(())
}
}