termcinema-engine 0.1.0

🧠 Core typewriter-style terminal animation engine (SVG renderer) for termcinema
Documentation
//! Parses raw terminal-like script into logical command blocks (prompt / command / output).
//!
//! This module reconstructs structured shell session flows for REPL-style rendering.
//! It is used by termcinema to convert real or simulated terminal logs
//! into data structures suitable for animated SVG output.
//!
//! Pipeline:
//! - Tokenize the script into low-level `ScriptBlock`s (via `tokenizer`);
//! - Group those blocks into higher-level `CommandGroup`s (via `grouper`).
//!
//! Used by: [`render_repl_from_script`] via `glue.rs`

use super::{grouper::group_blocks, tokenizer::parse_script_blocks};
use crate::CommandGroup;

/// Parses the entire input script into a sequence of [`CommandGroup`]s.
///
/// Internally delegates to the tokenizer and grouping modules.
/// This is the main entry point for turning raw shell-like transcripts
/// into structured command/output pairs used in REPL-mode SVG rendering.
pub(crate) fn parse_shell_blocks(input: &str) -> Vec<CommandGroup> {
    let blocks = parse_script_blocks(input);

    /*
        // 🐛 Debug: print all intermediate script blocks
        println!("📝 Parsed ScriptBlocks:");
        for (i, block) in blocks.iter().enumerate() {
            println!("  {}: {:?}", i, block);
        }
    */

    group_blocks(&blocks)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_group_blocks() {
        let input = r#"Last login: Wed Jun  4 23:31:44 on ttys000
[root@localhost:~]# echo "Hello, world!"
Hello, world!
[root@localhost:~]# date
Wed Jun  4 23:31:50 AM EDT 2025
[root@localhost:~]#
[root@localhost:~]#
[root@localhost:~]# cd /etc
[root@localhost:/etc]# cd ssh/
[root@localhost:/etc/ssh]# ps -ef
  UID   PID  PPID   C STIME   TTY           TIME CMD
    0     1     0   0 14 525  ??       109:26.82 /sbin/launchd
    0    88     1   0 14 525  ??        50:56.48 /usr/libexec/logd
"#;

        let cmds = parse_shell_blocks(input);

        for (i, cmd) in cmds.iter().enumerate() {
            println!("🎯 Cmd {}:\n{:#?}", i + 1, cmd);
        }

        // Expected group breakdown:
        // 1. Initial banner output (no prompt or command, only output)
        // 2. Command: echo "Hello, world!" + output
        // 3. Command: date + output
        // 4. Empty prompt (no command or output)
        // 5. Another empty prompt
        // 6. Command: cd /etc
        // 7. Command: cd ssh/
        // 8. Command: ps -ef + multi-line output

        assert!(cmds.len() >= 8);

        // First group should be output-only (no prompt or command)
        assert_eq!(cmds[0].prompt, "");
        assert_eq!(cmds[0].command, "");
        assert!(!cmds[0].output.is_empty());

        // Second group should be a valid echo command
        assert!(cmds[1].prompt.contains("[root@localhost:~]#"));
        assert_eq!(cmds[1].command, r#"echo "Hello, world!""#);
    }
}