cktrs 0.1.0

A rust(🚀) parser for the CKT(🚀) config language.
Documentation
/*

    ATTENTION!!!

    Abandon all hope, ye who enter here

*/

//! A rust(🚀) parser for the [CKT](https://git.sr.ht/~cricket/ckt)(🚀) config language. 

use std::collections::HashMap;
use regex::Regex;

/// Enum for tokens returned by parsing. 
#[derive(Debug)]
pub enum Tokens {
    Null,
    Boolean(bool),
    String(String),
    Table(HashMap<String, Tokens>)
}

/// Parse a CKT config into a hashmap of tokens
///
/// Example:
///
/// ```rs
/// use cktrs::parse;
/// use std::fs;
///
/// fn main() {
///    let unparsed_file = fs::read_to_string("test.ckt").expect("cannot read file");
///
///    let file = parse(&unparsed_file)
///        .expect("unsuccessful parse");
///
///    for (key, value) in file.into_iter() {
///        println!("{}: {:?}", key, value);
///    }
/// }
/// ```
pub fn parse(ckt: &String) -> Result<HashMap<String, Tokens>, String> {
    return run(ckt, 0, false).0
}

fn run(ckt: &String, offset: usize, spawned: bool) -> (Result<HashMap<String, Tokens>, String>,usize) {
    let mut cfg = HashMap::new();
    let mut tableindex = 0;
    let commentslurper = Regex::new(r"(?m)^\s*#.*").unwrap();
    let appr = commentslurper.replace_all(ckt, "");
    let mut skipto = offset;

    let mut reading = false;
    let mut quoted = false;
    let mut backslashed = false;
    let mut assigning = false;
    let mut trimval = true;
    let mut multiline = false;
    let mut multikey = "".to_string();
    let mut buffer = Vec::new();
    let mut key = Vec::new();
    for (i, c) in appr.chars().enumerate() {
        if skipto > i { continue }
        if c == '\\' {
            if backslashed {
                if reading {
                    buffer.push('\\');
                }
            } else {
                backslashed = true; // toggle escape
            }
            continue;
        }
        if !reading {
            if c == ' ' || c == '\t' || c == '\n' {
                continue; //slurp whitespace
            } else {
                if c == '"' {
                    quoted = true; // start reading quoted value
                    reading = true;
                    if assigning {
                        trimval = false;
                    }
                    multiline = false;
                    multikey = "".to_string();
                } else if c == '[' {
                    let res = run(ckt, i+1, true);
                    skipto = res.1;
                    multiline = false;
                    multikey = "".to_string();
                    if assigning {
                        let val = match res.0 {
                            Ok(v) => {
                                Tokens::Table(v)
                            }
                            Err(e) => {
                                return (Err(e.clone()),0);
                            }
                        };
                        cfg.insert(key.iter().collect::<String>().trim().to_string(), val);
                        key = Vec::new();
                        buffer = Vec::new();
                        assigning = false;
                    } else {
                        let val = match res.0 {
                            Ok(v) => {
                                Tokens::Table(v)
                            }
                            Err(e) => {
                                return (Err(e.clone()),0);
                            }
                        };
                        cfg.insert(tableindex.to_string(), val);
                        tableindex += 1;
                        buffer = Vec::new();
                    }
                } else if c == ']' {
                    if spawned {
                        return (Ok(cfg),i+1);
                    }
                } else if c == '|' {
                    if multiline {
                        reading = true;
                    } else {
                        multiline = true;
                        reading = true;
                    }
                } else {
                    buffer.push(c);
                    reading = true;
                    multiline = false;
                    multikey = "".to_string();
                }
            }
        } else {
            if c == '=' {
                reading = false;
                assigning = true;
                if multiline {
                    return (Err("Cannot use multiline string as key".to_string()),0);
                }
                key = buffer.clone(); // stop reading key and start reading value
                buffer = Vec::new();
            }
            if c == 'n' && backslashed {
                buffer.push('\n');
            }
            if c == '"' {
                if quoted {
                    if backslashed {
                        buffer.push('"');
                        backslashed = false;
                    } else {
                        quoted = false;
                    }
                } else {
                    buffer.push('"');
                }
            }
            if c == '\n' || c == ';' || c == ']' || c == ',' {
                if c == '\n' || c == ';' || c == ',' { reading = false; }
                if backslashed && c == '\n' {
                    return (Err("Cannot escape newline".to_string()),0);
                }
                if backslashed && (c == ';' || c == ']' || c == ',') {
                    buffer.push(c);
                    backslashed = false;
                }
                if quoted && c == '\n' {
                    return (Err("Expected end quote before linebreak".to_string()),0);
                }
                if quoted && (c == ';' || c == ']' || c == ',') {
                    buffer.push(c);
                }
                if assigning {
                    if buffer.len() == 0 {
                        return (Err("Expected value after assignment operator (=)".to_string()),0);
                    } else {
                        let mut v: String = buffer.iter().collect();
                        let k = key.iter().collect::<String>();
                        if multiline {
                            if multikey == "" {
                                multikey = k.trim().to_string();
                            } else {
                                let oldv = match cfg.get(&multikey) {
                                    Some(a) => match a {
                                        Tokens::String(o) => o.to_owned(),
                                        Tokens::Null => "null".to_string(),
                                        Tokens::Boolean(o) => if o.to_owned() { "true".to_string() } else { "false".to_string() },
                                        Tokens::Table(_o) => {
                                            return (Err("Something horrible has happened".to_string()),0);
                                        }
                                    }
                                    None => "".to_string()
                                };
                                v = if buffer.first().unwrap() == &']' && multiline { format!("{}{}",oldv,v) } else { format!("{}\n{}",oldv,v) }
                            }
                        }
                        if trimval {
                            v = v.trim().to_string();
                        }
                        let t = if v == "true" {
                            Tokens::Boolean(true)
                        } else if v == "false" {
                            Tokens::Boolean(false)
                        } else if v == "null" {
                            Tokens::Null
                        } else {
                            Tokens::String(v)
                        };
                        if multikey == "" {
                            cfg.insert(key.iter().collect::<String>().trim().to_string(), t);
                        } else {
                            cfg.insert(multikey.clone(), t);
                        }
                        buffer = Vec::new();
                        key = Vec::new();
                        assigning = false;
                    }
                } else {
                    let mut v: String = buffer.iter().collect();
                    let k = tableindex.to_string();
                    if multiline {
                        if multikey == "" {
                            multikey = k;
                        } else {
                            let oldv = match cfg.get(&multikey) {
                                Some(a) => match a {
                                    Tokens::String(o) => o.to_owned(),
                                    Tokens::Null => "null".to_string(),
                                    Tokens::Boolean(o) => if o.to_owned() { "true".to_string() } else { "false".to_string() },
                                    Tokens::Table(_o) => {
                                        return (Err("Something horrible has happened".to_string()),0);
                                    }
                                }
                                None => "".to_string()
                            };
                            v = if buffer.first().unwrap() == &']' && multiline { format!("{}{}",oldv,v) } else { format!("{}\n{}",oldv,v) }
                        }
                    }
                    if trimval {
                        v = v.trim().to_string();
                    }
                    let t = if v == "true" {
                        Tokens::Boolean(true)
                    } else if v == "false" {
                        Tokens::Boolean(false)
                    } else if v == "null" {
                        Tokens::Null
                    } else {
                        Tokens::String(v)
                    };
                    if multikey == "" {
                        cfg.insert(tableindex.to_string(), t);
                        tableindex += 1;
                    } else {
                        cfg.insert(multikey.clone(), t);
                    }
                    buffer = Vec::new();
                }
                if c == ']' {
                    if multiline && reading {
                        reading = true;
                        
                    } else {
                        if spawned {
                            return (Ok(cfg),i+1);
                        }
                    }
                }
            }
            if c != '=' && c != '"' && c != '\n' && c != ';' && c != ',' {
                if !backslashed {
                    buffer.push(c); // put value into buffer
                } else {
                    backslashed = false;
                }
            }
        }
    }
    if spawned {
        return (Err("Expected closing bracket".to_string()),0);
    }
    (Ok(cfg),0)
}

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

    #[test]
    fn exampletest() {
        let unparsed_file = fs::read_to_string("test.ckt").expect("cannot read file");

        let file = parse(&unparsed_file)
            .expect("unsuccessful parse");

        for (key, value) in file.into_iter() {
            println!("{}: {:?}", key, value);
        }
    }
}