sokoban-elements 0.2.1

Parsing and writing Sokoban, Hexoban, Trioban and Octoban file formats.
Documentation
// Regenerate grammar:
//
// ```
// peginator-cli --railroad src/rle/grammar.peg > doc/img/rle_grammar.svg
// peginator-cli src/rle/grammar.peg | rustfmt +nightly > src/rle/grammar.rs
// ```
//
// Grammar described by these rules is similar to following Peg.JS grammar
// (pegsh http://phrogz.net/js/pegsh/):
//
// ```
// grammar               = atoms_or_rle_or_group+
// atoms_or_rle_or_group = atoms / rle_chunk / group
// atom_or_group         = atom / group
// rle_chunk             = count atom_or_group
// group                 = "(" atoms_or_rle_or_group+ ")"
// atoms                 = atom+
// atom                  = [^()0-9]
// count                 = [0-9]+
// ```

use peginator::PegParser;

use super::grammar::{CharsOrGroup, Group, Rle as RleGrammar, RleChunk, Section};
use super::{Rle, RleSyntaxError, RleSyntaxErrorKind};

pub struct Parser {}

impl Parser {
    pub fn parse(input: &str, drop_spaces: bool) -> Result<String, RleSyntaxError> {
        let mut retv = String::new();

        match RleGrammar::parse(input) {
            Ok(rle) => {
                for section in rle.sections {
                    retv.push_str(&Self::process_section(&section));
                }
            }
            Err(e) => {
                return Err(RleSyntaxError(RleSyntaxErrorKind::DecodingFailure {
                    position: e.position,
                    input:    input.into(),
                }));
            }
        }

        Ok(retv
            .chars()
            .map(|chr| if chr == Rle::EOL { '\n' } else { chr })
            .filter(|chr| {
                if drop_spaces {
                    !chr.is_ascii_whitespace()
                } else {
                    true
                }
            })
            .collect())
    }

    fn process_section(section: &Section) -> String {
        match section {
            Section::Chars(chars) => chars.clone(),
            Section::Group(group) => Self::process_group(group),
            Section::RleChunk(rle_chunk) => Self::process_rle_chunk(rle_chunk),
        }
    }

    fn process_group(group: &Group) -> String {
        let mut group_str = String::new();

        for group_section in group {
            group_str.push_str(&Self::process_section(group_section));
        }
        group_str
    }

    fn process_rle_chunk(rle_chunk: &RleChunk) -> String {
        let Ok(count) = rle_chunk.count.parse::<usize>() else {
            unreachable!(
                "internal bug, failed to convert already parsed RLE count sequence {}!",
                rle_chunk.count
            )
        };

        match &rle_chunk.data {
            CharsOrGroup::Chars(chars) => {
                let mut first = chars[..1].to_string().repeat(count);
                first.push_str(&chars[1..]);
                first.clone()
            }
            CharsOrGroup::Group(group) => Self::process_group(group).repeat(count),
        }
    }
}

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

    #[test]
    fn it_decodes_rle_encoded_string() -> Result<(), RleSyntaxError> {
        for (input, output) in [
            ("3r4U", "rrrUUUU"),
            ("3rabcd4U", "rrrabcdUUUU"),
            ("2(3(dr)R)", "drdrdrRdrdrdrR"),
            ("4a4b2cd3e", "aaaabbbbccdeee"),
            ("aa2(bb)cc2(dd2(ee)ff)", "aabbbbccddeeeeffddeeeeff"),
            ("2a4b2c2d2e|2e2f2d4e2f", "aabbbbccddee\neeffddeeeeff"),
            ("2a4b2c2d2e5 2e2f2d4e2f", "aabbbbccddee     eeffddeeeeff"),
            ("2abc3def3(adfdf)2abc3def", "aabcdddefadfdfadfdfadfdfaabcdddef"),
            ("2abc3def3(2a3bc)2abc3def", "aabcdddefaabbbcaabbbcaabbbcaabcdddef"),
            (
                "2abc3def2(fdsfs2(dfgh)gtr)2abc3def",
                "aabcdddeffdsfsdfghdfghgtrfdsfsdfghdfghgtraabcdddef",
            ),
            (
                "2abc3def2(2a2bc2(2a3b)2d)2abc3def",
                "aabcdddefaabbcaabbbaabbbddaabbcaabbbaabbbddaabcdddef",
            ),
            ("2(abcd)efgh", "abcdabcdefgh"),
            ("2(3abcd)efgh", "aaabcdaaabcdefgh"),
            ("2(abcd)3efgh", "abcdabcdeeefgh"),
            ("(abcd)3efgh", "abcdeeefgh"),
            ("(ab3cd)efgh", "abcccdefgh"),
            ("(ab2(3c)d)efgh", "abccccccdefgh"),
            ("efgh(ab3cd)", "efghabcccd"),
        ] {
            assert_eq!(Parser::parse(input, false)?, output);
        }

        Ok(())
    }

    #[test]
    fn it_preserves_new_lines() -> Result<(), RleSyntaxError> {
        for (input, output) in [
            ("\n\n\n", "\n\n\n"),
            ("\n|\n", "\n\n\n"),
            ("|||", "\n\n\n"),
            ("\n  3r   4U\n", "\n  rrr   UUUU\n"),
            (
                "3-#-#-#-#10-|2-#7-#9-|-#-@5-#10-",
                "---#-#-#-#----------\n--#-------#---------\n-#-@-----#----------",
            ),
        ] {
            assert_eq!(Parser::parse(input, false)?, output);
        }

        Ok(())
    }

    #[test]
    fn it_refuses_to_decode_only_digits() {
        assert!(Parser::parse("12345678", false).is_err());
        assert!(Parser::parse("  1 2  3  \t 45 \n \r678", false).is_err());
    }
}