slash-lang 0.1.0

Parser and AST for the slash-command language
Documentation
use crate::parser::errors::ParseError;

/// A classified token produced by the lexer.
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    /// A slash-command token, e.g. `"/Build.target(release)!"`
    Command(String),
    /// `|`
    Pipe,
    /// `|&`
    PipeErr,
    /// `&&`
    And,
    /// `||`
    Or,
    /// `>`
    Redirect,
    /// `>>`
    Append,
    /// Any other token (redirection target, bare word, standalone `!`, etc.)
    Word(String),
}

/// Tokenize input, splitting on whitespace but respecting balanced parentheses.
///
/// Content inside `(...)` is kept as a single token even if it contains spaces.
/// This allows `/echo(hello world)` to remain one token.
pub fn lex(input: &str) -> Result<Vec<Token>, ParseError> {
    let raw_tokens = split_respecting_parens(input);
    let tokens = raw_tokens.into_iter().map(|s| classify(&s)).collect();
    Ok(tokens)
}

fn classify(s: &str) -> Token {
    match s {
        "|" => Token::Pipe,
        "|&" => Token::PipeErr,
        "&&" => Token::And,
        "||" => Token::Or,
        ">" => Token::Redirect,
        ">>" => Token::Append,
        _ if s.starts_with('/') => Token::Command(s.to_string()),
        _ => Token::Word(s.to_string()),
    }
}

/// Split on ASCII whitespace, but keep content inside balanced `()` together.
fn split_respecting_parens(input: &str) -> Vec<String> {
    let mut tokens = Vec::new();
    let mut current = String::new();
    let mut depth: usize = 0;

    for ch in input.chars() {
        match ch {
            '(' => {
                depth += 1;
                current.push(ch);
            }
            ')' => {
                depth = depth.saturating_sub(1);
                current.push(ch);
            }
            c if c.is_ascii_whitespace() && depth == 0 => {
                if !current.is_empty() {
                    tokens.push(std::mem::take(&mut current));
                }
            }
            _ => {
                current.push(ch);
            }
        }
    }

    if !current.is_empty() {
        tokens.push(current);
    }

    tokens
}