#[cfg(feature="unicode-width")] extern crate unicode_width;
#[cfg(feature="unicode-width")] use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub enum InsertAt<'a> {
Left,
Right,
Balanced,
Custom(&'a dyn Fn(usize, usize, usize, &Vec<&str>)->usize)
}
pub struct Settings<'a> {
pub justify_last_line: bool,
pub hyphenate_overflow: bool,
pub width: usize,
pub insert_at: InsertAt<'a>,
#[cfg(feature="unicode-width")]
pub wcwidth: bool,
pub ignore_spaces: bool,
pub newline: &'a str,
pub hyphen: &'a str,
pub separator: &'a str
}
impl<'a> Default for Settings<'a> {
fn default() -> Self {
Settings {
justify_last_line: false,
width: 80,
hyphenate_overflow: false,
insert_at: InsertAt::Balanced,
#[cfg(feature="unicode-width")]
wcwidth: false,
ignore_spaces: false,
newline: "\n",
hyphen: "-",
separator: "\n\n"
}
}
}
fn get_break_indexes(words: &Vec<&str>, settings: &Settings) -> Vec<usize> {
let mut n = 0;
let mut v = Vec::with_capacity(words.len()/4);
v.push(0);
for (i, word) in words.iter().enumerate() {
let mut c;
#[cfg(feature="unicode-width")] {
if settings.wcwidth {
c = n + word.width();
} else {
c = n + word.len();
}
}
#[cfg(not(feature="unicode-width"))] {
c = n + word.len();
}
if word.len() == 0 { continue }
let cc = word.chars().nth(word.len()-1);
if c - if cc.map_or(false, char::is_whitespace) { 1 } else { 0 } > settings.width {
v.push(i);
n = word.len();
} else {
n = c;
}
}
v
}
fn lines_from_indexes<'a>(words: &Vec<&'a str>, breaks: &Vec<usize>) -> Vec<Vec<&'a str>> {
let mut lines: Vec<Vec<&str>> = Vec::with_capacity(breaks.len());
for i in 0..breaks.len()-1 {
let mut t_v = Vec::from(&words[breaks[i]..breaks[i+1]]);
let t_l = t_v.len();
if t_v.len() == 0 { continue }
t_v[t_l-1] = &t_v[t_l-1][0..&t_v[t_l-1].len()-1];
lines.push(t_v);
}
lines.push(Vec::from(&words[breaks[breaks.len()-1]..]));
lines
}
fn spaces_to_add(lines: &Vec<Vec<&str>>, settings: &Settings) -> Vec<usize> {
let mut spaces: Vec<usize> = Vec::with_capacity(lines.len());
for line in lines.iter() {
let mut size = line.iter().fold(0, |acc, &x| acc + x.len());
#[cfg(feature="unicode-width")]
match settings.wcwidth {
true => {size = line.iter().fold(0, |acc, &x| acc + x.width())},
false => {}
}
if settings.width < size {
spaces.push(0);
} else {
spaces.push(settings.width - size);
}
}
spaces
}
fn add_spaces(add: usize, line: &Vec<&str>, insert_at: &InsertAt) -> String {
if line.len() == 0 { return String::new() }
let v_i = line.len()-1;
let mut add_v = vec![0; v_i];
if v_i == 0 {
return line[0].to_owned()
}
match *insert_at {
InsertAt::Left => {
for j in (1..v_i+1).into_iter().cycle().take(add) {
add_v[j-1] += 1;
}
},
InsertAt::Right => {
for j in (1..v_i+1).rev().into_iter().cycle().take(add) {
add_v[j-1] += 1;
}
},
InsertAt::Balanced => {
for j in (1..v_i+1).into_iter().cycle().take(add) {
if j % 2 == 0 { add_v[v_i - (j/2)] += 1;
} else { add_v[(j/2)] += 1;
}
}
},
InsertAt::Custom(f) => {
for j in 0..add {
add_v[f(j, add, v_i, line)] += 1;
}
}
}
let space_s: Vec<String> = add_v.iter()
.map(|i|" ".repeat(*i))
.collect();
let space_l: usize = add_v.iter().sum();
let line_l: usize = line.iter().map(|e|e.len()).sum();
line.iter()
.enumerate()
.fold(
String::with_capacity(space_l + line_l),
|acc, (i, x)| {
if i < line.len()-1 {
acc + x + &space_s[i]
} else {
acc + x
}
}
)
}
fn split_into_words(text: &str) -> Vec<&str> {
let zero = vec![0];
let indices: Vec<_> = zero.into_iter()
.chain(
text.match_indices(char::is_whitespace)
.map(|(i, _)|i+1)
)
.collect();
let mut wwords = Vec::with_capacity(indices.len());
for i in 0..indices.len()-1 {
let t = &text[indices[i]..indices[i+1]];
if !t.chars().all(char::is_whitespace) {
wwords.push(t);
}
}
wwords.push(&text[indices[indices.len()-1]..]);
wwords
}
#[cfg(feature="unicode-width")]
fn hyphenate_overflow(text: &str, settings: &Settings) -> String {
let mut ret = String::with_capacity(text.len());
let sws: Vec<_>;
let joiner: &str;
if settings.ignore_spaces {
sws = text.split(settings.newline).collect();
joiner = settings.newline;
} else {
sws = text.split_whitespace().collect();
joiner = " ";
}
let tl = sws.len();
for (i, s) in sws.iter().enumerate() {
if s.len() > settings.width {
let h = s.chars()
.collect::<Vec<_>>();
let widths: Vec<usize> = h.iter()
.map(|e| e.width().unwrap_or(0))
.collect();
let mut q = 0;
let mut hq = vec![0];
for (i, w) in widths.into_iter().enumerate() {
q += w;
if q > settings.width-(settings.hyphen.len()) {
hq.push(i);
q=w;
}
}
let mut hhq = Vec::new();
for e in hq.windows(2) {
if e.len() == 2 {
hhq.push(&h[e[0]..e[1]]);
} else {
continue
}
}
hhq.push(&h[*hq.last().unwrap()..]);
let mut hh = hhq.iter().peekable();
let mut f: Vec<String> = Vec::new();
loop {
let s: String = hh.next().unwrap().iter().collect();
if hh.peek().is_some() {
f.push(s + settings.hyphen);
} else {
f.push(s);
break
}
}
ret += &f.join(joiner);
} else {
ret += s;
}
if i != tl-1 {
ret += joiner;
}
}
ret
}
#[cfg(not(feature="unicode-width"))]
fn hyphenate_overflow(text: &str, settings: &Settings) -> String {
let mut ret = String::with_capacity(text.len());
let sws: Vec<_>;
let joiner: &str;
if settings.ignore_spaces {
sws = text.split(settings.newline).collect();
joiner = settings.newline;
} else {
sws = text.split_whitespace().collect();
joiner = " ";
}
let tl = sws.len();
for (i, s) in sws.iter().enumerate() {
if s.len() > settings.width {
let h = s.chars().collect::<Vec<_>>();
let mut f: Vec<String> = Vec::new();
let mut p = h.chunks(settings.width-(settings.hyphen.len())).peekable();
loop {
let s: String = p.next().unwrap().iter().collect();
if p.peek().is_some() {
f.push(s + settings.hyphen);
} else {
f.push(s);
break
}
}
ret += &f.join(joiner);
} else {
ret += s;
}
if i != tl-1 {
ret += joiner;
}
}
ret
}
pub fn justify_paragraph(text: &str, settings: &Settings) -> String {
if text.contains("\n") {
panic!("Expected `text` to contain no newlines but it did")
}
let mut ret = String::with_capacity(text.len() + (text.len() / 3));
let words = split_into_words(text);
let breaks = get_break_indexes(&words, &settings);
let lines = lines_from_indexes(&words, &breaks);
let spaces = spaces_to_add(&lines, &settings);
for (i, space) in spaces.iter().enumerate() {
if !settings.justify_last_line && i == spaces.len() - 1 {
ret += &lines[spaces.len()-1].join("");
break
}
if !settings.ignore_spaces {
let add = &add_spaces(*space, &lines[i], &settings.insert_at);
ret += add;
} else {
ret += &lines[i].join(" ");
}
ret += settings.newline;
}
ret
}
pub fn justify(text: &str, settings: &Settings) -> String {
let mut h = String::new();
if settings.hyphenate_overflow {
h = hyphenate_overflow(text, &settings);
}
if settings.ignore_spaces {
return h;
}
if settings.hyphenate_overflow { h.as_str() } else { text }
.split(settings.newline)
.filter(
|e|e.len()!=0
)
.map(
|p| justify_paragraph(p, settings)
)
.collect::<Vec<_>>()
.join(settings.separator)
}