Add word-wrapping algorithm.

This commit is contained in:
Condorra 2022-12-29 16:20:34 +11:00
parent 8b628eb831
commit 3dd6cf9bf2
3 changed files with 172 additions and 11 deletions

View File

@ -12,7 +12,6 @@ pub fn ignore_special_characters(input: &str) -> String {
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
struct AnsiState {
col: u64,
background: u64, // 0 means default.
foreground: u64,
bold: bool,
@ -70,7 +69,6 @@ impl AnsiIterator<'_> {
AnsiIterator { underlying: input.chars().enumerate(),
input: input,
state: Rc::new(AnsiState {
col: 0,
background: 0,
foreground: 0,
bold: false,
@ -88,7 +86,6 @@ impl <'l>Iterator for AnsiIterator<'l> {
fn next(self: &mut Self) -> Option<AnsiEvent<'l>> {
if self.pending_col {
Rc::make_mut(&mut self.state).col += 1;
self.pending_col = false;
}
if self.inject_spaces > 0 {
@ -98,7 +95,6 @@ impl <'l>Iterator for AnsiIterator<'l> {
}
while let Some((i0, c)) = self.underlying.next() {
if c == '\n' {
Rc::make_mut(&mut self.state).col = 0;
return Some(AnsiEvent::<'l>(AnsiParseToken::Newline, self.state.clone()));
} else if c == '\t' {
for _ in 0..4 {
@ -314,6 +310,132 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
buf
}
fn is_wrappable(c: char) -> bool {
c == ' ' || c == '-'
}
pub fn word_wrap<F>(input: &str, limit: F) -> String
where F: Fn(usize) -> usize {
let mut it_main = AnsiIterator::new(input);
let mut start_word = true;
let mut row: usize = 0;
let mut col: usize = 0;
let mut buf: String = String::new();
loop {
let ev = it_main.next();
match ev {
None => break,
Some(AnsiEvent(AnsiParseToken::Character(c), _)) => {
col += 1;
if is_wrappable(c) {
start_word = true;
if col < limit(row) || (col == limit(row) && c != ' ') {
buf.push(c);
}
if col == limit(row) {
let mut it_lookahead = it_main.clone();
let fits = 'check_fits: loop {
match it_lookahead.next() {
None => break 'check_fits true,
Some(AnsiEvent(AnsiParseToken::Newline, _)) => break 'check_fits true,
Some(AnsiEvent(AnsiParseToken::Character(c), _)) =>
break 'check_fits is_wrappable(c),
_ => {}
}
};
if !fits {
buf.push('\n');
row += 1;
col = 0;
}
} else if col > limit(row) {
buf.push('\n');
row += 1;
if c == ' ' {
col = 0;
} else {
buf.push(c);
col = 1;
}
}
continue;
}
assert!(col <= limit(row),
"col must be below limit, but found c={}, col={}, limit={}",
c, col, limit(row));
if !start_word {
if col == limit(row) {
// We are about to hit the limit, and we need to decide
// if we save it for a hyphen or just push the char.
let mut it_lookahead = it_main.clone();
let fits = 'check_fits: loop {
match it_lookahead.next() {
None => break 'check_fits true,
Some(AnsiEvent(AnsiParseToken::Newline, _)) => break 'check_fits true,
Some(AnsiEvent(AnsiParseToken::Character(c), _)) =>
break 'check_fits is_wrappable(c),
_ => {}
}
};
if fits {
buf.push(c);
} else {
buf.push('-');
buf.push('\n');
row += 1;
col = 1;
buf.push(c);
}
continue;
}
buf.push(c);
continue;
}
start_word = false;
// We are about to start a word. Do we start the word, wrap, or
// hyphenate?
let it_lookahead = it_main.clone();
let mut wordlen = 0;
'lookahead: for AnsiEvent(e, _) in it_lookahead {
match e {
AnsiParseToken::ControlSeq(_) => {}
AnsiParseToken::Character(c) if !is_wrappable(c) => {
wordlen += 1;
}
AnsiParseToken::Character(c) if c == '-' => {
// Hyphens are special. The hyphen has to fit before
// we break the word.
wordlen += 1;
break 'lookahead;
}
_ => break 'lookahead,
}
}
// Note we already increased col.
if wordlen < limit(row) + 1 - col || (wordlen > limit(row) && col != limit(row)) {
buf.push(c);
continue;
}
// So we can't hyphenate or fit it, let's break now.
buf.push('\n');
row += 1;
col = 1;
buf.push(c);
}
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
col = 0;
row += 1;
buf.push('\n');
start_word = true;
}
Some(AnsiEvent(AnsiParseToken::ControlSeq(t), _)) => buf.push_str(t)
}
}
buf
}
#[cfg(test)]
mod test {
use super::*;
@ -380,4 +502,40 @@ mod test {
assert_eq!(flow_around(str1, 10, " | ", str2, 67), expected);
}
#[test]
fn word_wrap_works_on_long_text() {
let unwrapped = "Hello, this is a very long passage of text that needs to be wrapped. Some words are superduperlong! There are some new\nlines in it though!\nLet's try manuallya-hyphenating.\nManually-hyphenating\nOneverylongrunonwordthatjustkeepsgoing.\n - -- --- - -- - - - -testing";
let wrapped = "Hello, \n\
this is a\n\
very long\n\
passage of\n\
text that\n\
needs to \n\
be \n\
wrapped. \n\
Some words\n\
are super-\n\
duperlong!\n\
There are\n\
some new\n\
lines in \n\
it though!\n\
Let's try\n\
manuallya-\n\
hyphenati-\n\
ng.\n\
Manually-\n\
hyphenati-\n\
ng\n\
Oneverylo-\n\
ngrunonwo-\n\
rdthatjus-\n\
tkeepsgoi-\n\
ng.\n \
- -- ---\n\
- -- - -\n\
- -testing";
assert_eq!(word_wrap(unwrapped, |_| 10), wrapped);
}
}

View File

@ -1,6 +1,6 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, explicit_if_allowed};
use async_trait::async_trait;
use ansi::{ansi, flow_around};
use ansi::{ansi, flow_around, word_wrap};
use crate::models::{user::User, item::Item};
use crate::static_content::room;
@ -55,8 +55,11 @@ pub async fn describe_room(ctx: &VerbContext<'_>, room: &room::Room) -> UResult<
ctx.session,
Some(&flow_around(&render_map(room, 5, 5), 10, " ",
&format!("{} ({})\n{}\n", room.name, zone,
word_wrap(
explicit_if_allowed(ctx, room.description,
room.description_less_explicit)), 68))
room.description_less_explicit),
|row| if row >= 5 { 80 } else { 68 }
)), 68))
).await?;
Ok(())
}

View File

@ -81,10 +81,10 @@ pub fn room_list() -> &'static Vec<Room> {
name: ansi!("<yellow>Choice Room<reset>"),
short: ansi!("<green>CR<reset>"),
description: ansi!(
"A room brightly lit in unnaturally white light, covered in sparkling\n\
white tiles from floor to ceiling. A loudspeaker plays a message on\n\
loop:\r\n\
\t<blue>\"Citizen, you are here because your memory has been wiped and\n\
"A room brightly lit in unnaturally white light, covered in sparkling \
white tiles from floor to ceiling. A loudspeaker plays a message on \
loop:\n\
\t<blue>\"Citizen, you are here because your memory has been wiped and \
you are ready to start a fresh life. As a being enhanced by \
Gazos-Murlison Co technology, the emperor has granted you the power \
to choose 14 points of upgrades to yourself. Choose wisely, as it \