blob: 63e0ad825faebb58f2f3a06517356a4fe23d6107 [file] [log] [blame]
use std::borrow::Cow;
/// The function cuts the string to a specific width.
/// Preserving colors with `ansi` feature on.
pub(crate) fn split_str(s: &str, width: usize) -> (Cow<'_, str>, Cow<'_, str>) {
#[cfg(feature = "ansi")]
{
const REPLACEMENT: char = '\u{FFFD}';
let stripped = ansi_str::AnsiStr::ansi_strip(s);
let (length, cutwidth, csize) = split_at_width(&stripped, width);
let (mut lhs, mut rhs) = ansi_str::AnsiStr::ansi_split_at(s, length);
if csize > 0 {
let mut buf = lhs.into_owned();
let count_unknowns = width - cutwidth;
buf.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns));
lhs = Cow::Owned(buf);
rhs = Cow::Owned(ansi_str::AnsiStr::ansi_cut(rhs.as_ref(), csize..).into_owned());
}
(lhs, rhs)
}
#[cfg(not(feature = "ansi"))]
{
const REPLACEMENT: char = '\u{FFFD}';
let (length, cutwidth, csize) = split_at_width(s, width);
let (lhs, rhs) = s.split_at(length);
if csize == 0 {
return (Cow::Borrowed(lhs), Cow::Borrowed(rhs));
}
let count_unknowns = width - cutwidth;
let mut buf = lhs.to_owned();
buf.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns));
(Cow::Owned(buf), Cow::Borrowed(&rhs[csize..]))
}
}
/// The function cuts the string to a specific width.
/// Preserving colors with `ansi` feature on.
pub(crate) fn cut_str(s: &str, width: usize) -> Cow<'_, str> {
#[cfg(feature = "ansi")]
{
const REPLACEMENT: char = '\u{FFFD}';
let stripped = ansi_str::AnsiStr::ansi_strip(s);
let (length, cutwidth, csize) = split_at_width(&stripped, width);
let mut buf = ansi_str::AnsiStr::ansi_cut(s, ..length);
if csize != 0 {
let mut b = buf.into_owned();
let count_unknowns = width - cutwidth;
b.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns));
buf = Cow::Owned(b);
}
buf
}
#[cfg(not(feature = "ansi"))]
{
cut_str2(s, width)
}
}
/// The function cuts the string to a specific width.
/// While not preserving ansi sequences.
pub(crate) fn cut_str2(text: &str, width: usize) -> Cow<'_, str> {
const REPLACEMENT: char = '\u{FFFD}';
let (length, cutwidth, csize) = split_at_width(text, width);
if csize == 0 {
let buf = &text[..length];
return Cow::Borrowed(buf);
}
let buf = &text[..length];
let mut buf = buf.to_owned();
let count_unknowns = width - cutwidth;
buf.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns));
Cow::Owned(buf)
}
/// The function splits a string in the position and
/// returns a exact number of bytes before the position and in case of a split in an unicode grapheme
/// a width of a character which was tried to be splited in.
pub(crate) fn split_at_width(s: &str, at_width: usize) -> (usize, usize, usize) {
let mut length = 0;
let mut width = 0;
for c in s.chars() {
if width == at_width {
break;
};
let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or_default();
let c_length = c.len_utf8();
// We cut the chars which takes more then 1 symbol to display,
// in order to archive the necessary width.
if width + c_width > at_width {
return (length, width, c_length);
}
width += c_width;
length += c_length;
}
(length, width, 0)
}
/// Strip OSC codes from `s`. If `s` is a single OSC8 hyperlink, with no other text, then return
/// (s_with_all_hyperlinks_removed, Some(url)). If `s` does not meet this description, then return
/// (s_with_all_hyperlinks_removed, None). Any ANSI color sequences in `s` will be retained. See
/// <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>
///
/// The function is based on Dan Davison <https://github.com/dandavison> delta <https://github.com/dandavison/delta> ansi library.
#[cfg(feature = "ansi")]
pub(crate) fn strip_osc(text: &str) -> (String, Option<String>) {
#[derive(Debug)]
enum ExtractOsc8HyperlinkState {
ExpectOsc8Url,
ExpectFirstText,
ExpectMoreTextOrTerminator,
SeenOneHyperlink,
WillNotReturnUrl,
}
use ExtractOsc8HyperlinkState::*;
let mut url = None;
let mut state = ExpectOsc8Url;
let mut buf = String::with_capacity(text.len());
for el in ansitok::parse_ansi(text) {
match el.kind() {
ansitok::ElementKind::Osc => match state {
ExpectOsc8Url => {
url = Some(&text[el.start()..el.end()]);
state = ExpectFirstText;
}
ExpectMoreTextOrTerminator => state = SeenOneHyperlink,
_ => state = WillNotReturnUrl,
},
ansitok::ElementKind::Sgr => buf.push_str(&text[el.start()..el.end()]),
ansitok::ElementKind::Csi => buf.push_str(&text[el.start()..el.end()]),
ansitok::ElementKind::Esc => {}
ansitok::ElementKind::Text => {
buf.push_str(&text[el.start()..el.end()]);
match state {
ExpectFirstText => state = ExpectMoreTextOrTerminator,
ExpectMoreTextOrTerminator => {}
_ => state = WillNotReturnUrl,
}
}
}
}
match state {
WillNotReturnUrl => (buf, None),
_ => {
let url = url.and_then(|s| {
s.strip_prefix("\x1b]8;;")
.and_then(|s| s.strip_suffix('\x1b'))
});
if let Some(url) = url {
(buf, Some(url.to_string()))
} else {
(buf, None)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grid::util::string::string_width;
#[cfg(feature = "ansi")]
use owo_colors::{colors::Yellow, OwoColorize};
#[test]
fn strip_test() {
assert_eq!(cut_str("123456", 0), "");
assert_eq!(cut_str("123456", 3), "123");
assert_eq!(cut_str("123456", 10), "123456");
assert_eq!(cut_str("a week ago", 4), "a we");
assert_eq!(cut_str("😳😳😳😳😳", 0), "");
assert_eq!(cut_str("😳😳😳😳😳", 3), "😳�");
assert_eq!(cut_str("😳😳😳😳😳", 4), "😳😳");
assert_eq!(cut_str("😳😳😳😳😳", 20), "😳😳😳😳😳");
assert_eq!(cut_str("🏳️🏳️", 0), "");
assert_eq!(cut_str("🏳️🏳️", 1), "🏳");
assert_eq!(cut_str("🏳️🏳️", 2), "🏳\u{fe0f}🏳");
assert_eq!(string_width("🏳️🏳️"), string_width("🏳\u{fe0f}🏳"));
assert_eq!(cut_str("🎓", 1), "�");
assert_eq!(cut_str("🎓", 2), "🎓");
assert_eq!(cut_str("🥿", 1), "�");
assert_eq!(cut_str("🥿", 2), "🥿");
assert_eq!(cut_str("🩰", 1), "�");
assert_eq!(cut_str("🩰", 2), "🩰");
assert_eq!(cut_str("👍🏿", 1), "�");
assert_eq!(cut_str("👍🏿", 2), "👍");
assert_eq!(cut_str("👍🏿", 3), "👍�");
assert_eq!(cut_str("👍🏿", 4), "👍🏿");
assert_eq!(cut_str("🇻🇬", 1), "🇻");
assert_eq!(cut_str("🇻🇬", 2), "🇻🇬");
assert_eq!(cut_str("🇻🇬", 3), "🇻🇬");
assert_eq!(cut_str("🇻🇬", 4), "🇻🇬");
}
#[cfg(feature = "ansi")]
#[test]
fn strip_color_test() {
let numbers = "123456".red().on_bright_black().to_string();
assert_eq!(cut_str(&numbers, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m");
assert_eq!(
cut_str(&numbers, 3),
"\u{1b}[31;100m123\u{1b}[39m\u{1b}[49m"
);
assert_eq!(cut_str(&numbers, 10), "\u{1b}[31;100m123456\u{1b}[0m");
let emojies = "😳😳😳😳😳".red().on_bright_black().to_string();
assert_eq!(cut_str(&emojies, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m");
assert_eq!(
cut_str(&emojies, 3),
"\u{1b}[31;100m😳\u{1b}[39m\u{1b}[49m�"
);
assert_eq!(
cut_str(&emojies, 4),
"\u{1b}[31;100m😳😳\u{1b}[39m\u{1b}[49m"
);
assert_eq!(cut_str(&emojies, 20), "\u{1b}[31;100m😳😳😳😳😳\u{1b}[0m");
let emojies = "🏳️🏳️".red().on_bright_black().to_string();
assert_eq!(cut_str(&emojies, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m");
assert_eq!(cut_str(&emojies, 1), "\u{1b}[31;100m🏳\u{1b}[39m\u{1b}[49m");
assert_eq!(
cut_str(&emojies, 2),
"\u{1b}[31;100m🏳\u{fe0f}🏳\u{1b}[39m\u{1b}[49m"
);
assert_eq!(
string_width(&emojies),
string_width("\u{1b}[31;100m🏳\u{fe0f}🏳\u{1b}[39m\u{1b}[49m")
);
}
#[test]
#[cfg(feature = "ansi")]
fn test_color_strip() {
let s = "Collored string"
.fg::<Yellow>()
.on_truecolor(12, 200, 100)
.blink()
.to_string();
assert_eq!(
cut_str(&s, 1),
"\u{1b}[5m\u{1b}[48;2;12;200;100m\u{1b}[33mC\u{1b}[25m\u{1b}[39m\u{1b}[49m"
)
}
#[test]
#[cfg(feature = "ansi")]
fn test_srip_osc() {
assert_eq!(
strip_osc("just a string here"),
(String::from("just a string here"), None)
);
assert_eq!(
strip_osc("/etc/rc.conf"),
(String::from("/etc/rc.conf"), None)
);
assert_eq!(
strip_osc(
"https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982"
),
(String::from("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982"), None)
);
assert_eq!(
strip_osc(&build_link_prefix_suffix("just a string here")),
(String::default(), Some(String::from("just a string here")))
);
assert_eq!(
strip_osc(&build_link_prefix_suffix("/etc/rc.conf")),
(String::default(), Some(String::from("/etc/rc.conf")))
);
assert_eq!(
strip_osc(
&build_link_prefix_suffix("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982")
),
(String::default(), Some(String::from("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982")))
);
#[cfg(feature = "ansi")]
fn build_link_prefix_suffix(url: &str) -> String {
// https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
let osc8 = "\x1b]8;;";
let st = "\x1b\\";
format!("{osc8}{url}{st}")
}
}
}