forked from blasthavers/blastmud
Add word-wrapping algorithm.
This commit is contained in:
parent
8b628eb831
commit
3dd6cf9bf2
166
ansi/src/lib.rs
166
ansi/src/lib.rs
@ -12,7 +12,6 @@ pub fn ignore_special_characters(input: &str) -> String {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
struct AnsiState {
|
struct AnsiState {
|
||||||
col: u64,
|
|
||||||
background: u64, // 0 means default.
|
background: u64, // 0 means default.
|
||||||
foreground: u64,
|
foreground: u64,
|
||||||
bold: bool,
|
bold: bool,
|
||||||
@ -70,7 +69,6 @@ impl AnsiIterator<'_> {
|
|||||||
AnsiIterator { underlying: input.chars().enumerate(),
|
AnsiIterator { underlying: input.chars().enumerate(),
|
||||||
input: input,
|
input: input,
|
||||||
state: Rc::new(AnsiState {
|
state: Rc::new(AnsiState {
|
||||||
col: 0,
|
|
||||||
background: 0,
|
background: 0,
|
||||||
foreground: 0,
|
foreground: 0,
|
||||||
bold: false,
|
bold: false,
|
||||||
@ -88,7 +86,6 @@ impl <'l>Iterator for AnsiIterator<'l> {
|
|||||||
|
|
||||||
fn next(self: &mut Self) -> Option<AnsiEvent<'l>> {
|
fn next(self: &mut Self) -> Option<AnsiEvent<'l>> {
|
||||||
if self.pending_col {
|
if self.pending_col {
|
||||||
Rc::make_mut(&mut self.state).col += 1;
|
|
||||||
self.pending_col = false;
|
self.pending_col = false;
|
||||||
}
|
}
|
||||||
if self.inject_spaces > 0 {
|
if self.inject_spaces > 0 {
|
||||||
@ -98,7 +95,6 @@ impl <'l>Iterator for AnsiIterator<'l> {
|
|||||||
}
|
}
|
||||||
while let Some((i0, c)) = self.underlying.next() {
|
while let Some((i0, c)) = self.underlying.next() {
|
||||||
if c == '\n' {
|
if c == '\n' {
|
||||||
Rc::make_mut(&mut self.state).col = 0;
|
|
||||||
return Some(AnsiEvent::<'l>(AnsiParseToken::Newline, self.state.clone()));
|
return Some(AnsiEvent::<'l>(AnsiParseToken::Newline, self.state.clone()));
|
||||||
} else if c == '\t' {
|
} else if c == '\t' {
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
@ -314,6 +310,132 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
|||||||
buf
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -380,4 +502,40 @@ mod test {
|
|||||||
assert_eq!(flow_around(str1, 10, " | ", str2, 67), expected);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, explicit_if_allowed};
|
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, explicit_if_allowed};
|
||||||
use async_trait::async_trait;
|
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::models::{user::User, item::Item};
|
||||||
use crate::static_content::room;
|
use crate::static_content::room;
|
||||||
|
|
||||||
@ -55,8 +55,11 @@ pub async fn describe_room(ctx: &VerbContext<'_>, room: &room::Room) -> UResult<
|
|||||||
ctx.session,
|
ctx.session,
|
||||||
Some(&flow_around(&render_map(room, 5, 5), 10, " ",
|
Some(&flow_around(&render_map(room, 5, 5), 10, " ",
|
||||||
&format!("{} ({})\n{}\n", room.name, zone,
|
&format!("{} ({})\n{}\n", room.name, zone,
|
||||||
|
word_wrap(
|
||||||
explicit_if_allowed(ctx, room.description,
|
explicit_if_allowed(ctx, room.description,
|
||||||
room.description_less_explicit)), 68))
|
room.description_less_explicit),
|
||||||
|
|row| if row >= 5 { 80 } else { 68 }
|
||||||
|
)), 68))
|
||||||
).await?;
|
).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -81,10 +81,10 @@ pub fn room_list() -> &'static Vec<Room> {
|
|||||||
name: ansi!("<yellow>Choice Room<reset>"),
|
name: ansi!("<yellow>Choice Room<reset>"),
|
||||||
short: ansi!("<green>CR<reset>"),
|
short: ansi!("<green>CR<reset>"),
|
||||||
description: ansi!(
|
description: ansi!(
|
||||||
"A room brightly lit in unnaturally white light, covered in sparkling\n\
|
"A room brightly lit in unnaturally white light, covered in sparkling \
|
||||||
white tiles from floor to ceiling. A loudspeaker plays a message on\n\
|
white tiles from floor to ceiling. A loudspeaker plays a message on \
|
||||||
loop:\r\n\
|
loop:\n\
|
||||||
\t<blue>\"Citizen, you are here because your memory has been wiped and\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 \
|
you are ready to start a fresh life. As a being enhanced by \
|
||||||
Gazos-Murlison Co technology, the emperor has granted you the power \
|
Gazos-Murlison Co technology, the emperor has granted you the power \
|
||||||
to choose 14 points of upgrades to yourself. Choose wisely, as it \
|
to choose 14 points of upgrades to yourself. Choose wisely, as it \
|
||||||
|
Loading…
Reference in New Issue
Block a user