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