inkferro-core 0.1.0

Layout, text measurement, ANSI render, and frame-diff engine for inkferro — a Rust-backed, byte-for-byte drop-in for the ink terminal UI library.
Documentation
//! Port of `reduce.js` from `@alcalzone/ansi-tokenize@0.3.0`.

use super::ansi_codes::{RESET_CODE, is_end_code, is_intensity_code};
use super::types::AnsiToken;

/// Reduces the given array of ANSI codes to the minimum necessary to render
/// with the same style.
///
/// Equivalent to `reduceAnsiCodes(codes)` in the JS source.
pub fn reduce_ansi_codes(codes: &[AnsiToken]) -> Vec<AnsiToken> {
    reduce_ansi_codes_incremental(&[], codes)
}

/// Like [`reduce_ansi_codes`], but assumes `codes` is already reduced.
/// Further reductions are only applied to items in `new_codes`.
///
/// Equivalent to `reduceAnsiCodesIncremental(codes, newCodes)`.
pub fn reduce_ansi_codes_incremental(
    codes: &[AnsiToken],
    new_codes: &[AnsiToken],
) -> Vec<AnsiToken> {
    let mut ret: Vec<AnsiToken> = codes.to_vec();
    reduce_ansi_codes_in_place(&mut ret, new_codes);
    ret
}

/// In-place core of [`reduce_ansi_codes_incremental`]: applies `new_codes` to
/// the already-reduced stack `ret`, mutating it directly.
///
/// Same loop, same semantics — the only difference is that the caller keeps
/// ownership of the `Vec`, so a hot loop (`styled_chars_from_tokens`, one call
/// per SGR token) reuses one buffer instead of cloning the stack into a fresh
/// allocation per token.
pub(crate) fn reduce_ansi_codes_in_place(ret: &mut Vec<AnsiToken>, new_codes: &[AnsiToken]) {
    for code in new_codes {
        if code.code == RESET_CODE {
            // Global reset — clear everything
            ret.clear();
        } else if is_end_code(&code.code) {
            // End/close code — remove all start codes with the same end
            ret.retain(|c| c.end_code != code.code);
        } else {
            // Start code
            if is_intensity_code(code) {
                // Bold/dim can coexist; only add if the exact same code isn't present
                let already = ret
                    .iter()
                    .any(|c| c.code == code.code && c.end_code == code.end_code);
                if !already {
                    ret.push(code.clone());
                }
            } else {
                // Regular start code: remove any existing code with the same end_code,
                // then push the new one
                ret.retain(|c| c.end_code != code.end_code);
                ret.push(code.clone());
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::text::ansi_tokenize::ansi_codes::get_end_code;

    fn tok(code: &str) -> AnsiToken {
        AnsiToken {
            end_code: get_end_code(code),
            code: code.into(),
        }
    }

    // Test 13: reduceAnsiCodes handles reset, override, intensity coexistence
    #[test]
    fn reduce_handles_reset() {
        let codes = vec![tok("\x1B[31m"), tok("\x1B[1m")];
        let result = reduce_ansi_codes_incremental(&codes, &[tok("\x1B[0m")]);
        assert!(result.is_empty(), "reset should clear all codes");
    }

    #[test]
    fn reduce_handles_override() {
        // Red then blue — blue should override red (both end with 39m)
        let codes = vec![tok("\x1B[31m")];
        let result = reduce_ansi_codes_incremental(&codes, &[tok("\x1B[34m")]);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].code, "\x1B[34m");
    }

    #[test]
    fn reduce_intensity_coexistence() {
        // Bold (1m) and dim (2m) share end_code 22m but can coexist
        let result = reduce_ansi_codes(&[tok("\x1B[1m"), tok("\x1B[2m")]);
        assert_eq!(result.len(), 2);
        assert!(result.iter().any(|c| c.code == "\x1B[1m"));
        assert!(result.iter().any(|c| c.code == "\x1B[2m"));
    }

    #[test]
    fn reduce_intensity_no_duplicate() {
        // Adding bold twice should not duplicate it
        let result = reduce_ansi_codes(&[tok("\x1B[1m"), tok("\x1B[1m")]);
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn reduce_end_code_removes_start() {
        // Adding \x1B[39m should remove any fg color
        let codes = vec![tok("\x1B[31m")];
        let result = reduce_ansi_codes_incremental(&codes, &[tok("\x1B[39m")]);
        assert!(result.is_empty());
    }
}