Start ansi formatting utilities ready for look command.

This commit is contained in:
Condorra 2022-12-28 20:00:55 +11:00
parent d362c2f371
commit 1a46405a49
14 changed files with 282 additions and 20 deletions

9
Cargo.lock generated
View File

@ -32,6 +32,13 @@ dependencies = [
"libc",
]
[[package]]
name = "ansi"
version = "0.1.0"
dependencies = [
"ansi_macro",
]
[[package]]
name = "ansi_macro"
version = "0.1.0"
@ -103,7 +110,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
name = "blastmud_game"
version = "0.1.0"
dependencies = [
"ansi_macro",
"ansi",
"async-trait",
"base64 0.20.0",
"bcrypt",

View File

@ -4,4 +4,6 @@ members = [
"blastmud_listener",
"blastmud_interfaces",
"blastmud_game",
"ansi_macro",
"ansi",
]

7
ansi/Cargo.toml Normal file
View File

@ -0,0 +1,7 @@
[package]
name = "ansi"
version = "0.1.0"
edition = "2021"
[dependencies]
ansi_macro = { path = "../ansi_macro" }

229
ansi/src/lib.rs Normal file
View File

@ -0,0 +1,229 @@
pub use ansi_macro::ansi;
use std::rc::Rc;
/// Removes all non-printable characters except tabs and newlines.
/// Doesn't attempt to remove printable characters as part of an
/// escape - so use this for untrusted input that you don't expect
/// to contain ansi escapes at all.
pub fn ignore_special_characters(input: &str) -> String {
input.chars().filter(|c| *c == '\t' || *c == '\n' ||
(*c >= ' ' && *c <= '~')).collect()
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
struct AnsiState {
col: u64,
background: u64, // 0 means default.
foreground: u64,
bold: bool,
underline: bool,
strike: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
struct AnsiEvent<'l> (
AnsiParseToken<'l>,
Rc<AnsiState>
);
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum AnsiParseToken<'l> {
Character(char),
ControlSeq(&'l str),
Newline,
}
/// Emits events with only LF, spaces, tabs, and a small set of
/// character attributes (colours, bold, underline). Anything else
/// sent will be emitted as printable characters. Tabs are replaced
/// with 4 spaces.
#[derive(Clone, Debug)]
struct AnsiIterator<'l> {
underlying: std::iter::Enumerate<std::str::Chars<'l>>,
input: &'l str,
state: Rc<AnsiState>,
pending_col: bool,
inject_spaces: u64,
}
impl AnsiIterator<'_> {
fn new<'l>(input: &'l str) -> AnsiIterator<'l> {
AnsiIterator { underlying: input.chars().enumerate(),
input: input,
state: Rc::new(AnsiState {
col: 0,
background: 0,
foreground: 0,
bold: false,
underline: false,
strike: false
}),
pending_col: false,
inject_spaces: 0
}
}
}
impl <'l>Iterator for AnsiIterator<'l> {
type Item = AnsiEvent<'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 {
self.pending_col = true;
self.inject_spaces -= 1;
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(' '), self.state.clone()));
}
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 {
self.pending_col = true;
self.inject_spaces = 3;
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(' '), self.state.clone()));
}
} else if c >= ' ' && c <= '~' {
self.pending_col = true;
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(c), self.state.clone()));
} else if c == '\x1b' {
if let Some((_, c2)) = self.underlying.next() {
if c2 != '[' {
continue;
}
}
if let Some((_, cs1)) = self.underlying.next() {
let mut imax = i0;
let mut cs_no: i64 = cs1 as i64 - b'0' as i64;
if cs_no < 0 || cs_no > 9 {
continue;
}
if let Some((i2, cs2)) = self.underlying.next() {
let cs_no2: i64 = cs2 as i64 - b'0' as i64;
if cs_no2 >= 0 && cs_no2 <= 9 {
if let Some((i3, cs3)) = self.underlying.next() {
if cs3 == 'm' {
cs_no *= 10;
cs_no += cs_no2;
imax = i3;
} else { continue; }
}
} else if cs2 != 'm' {
continue;
} else {
imax = i2;
}
let st = Rc::make_mut(&mut self.state);
match cs_no {
0 => {
st.background = 0;
st.foreground = 0;
st.bold = false;
st.underline = false;
st.strike = false;
}
1 => { st.bold = true; }
4 => { st.underline = true; }
9 => { st.strike = true; }
24 => { st.underline = false; }
i if i >= 30 && i <= 37 => {
st.foreground = i as u64 - 29;
}
i if i >= 40 && i <= 47 => {
st.foreground = i as u64 - 39;
}
_ => continue
}
drop(st);
return Some(AnsiEvent::<'l>(
AnsiParseToken::ControlSeq(
&self.input[i0..(imax + 1)]
), self.state.clone()));
}
}
}
}
None
}
}
/// Strips out basic colours / character formatting codes cleanly. Tabs are
/// changed to spaces, and newlines are preserved. All other ANSI non-printables
/// are stripped but might display incorrectly.
pub fn strip_special_characters(input: &str) -> String {
let mut buf: String = String::new();
let it = AnsiIterator::new(input);
for AnsiEvent(e, _) in it {
match e {
AnsiParseToken::Character(c) => buf.push(c),
AnsiParseToken::Newline => buf.push('\n'),
_ => {}
}
}
buf
}
/// Allows basic colours / character formatting codes. Tabs are
/// changed to spaces, and newlines are preserved. All other ANSI non-printables
/// are stripped but might display incorrectly.
pub fn limit_special_characters(input: &str) -> String {
let mut buf: String = String::new();
let it = AnsiIterator::new(input);
for AnsiEvent(e, _) in it {
match e {
AnsiParseToken::Character(c) => buf.push(c),
AnsiParseToken::Newline => buf.push('\n'),
AnsiParseToken::ControlSeq(t) => buf.push_str(t)
}
}
buf
}
/// Flows a second column around a first column, limiting the width of both
/// columns as specified, and adding a gutter.
pub fn flow_around(col1: &str, col1_width: u64, gutter: &str,
col2: &str, col2_width: u64) -> String {
"not yet".to_owned()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn ignore_special_characters_removes_esc() {
assert_eq!(ignore_special_characters("hello\x1b[world"), "hello[world");
}
#[test]
fn strip_special_characters_makes_plaintext() {
assert_eq!(strip_special_characters("a\tb"), "a b");
assert_eq!(
strip_special_characters(ansi!("<red>hello<green>world")),
"helloworld");
assert_eq!(
strip_special_characters("hello\r\x07world\n"),
"helloworld\n");
assert_eq!(
strip_special_characters("hello\r\x07world\n"),
"helloworld\n");
assert_eq!(
strip_special_characters("Test\x1b[5;5fing"),
"Test5fing");
}
#[test]
fn limit_special_characters_strips_some_things() {
assert_eq!(limit_special_characters(ansi!("a<bgred><green>b<bggreen><red>c<reset>d")),
ansi!("a<bgred><green>b<bggreen><red>c<reset>d"));
assert_eq!(limit_special_characters("Test\x1b[5;5fing"),
"Test5fing");
}
}

View File

@ -8,7 +8,7 @@ edition = "2021"
[dependencies]
base64 = "0.20.0"
blastmud_interfaces = { path = "../blastmud_interfaces" }
ansi_macro = { path = "../ansi_macro" }
ansi = { path = "../ansi" }
deadpool = "0.9.5"
deadpool-postgres = { version = "0.10.3", features = ["serde"] }
futures = "0.3.25"

View File

@ -1,7 +1,7 @@
use crate::message_handler::ListenerSession;
use crate::DResult;
use crate::db::DBPool;
use ansi_macro::ansi;
use ansi::ansi;
use std::default::Default;
use crate::models::session::Session;

View File

@ -1,20 +1,21 @@
use super::ListenerSession;
use crate::DResult;
use crate::db::{DBTrans, DBPool};
use ansi_macro::ansi;
use ansi::ansi;
use phf::phf_map;
use async_trait::async_trait;
use crate::models::{session::Session, user::User};
use log::warn;
mod parsing;
mod ignore;
mod help;
mod quit;
mod less_explicit_mode;
mod register;
mod agree;
mod help;
mod ignore;
mod less_explicit_mode;
mod login;
mod look;
mod parsing;
mod quit;
mod register;
pub struct VerbContext<'l> {
session: &'l ListenerSession,
@ -59,14 +60,16 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
};
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"less_explicit_mode" => less_explicit_mode::VERB,
"register" => register::VERB,
"login" => login::VERB,
"agree" => agree::VERB,
"connect" => login::VERB,
"agree" => agree::VERB
"less_explicit_mode" => less_explicit_mode::VERB,
"login" => login::VERB,
"register" => register::VERB,
};
static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"l" => look::VERB,
"look" => look::VERB,
};
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {

View File

@ -1,7 +1,7 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error};
use crate::models::user::{User, UserTermData};
use async_trait::async_trait;
use ansi_macro::ansi;
use ansi::ansi;
use chrono::Utc;
pub struct Verb;

View File

@ -3,7 +3,7 @@ use super::{
CommandHandlingError::UserError
};
use async_trait::async_trait;
use ansi_macro::ansi;
use ansi::ansi;
use phf::phf_map;
static ALWAYS_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {

View File

@ -0,0 +1,14 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError};
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
let user = ctx.user_dat.as_ref()
.ok_or_else(|| UserError("Not logged in".to_owned()))?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -2,7 +2,7 @@ use super::{
VerbContext, UserVerb, UserVerbRef, UResult
};
use async_trait::async_trait;
use ansi_macro::ansi;
use ansi::ansi;
pub struct Verb;
#[async_trait]

View File

@ -3,7 +3,7 @@ use async_trait::async_trait;
use super::{user_error, parsing::parse_username};
use crate::models::{user::User, item::Item};
use chrono::Utc;
use ansi_macro::ansi;
use ansi::ansi;
use tokio::time;
pub struct Verb;

View File

@ -1,7 +1,6 @@
use crate::DResult;
use crate::db::DBPool;
use crate::models::item::Item;
use itertools::Itertools;
use std::collections::{BTreeSet, BTreeMap};
use log::info;
@ -69,6 +68,7 @@ pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> {
#[cfg(test)]
mod test {
use itertools::Itertools;
use super::*;
#[test]

View File

@ -1,7 +1,7 @@
use super::StaticItem;
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
use ansi_macro::ansi;
use ansi::ansi;
use crate::models::item::Item;
pub struct Zone {