bitsy-parser 0.70.2

A parser and utilities for working with Bitsy game data
Documentation
extern crate loe;

use std::io::Cursor;
use radix_fmt::radix_36;
use loe::{process, Config, TransformMode};

pub mod colour;
pub mod dialogue;
pub mod ending;
pub mod exit;
pub mod game;
pub mod image;
pub mod item;
pub mod mock;
pub mod palette;
pub mod position;
pub mod room;
pub mod sprite;
pub mod text;
pub mod tile;
pub mod variable;

pub mod test_omnibus;

use colour::Colour;
use dialogue::Dialogue;
use ending::Ending;
use exit::{Exit, Transition};
use game::{Game, Version};
use image::Image;
use item::Item;
use palette::Palette;
use position::Position;
use room::Room;
use sprite::Sprite;
use std::fmt::Display;
use text::{Font, TextDirection};
use tile::Tile;
use variable::Variable;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Instance {
    position: Position,
    id: String, // item / ending id
}

/// a Room can have many Exits in different positions,
/// optionally with a transition and dialogue
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExitInstance {
    position: Position,
    exit: Exit,
    transition: Option<Transition>,
    dialogue_id: Option<String>,
}

pub trait AnimationFrames {
    fn to_string(&self) -> String;
}

impl AnimationFrames for Vec<Image> {
    #[inline]
    fn to_string(&self) -> String {
        let mut string = String::new();
        let last_frame = self.len() - 1;

        for (i, frame) in self.into_iter().enumerate() {
            string.push_str(&frame.to_string());

            if i < last_frame {
                string.push_str(&"\n>\n".to_string());
            }
        }

        string
    }
}

/// this doesn't work inside ToBase36 for some reason
#[inline]
fn to_base36(int: u64) -> String {
    format!("{}", radix_36(int))
}

pub trait ToBase36 {
    fn to_base36(&self) -> String;
}

impl ToBase36 for u64 {
    #[inline]
    fn to_base36(&self) -> String {
        to_base36(*self)
    }
}

/// e.g. `\nNAME DLG_0`
#[inline]
fn optional_data_line<T: Display>(label: &str, item: Option<T>) -> String {
    if item.is_some() {
        format!("\n{} {}", label, item.unwrap())
    } else {
        "".to_string()
    }
}

#[inline]
fn transform_line_endings(input: String, mode: TransformMode) -> String {
    let mut input = Cursor::new(input);
    let mut output = Cursor::new(Vec::new());

    process(&mut input, &mut output, Config::default().transform(mode)).unwrap();
    String::from_utf8(output.into_inner()).unwrap()
}

#[inline]
fn segments_from_string(string: String) -> Vec<String> {
    // this is pretty weird but a dialogue can just have an empty line followed by a name
    // however, on entering two empty lines, dialogue will be wrapped in triple quotation marks
    // so, handle this here
    let string = string.replace("\n\nNAME", "\n\"\"\"\n\"\"\"\nNAME");

    let mut output:Vec<String> = Vec::new();
    // are we inside `"""\n...\n"""`? if so, ignore empty lines
    let mut inside_escaped_block = false;
    let mut current_segment : Vec<String> = Vec::new();

    for line in string.lines() {
        if line == "\"\"\"" {
            inside_escaped_block = ! inside_escaped_block;
        }

        if line == "" && !inside_escaped_block {
            output.push(current_segment.join("\n"));
            current_segment = Vec::new();
        } else {
            current_segment.push(line.to_string());
        }
    }

    output.push(current_segment.join("\n"));

    output
}

/// tries to use an existing ID - if it is already in use, generate a new one
/// then return the ID (either original or new)
/// todo refactor (unnecessary clones etc.)
fn try_id(ids: Vec<String>, id: String) -> String {
    let id = id.clone();
    let ids = ids.clone();
    if is_id_available(&ids, &id) {
        id
    } else {
        new_unique_id(ids)
    }
}

fn is_id_available(ids: &Vec<String>, id: &String) -> bool {
    ! ids.contains(id)
}

/// e.g. pass all tile IDs into this to get a new non-conflicting tile ID
#[inline]
fn new_unique_id(ids: Vec<String>) -> String {
    let mut new_id: u64 = 0;

    while ids.contains(&new_id.to_base36()) {
        new_id += 1;
    }

    return to_base36(new_id);
}

pub trait Quote {
    fn quote(&self) -> String;
}

impl Quote for String {
    #[inline]
    fn quote(&self) -> String {
        format!("\"\"\"\n{}\n\"\"\"", self).to_string()
    }
}

pub trait Unquote {
    fn unquote(&self) -> String;
}

impl Unquote for String {
    #[inline]
    fn unquote(&self) -> String {
        self.trim_matches('\"').trim_matches('\n').to_string()
    }
}

#[cfg(test)]
mod test {
    use crate::{ToBase36, optional_data_line, mock, segments_from_string, Quote, Unquote, new_unique_id, try_id};

    #[test]
    fn test_to_base36() {
        assert_eq!((37 as u64).to_base36(), "11");
    }

    #[test]
    fn test_optional_data_line() {
        let output = optional_data_line("NAME", mock::item().name);
        assert_eq!(output, "\nNAME door".to_string());
    }

    #[test]
    fn test_string_to_segments() {
        let output = segments_from_string(
            include_str!("./test-resources/segments").to_string()
        );

        let expected = vec![
            "\"\"\"\nthe first segment is a long bit of text\n\n\nit contains empty lines\n\n\"\"\"".to_string(),
            "this is a new segment\nthis is still the second segment\nblah\nblah".to_string(),
            "DLG SEGMENT_3\n\"\"\"\nthis is a short \"long\" bit of text\n\"\"\"".to_string(),
            "this is the last segment".to_string(),
        ];

        assert_eq!(output, expected);
    }

    #[test]
    fn test_quote() {
        let output = "this is a string.\nIt has 2 lines".to_string().quote();
        let expected = "\"\"\"\nthis is a string.\nIt has 2 lines\n\"\"\"".to_string();
        assert_eq!(output, expected);
    }

    #[test]
    fn test_unquote() {
        let output = "\"\"\"\nwho the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off\n\"\"\"".to_string().unquote();
        let expected = "who the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off".to_string();
        assert_eq!(output, expected);
    }

    #[test]
    fn test_try_id() {
        // does a conflict generate a new ID?
        assert_eq!(
            try_id(vec!["0".to_string(), "1".to_string()], "1".to_string()),
            "2".to_string()
        );
        // with no conflict, does the ID remain the same?
        assert_eq!(
            try_id(vec!["0".to_string(), "1".to_string()], "3".to_string()),
            "3".to_string()
        );
    }

    #[test]
    fn test_new_unique_id() {
        // start
        assert_eq!(new_unique_id(vec!["1".to_string(), "z".to_string()]), "0".to_string());
        // middle
        assert_eq!(new_unique_id(vec!["0".to_string(), "2".to_string()]), "1".to_string());
        // end
        assert_eq!(new_unique_id(vec!["0".to_string(), "1".to_string()]), "2".to_string());
        // check sorting
        assert_eq!(new_unique_id(vec!["1".to_string(), "0".to_string()]), "2".to_string());
        // check deduplication
        assert_eq!(
            new_unique_id(vec!["0".to_string(), "0".to_string(), "1".to_string()]),
            "2".to_string()
        );
    }
}