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
//! Parity tests for [`slice_ansi`], pinned against slice-ansi@9 in Node.
//!
//! Each `// node:` comment cites the verified JS output for the assertion below
//! it, produced by running `slice-ansi@9` under Node and copying the result.

use super::slice_ansi;

// ── 1. Plain slice ───────────────────────────────────────────────────────────
// node: sliceAnsi("hello world", 0, 5) === "hello"
// node: sliceAnsi("hello world", 6) === "world"
#[test]
fn plain_slice() {
    assert_eq!(slice_ansi("hello world", 0, Some(5)), "hello");
    assert_eq!(slice_ansi("hello world", 6, None), "world");
}

// ── 2. Colored slice: styles reopened at start, closed at end ─────────────────
// node: sliceAnsi("\x1b[31mhello\x1b[39m", 1, 3) === "\x1b[31mel\x1b[39m"
#[test]
fn colored_slice_reopen_close() {
    assert_eq!(
        slice_ansi("\x1b[31mhello\x1b[39m", 1, Some(3)),
        "\x1b[31mel\x1b[39m"
    );
}

// ── 3. Slice starting mid-string picks up active styles ───────────────────────
// node: sliceAnsi("\x1b[31mhello world\x1b[39m", 6, 11) === "\x1b[31mworld\x1b[39m"
#[test]
fn mid_string_picks_up_active_styles() {
    assert_eq!(
        slice_ansi("\x1b[31mhello world\x1b[39m", 6, Some(11)),
        "\x1b[31mworld\x1b[39m"
    );
}

// ── 4. CJK: a fullwidth char straddling `end` is excluded ─────────────────────
// node: sliceAnsi("a中b", 0, 2) === "a"   (中 is width 2; would overflow end=2)
// node: sliceAnsi("a中b", 0, 3) === "a中"
#[test]
fn cjk_straddle_excluded() {
    assert_eq!(slice_ansi("a中b", 0, Some(2)), "a");
    assert_eq!(slice_ansi("a中b", 0, Some(3)), "a中");
}

// ── 5. Emoji ZWJ cluster never split ──────────────────────────────────────────
// node: sliceAnsi("👨‍👩‍👧‍👦x", 0, 2) === "👨‍👩‍👧‍👦"  (family cluster is width 2)
// node: sliceAnsi("👨‍👩‍👧‍👦x", 0, 3) === "👨‍👩‍👧‍👦x"
#[test]
fn emoji_zwj_not_split() {
    let family = "👨‍👩‍👧‍👦";
    assert_eq!(slice_ansi(&format!("{family}x"), 0, Some(2)), family);
    assert_eq!(
        slice_ansi(&format!("{family}x"), 0, Some(3)),
        format!("{family}x")
    );
}

// ── 6. Flag emoji (regional indicators) ───────────────────────────────────────
// node: sliceAnsi("🇸🇪x", 0, 2) === "🇸🇪"   (flag is width 2)
#[test]
fn flag_emoji_regional_indicators() {
    assert_eq!(slice_ansi("🇸🇪x", 0, Some(2)), "🇸🇪");
}

// ── 7. Hyperlink included / excluded / empty-discarded ────────────────────────
// node: sliceAnsi("\x1b]8;;https://x.com\x07link\x1b]8;;\x07", 0, 4)
//   === "\x1b]8;;https://x.com\x07link\x1b]8;;\x07"
#[test]
fn hyperlink_included() {
    let input = "\x1b]8;;https://x.com\x07link\x1b]8;;\x07";
    assert_eq!(slice_ansi(input, 0, Some(4)), input);
}

// node: sliceAnsi("ab\x1b]8;;https://x.com\x07link\x1b]8;;\x07", 0, 2) === "ab"
// The hyperlink opens after the cut → not included (empty-discard path: an
// opened-but-never-visible hyperlink is discarded).
#[test]
fn hyperlink_excluded_and_discarded() {
    let input = "ab\x1b]8;;https://x.com\x07link\x1b]8;;\x07";
    assert_eq!(slice_ansi(input, 0, Some(2)), "ab");
}

// node: sliceAnsi("\x1b]8;;https://x.com\x07\x1b]8;;\x07ab", 0, 2) === "ab"
// An empty hyperlink (open immediately followed by close) before visible text
// is discarded.
#[test]
fn hyperlink_empty_discarded() {
    let input = "\x1b]8;;https://x.com\x07\x1b]8;;\x07ab";
    assert_eq!(slice_ansi(input, 0, Some(2)), "ab");
}

// ── 8. Hyperlink closed at slice end when still open ──────────────────────────
// node: sliceAnsi("\x1b]8;;https://x.com\x07link text\x1b]8;;\x07", 0, 4)
//   === "\x1b]8;;https://x.com\x07link\x1b]8;;\x07"
#[test]
fn hyperlink_closed_at_end() {
    let input = "\x1b]8;;https://x.com\x07link text\x1b]8;;\x07";
    assert_eq!(
        slice_ansi(input, 0, Some(4)),
        "\x1b]8;;https://x.com\x07link\x1b]8;;\x07"
    );
}

// ── 9. 256-color and 24-bit SGR preserved ─────────────────────────────────────
// node: sliceAnsi("\x1b[38;5;200mhello\x1b[39m", 0, 3) === "\x1b[38;5;200mhel\x1b[39m"
#[test]
fn color256_preserved() {
    assert_eq!(
        slice_ansi("\x1b[38;5;200mhello\x1b[39m", 0, Some(3)),
        "\x1b[38;5;200mhel\x1b[39m"
    );
}

// node: sliceAnsi("\x1b[38;2;255;0;0mhello\x1b[39m", 0, 3)
//   === "\x1b[38;2;255;0;0mhel\x1b[39m"
#[test]
fn truecolor_preserved() {
    assert_eq!(
        slice_ansi("\x1b[38;2;255;0;0mhello\x1b[39m", 0, Some(3)),
        "\x1b[38;2;255;0;0mhel\x1b[39m"
    );
}

// ── 10. end=None slices to end ────────────────────────────────────────────────
// node: sliceAnsi("hello world", 6) === "world"
#[test]
fn end_none_to_end() {
    assert_eq!(slice_ansi("hello world", 6, None), "world");
}

// ── 11. start beyond string → "" ──────────────────────────────────────────────
// node: sliceAnsi("hello", 10, 20) === ""
#[test]
fn start_beyond_string() {
    assert_eq!(slice_ansi("hello", 10, Some(20)), "");
}

// ── 12. ANSI-split grapheme → fallback path ───────────────────────────────────
// "e" + ESC[31m + combining acute (U+0301): the SGR splits the grapheme "é".
// node: sliceAnsi("e\x1b[31ḿx", 0, 1) === "e\x1b[31ḿ\x1b[39m"
#[test]
fn ansi_split_grapheme_fallback() {
    let input = "e\x1b[31m\u{301}x";
    // node verified: the combining mark stays with "e", styled red, closed.
    assert_eq!(slice_ansi(input, 0, Some(1)), "e\x1b[31m\u{301}\x1b[39m");
}

// ── 13. Control sequences (window title) handling ─────────────────────────────
// A control sequence BEFORE the first visible character is not emitted: control
// tokens only append when `include` is already true, and `include` flips on at
// the first in-range visible char.
// node: sliceAnsi("\x1b]0;title\x07hello", 0, 3) === "hel"
#[test]
fn control_sequence_before_visible_dropped() {
    assert_eq!(slice_ansi("\x1b]0;title\x07hello", 0, Some(3)), "hel");
}

// A control sequence BETWEEN visible characters (in range) is passed through.
// node: sliceAnsi("ab\x1b]0;t\x07cd", 0, 4) === "ab\x1b]0;t\x07cd"
#[test]
fn control_sequence_inline_passthrough() {
    assert_eq!(
        slice_ansi("ab\x1b]0;t\x07cd", 0, Some(4)),
        "ab\x1b]0;t\x07cd"
    );
}

// ── 14. pendingSgr rollback at past-end boundary ──────────────────────────────
// An SGR-start immediately before the cut, with no visible char after it within
// range, is rolled back (not emitted) at the boundary.
// node: sliceAnsi("ab\x1b[31mcd\x1b[39m", 0, 2) === "ab"
#[test]
fn pending_sgr_rollback() {
    assert_eq!(slice_ansi("ab\x1b[31mcd\x1b[39m", 0, Some(2)), "ab");
}

// ── 15. C1 CSI sequences ──────────────────────────────────────────────────────
// \x9b is the C1 CSI opener. The SGR fragment's open code preserves the C1
// prefix (getSgrPrefix), so the re-open at the slice start uses the \x9b form,
// while the close uses the end code \x1b[39m.
// node: sliceAnsi("\x9b31mhello\x9b39m", 1, 3) === "\x9b31mel\x1b[39m"
#[test]
fn c1_csi_sequences() {
    assert_eq!(
        slice_ansi("\u{9b}31mhello\u{9b}39m", 1, Some(3)),
        "\u{9b}31mel\x1b[39m"
    );
}

// ── 16. reset mid-string clears styles ────────────────────────────────────────
// node: sliceAnsi("\x1b[31mhe\x1b[0mllo", 0, 4) === "\x1b[31mhe\x1b[0mll"
#[test]
fn reset_mid_string_clears() {
    assert_eq!(
        slice_ansi("\x1b[31mhe\x1b[0mllo", 0, Some(4)),
        "\x1b[31mhe\x1b[0mll"
    );
}

// ── 17. bold+dim / stacked modifier coexistence ───────────────────────────────
// 1 (bold) and 2 (dim) both map to close 22 in ansi-styles, so the Map keys
// collide: the second overwrites the first.
// node: sliceAnsi("\x1b[1m\x1b[2mhi\x1b[22m", 0, 2) === "\x1b[2mhi\x1b[22m"
#[test]
fn bold_dim_map_collision() {
    assert_eq!(
        slice_ansi("\x1b[1m\x1b[2mhi\x1b[22m", 0, Some(2)),
        "\x1b[2mhi\x1b[22m"
    );
}

// Distinct modifiers (bold + underline) coexist and reopen in order.
// node: sliceAnsi("\x1b[1m\x1b[4mhi\x1b[24m\x1b[22m", 0, 2)
//   === "\x1b[1m\x1b[4mhi\x1b[24m\x1b[22m"
#[test]
fn distinct_modifiers_coexist() {
    assert_eq!(
        slice_ansi("\x1b[1m\x1b[4mhi\x1b[24m\x1b[22m", 0, Some(2)),
        "\x1b[1m\x1b[4mhi\x1b[24m\x1b[22m"
    );
}

// ── 18. endCharacter trailing-ANSI pickup (appendTrailingAnsiTokens) ──────────
// A trailing SGR after the last in-range char IS tokenized (appendTrailingAnsi-
// Tokens), but when it has no closing effect (no matching active style) it is
// dropped past the boundary.
// node: sliceAnsi("ab\x1b[39m", 0, 2) === "ab"
#[test]
fn trailing_ansi_no_closing_effect_dropped() {
    assert_eq!(slice_ansi("ab\x1b[39m", 0, Some(2)), "ab");
}

// A trailing SGR that closes an active style is processed; the visible result
// matches the auto-close emitted at the end.
// node: sliceAnsi("\x1b[31mab\x1b[39m", 0, 2) === "\x1b[31mab\x1b[39m"
#[test]
fn trailing_ansi_closes_active_style() {
    assert_eq!(
        slice_ansi("\x1b[31mab\x1b[39m", 0, Some(2)),
        "\x1b[31mab\x1b[39m"
    );
}

// ── Defaults / edge cases ─────────────────────────────────────────────────────
// node: sliceAnsi("", 0, 5) === ""
#[test]
fn empty_input() {
    assert_eq!(slice_ansi("", 0, Some(5)), "");
}

// node: sliceAnsi("hello", 0, 0) === ""
#[test]
fn zero_width_slice() {
    assert_eq!(slice_ansi("hello", 0, Some(0)), "");
}

// Generic OSC control string with embedded spaces (a window-title sequence):
// slice-ansi's own tokenizer parses it as a single zero-width control token, so
// the visible positions are unaffected by its content. (Note: the shared
// `string_width` port over-counts this sequence — documented in cli_truncate —
// but slice_ansi does not depend on string_width.)
// node: sliceAnsi("\x1b]0;window title\x07visible after", 12, 13) === "r"
// node: sliceAnsi("\x1b]0;window title\x07visible after", 0, 2) === "vi"
#[test]
fn generic_osc_control_string_is_zero_width() {
    let t = "\x1b]0;window title\x07visible after";
    assert_eq!(slice_ansi(t, 12, Some(13)), "r");
    assert_eq!(slice_ansi(t, 0, Some(2)), "vi");
}

// ── Adversarial: begin > end ordering ─────────────────────────────────────────
// begin>end is a distinct path from start_beyond_string (which is start>len). If
// the slice loop handled begin>end by under/overflow it could panic or emit text;
// Node returns "". Both the ANSI-wrapped and the plain case return "".
// node: sliceAnsi("\x1b[31mabcdef\x1b[39m", 4, 2) === ""
// node: sliceAnsi("hello world", 5, 2) === ""
#[test]
fn slice_ansi_begin_greater_than_end_returns_empty() {
    assert_eq!(slice_ansi("\x1b[31mabcdef\x1b[39m", 4, Some(2)), "");
    assert_eq!(slice_ansi("hello world", 5, Some(2)), "");
}