mod tokenize_ansi;
use std::collections::BTreeMap;
use tokenize_ansi::{HyperlinkAction, SgrFragment, Token, tokenize_ansi};
pub fn slice_ansi(input: &str, start: usize, end: Option<usize>) -> String {
let tokens = tokenize_ansi(input, end);
let has_continuation_ahead = create_has_continuation_ahead_map(&tokens);
let mut params = SliceState::new();
for (token_index, token) in tokens.iter().enumerate() {
let mut is_past_end = is_past_end_boundary(token, params.position, end);
if is_past_end
&& !matches!(token, Token::Character { .. })
&& has_continuation_ahead[token_index]
{
is_past_end = false;
}
if is_past_end && is_non_continuation_character(token) {
params.roll_back_at_boundary();
break;
}
params.apply_token(token, is_past_end, start);
}
params.finish()
}
struct OrderedStyleMap {
order: Vec<String>,
values: BTreeMap<String, String>,
}
impl OrderedStyleMap {
fn new() -> Self {
Self {
order: Vec::new(),
values: BTreeMap::new(),
}
}
fn clear(&mut self) {
self.order.clear();
self.values.clear();
}
fn delete(&mut self, key: &str) {
if self.values.remove(key).is_none() {
return;
}
if let Some(pos) = self.order.iter().position(|k| k == key) {
self.order.remove(pos);
}
}
fn set(&mut self, key: String, value: String) {
if self.values.insert(key.clone(), value).is_none() {
self.order.push(key);
}
}
fn has(&self, key: &str) -> bool {
self.values.contains_key(key)
}
fn size(&self) -> usize {
self.order.len()
}
fn values_joined(&self) -> String {
let mut out = String::new();
for key in &self.order {
if let Some(v) = self.values.get(key) {
out.push_str(v);
}
}
out
}
fn keys_reversed_joined(&self) -> String {
let mut out = String::new();
for key in self.order.iter().rev() {
out.push_str(key);
}
out
}
fn snapshot(&self) -> StyleSnapshot {
StyleSnapshot {
order: self.order.clone(),
values: self.values.clone(),
}
}
fn restore(&mut self, snapshot: StyleSnapshot) {
self.order = snapshot.order;
self.values = snapshot.values;
}
}
#[derive(Clone)]
struct StyleSnapshot {
order: Vec<String>,
values: BTreeMap<String, String>,
}
fn apply_sgr_fragments(active_styles: &mut OrderedStyleMap, fragments: &[SgrFragment]) {
for fragment in fragments {
match fragment {
SgrFragment::Reset => active_styles.clear(),
SgrFragment::End { end_code } => active_styles.delete(end_code),
SgrFragment::Start { code, end_code } => {
active_styles.delete(end_code);
active_styles.set(end_code.clone(), code.clone());
}
}
}
}
#[derive(Clone)]
struct ActiveHyperlink {
code: String,
close_prefix: String,
terminator: String,
}
fn close_hyperlink(link: &ActiveHyperlink) -> String {
format!("{}{}", link.close_prefix, link.terminator)
}
fn should_include_sgr_after_end(
fragments: &[SgrFragment],
active_styles: &OrderedStyleMap,
) -> bool {
let mut has_start_fragment = false;
let mut has_closing_effect = false;
for fragment in fragments {
match fragment {
SgrFragment::Start { .. } => has_start_fragment = true,
SgrFragment::Reset if active_styles.size() > 0 => has_closing_effect = true,
SgrFragment::End { end_code } if active_styles.has(end_code) => {
has_closing_effect = true;
}
_ => {}
}
}
has_closing_effect && !has_start_fragment
}
fn has_sgr_start_fragment(fragments: &[SgrFragment]) -> bool {
fragments
.iter()
.any(|f| matches!(f, SgrFragment::Start { .. }))
}
struct SliceState {
active_styles: OrderedStyleMap,
active_hyperlink: Option<ActiveHyperlink>,
active_hyperlink_has_visible_text: bool,
active_hyperlink_output_index: Option<usize>,
pending_sgr_output_index: Option<usize>,
pending_sgr_active_styles: Option<StyleSnapshot>,
position: usize,
return_value: String,
include: bool,
}
impl SliceState {
fn new() -> Self {
Self {
active_styles: OrderedStyleMap::new(),
active_hyperlink: None,
active_hyperlink_has_visible_text: false,
active_hyperlink_output_index: None,
pending_sgr_output_index: None,
pending_sgr_active_styles: None,
position: 0,
return_value: String::new(),
include: false,
}
}
fn discard_pending_hyperlink(&mut self) {
self.splice_out_pending_hyperlink();
self.active_hyperlink = None;
self.active_hyperlink_has_visible_text = false;
self.active_hyperlink_output_index = None;
}
fn splice_out_pending_hyperlink(&mut self) {
let Some(link) = &self.active_hyperlink else {
return;
};
if self.active_hyperlink_has_visible_text {
return;
}
let Some(output_index) = self.active_hyperlink_output_index else {
return;
};
let open_code_length = link.code.len();
let mut new_value = String::with_capacity(self.return_value.len());
new_value.push_str(&self.return_value[..output_index]);
new_value.push_str(&self.return_value[output_index + open_code_length..]);
self.return_value = new_value;
if self
.pending_sgr_output_index
.is_some_and(|p| p > output_index)
{
let pending = self.pending_sgr_output_index.expect("checked is_some_and");
self.pending_sgr_output_index = Some(pending - open_code_length);
}
}
fn roll_back_at_boundary(&mut self) {
if self.active_hyperlink.is_some() && !self.active_hyperlink_has_visible_text {
self.discard_pending_hyperlink();
}
if let Some(pending) = self.pending_sgr_output_index {
self.return_value.truncate(pending);
if let Some(snapshot) = self.pending_sgr_active_styles.take() {
self.active_styles.restore(snapshot);
}
self.pending_sgr_output_index = None;
}
}
fn apply_token(&mut self, token: &Token, is_past_end: bool, start: usize) {
match token {
Token::Sgr { code, fragments } => self.apply_sgr_token(code, fragments, is_past_end),
Token::Hyperlink {
code,
action,
close_prefix,
terminator,
} => self.apply_hyperlink_token(code, *action, close_prefix, terminator, is_past_end),
Token::Control { code } => self.apply_control_token(code, is_past_end),
Token::Character {
value,
visible_width,
is_grapheme_continuation,
} => {
self.apply_character_token(value, *visible_width, *is_grapheme_continuation, start)
}
}
}
fn apply_sgr_token(&mut self, code: &str, fragments: &[SgrFragment], is_past_end: bool) {
if is_past_end && !should_include_sgr_after_end(fragments, &self.active_styles) {
return;
}
if self.include
&& has_sgr_start_fragment(fragments)
&& self.pending_sgr_output_index.is_none()
{
self.pending_sgr_output_index = Some(self.return_value.len());
self.pending_sgr_active_styles = Some(self.active_styles.snapshot());
}
apply_sgr_fragments(&mut self.active_styles, fragments);
if self.include {
self.return_value.push_str(code);
}
}
fn apply_hyperlink_token(
&mut self,
code: &str,
action: HyperlinkAction,
close_prefix: &str,
terminator: &str,
is_past_end: bool,
) {
if is_past_end && (action != HyperlinkAction::Close || self.active_hyperlink.is_none()) {
return;
}
match action {
HyperlinkAction::Open => {
self.active_hyperlink = Some(ActiveHyperlink {
code: code.to_owned(),
close_prefix: close_prefix.to_owned(),
terminator: terminator.to_owned(),
});
self.active_hyperlink_has_visible_text = false;
self.active_hyperlink_output_index = None;
if self.include {
self.active_hyperlink_output_index = Some(self.return_value.len());
}
}
HyperlinkAction::Close => {
if self.include
&& self.active_hyperlink.is_some()
&& !self.active_hyperlink_has_visible_text
{
self.discard_pending_hyperlink();
return;
}
self.active_hyperlink = None;
self.active_hyperlink_has_visible_text = false;
self.active_hyperlink_output_index = None;
}
}
if self.include {
self.return_value.push_str(code);
}
}
fn apply_control_token(&mut self, code: &str, is_past_end: bool) {
if !is_past_end && self.include {
self.return_value.push_str(code);
}
}
fn apply_character_token(
&mut self,
value: &str,
visible_width: usize,
is_grapheme_continuation: bool,
start: usize,
) {
if !self.include && self.position >= start && !is_grapheme_continuation {
self.include = true;
self.return_value = self.active_styles.values_joined();
if let Some(link) = &self.active_hyperlink {
self.active_hyperlink_output_index = Some(self.return_value.len());
let code = link.code.clone();
self.return_value.push_str(&code);
}
}
if self.include {
self.return_value.push_str(value);
self.pending_sgr_output_index = None;
self.pending_sgr_active_styles = None;
if self.active_hyperlink.is_some() {
self.active_hyperlink_has_visible_text = true;
}
}
self.position += visible_width;
}
fn finish(mut self) -> String {
if !self.include {
return String::new();
}
if let Some(link) = &self.active_hyperlink {
self.return_value.push_str(&close_hyperlink(link));
}
self.return_value
.push_str(&self.active_styles.keys_reversed_joined());
self.return_value
}
}
fn create_has_continuation_ahead_map(tokens: &[Token]) -> Vec<bool> {
let mut has_continuation_ahead = vec![false; tokens.len()];
let mut next_is_continuation = false;
for token_index in (0..tokens.len()).rev() {
has_continuation_ahead[token_index] = next_is_continuation;
if let Token::Character {
is_grapheme_continuation,
..
} = &tokens[token_index]
{
next_is_continuation = *is_grapheme_continuation;
}
}
has_continuation_ahead
}
fn is_past_end_boundary(token: &Token, position: usize, end: Option<usize>) -> bool {
let Some(end) = end else {
return false;
};
if position >= end {
return true;
}
matches!(
token,
Token::Character {
is_grapheme_continuation: false,
visible_width,
..
} if position + visible_width > end
)
}
fn is_non_continuation_character(token: &Token) -> bool {
matches!(
token,
Token::Character {
is_grapheme_continuation: false,
..
}
)
}
#[cfg(test)]
mod tests;