bitsy_parser/
lib.rs

1use std::fmt::Display;
2use std::io::Cursor;
3
4use radix_fmt::radix_36;
5use loe::{process, Config, TransformMode};
6
7pub mod colour;
8pub mod dialogue;
9pub mod ending;
10pub mod error;
11pub mod exit;
12pub mod game;
13pub mod image;
14pub mod item;
15pub mod mock;
16pub mod palette;
17pub mod position;
18pub mod room;
19pub mod sprite;
20pub mod text;
21pub mod tile;
22pub mod variable;
23pub mod test_omnibus;
24
25pub use colour::Colour;
26pub use dialogue::Dialogue;
27pub use ending::Ending;
28pub use error::Error;
29pub use exit::*;
30pub use game::*;
31pub use image::Image;
32pub use item::Item;
33pub use palette::Palette;
34pub use position::Position;
35pub use room::Room;
36pub use sprite::Sprite;
37pub use text::*;
38pub use tile::Tile;
39pub use variable::Variable;
40
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct Instance {
43    position: Position,
44    id: String, // item / ending id
45}
46
47/// a Room can have many Exits in different positions,
48/// optionally with a transition and dialogue
49/// todo make a from_str() function for this
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct ExitInstance {
52    position: Position,
53    exit: Exit,
54    transition: Option<Transition>,
55    dialogue_id: Option<String>,
56}
57
58pub trait AnimationFrames {
59    fn to_string(&self) -> String;
60}
61
62impl AnimationFrames for Vec<Image> {
63    fn to_string(&self) -> String {
64        let mut string = String::new();
65        let last_frame = self.len() - 1;
66
67        for (i, frame) in self.iter().enumerate() {
68            string.push_str(&frame.to_string());
69
70            if i < last_frame {
71                string.push_str(&"\n>\n".to_string());
72            }
73        }
74
75        string
76    }
77}
78
79/// this doesn't work inside ToBase36 for some reason
80fn to_base36(int: u64) -> String {
81    format!("{}", radix_36(int))
82}
83
84pub trait ToBase36 {
85    fn to_base36(&self) -> String;
86}
87
88impl ToBase36 for u64 {
89    fn to_base36(&self) -> String {
90        to_base36(*self)
91    }
92}
93
94/// e.g. `\nNAME DLG_0`
95fn optional_data_line<T: Display>(label: &str, item: Option<T>) -> String {
96    if item.is_some() {
97        format!("\n{} {}", label, item.unwrap())
98    } else {
99        "".to_string()
100    }
101}
102
103fn transform_line_endings(input: String, mode: TransformMode) -> String {
104    let mut input = Cursor::new(input);
105    let mut output = Cursor::new(Vec::new());
106
107    process(&mut input, &mut output, Config::default().transform(mode)).unwrap();
108    String::from_utf8(output.into_inner()).unwrap()
109}
110
111fn segments_from_str(str: &str) -> Vec<String> {
112    // this is pretty weird but a dialogue can just have an empty line followed by a name
113    // however, on entering two empty lines, dialogue will be wrapped in triple quotation marks
114    // so, handle this here
115    let string = str.replace("\n\nNAME", "\n\"\"\"\n\"\"\"\nNAME");
116
117    let mut output:Vec<String> = Vec::new();
118    // are we inside `"""\n...\n"""`? if so, ignore empty lines
119    let mut inside_escaped_block = false;
120    let mut current_segment : Vec<String> = Vec::new();
121
122    for line in string.lines() {
123        if line == "\"\"\"" {
124            inside_escaped_block = ! inside_escaped_block;
125        }
126
127        if line == "" && !inside_escaped_block {
128            output.push(current_segment.join("\n"));
129            current_segment = Vec::new();
130        } else {
131            current_segment.push(line.to_string());
132        }
133    }
134
135    output.push(current_segment.join("\n"));
136
137    output
138}
139
140/// tries to use an existing ID - if it is already in use, generate a new one
141/// then return the ID (either original or new)
142/// todo refactor (unnecessary clones etc.)
143fn try_id(ids: &Vec<String>, id: &String) -> String {
144    let id = id.clone();
145    let ids = ids.clone();
146    if is_id_available(&ids, &id) {
147        id
148    } else {
149        new_unique_id(ids)
150    }
151}
152
153fn is_id_available(ids: &Vec<String>, id: &String) -> bool {
154    ! ids.contains(id)
155}
156
157/// e.g. pass all tile IDs into this to get a new non-conflicting tile ID
158fn new_unique_id(ids: Vec<String>) -> String {
159    let mut new_id: u64 = 0;
160
161    while ids.contains(&new_id.to_base36()) {
162        new_id += 1;
163    }
164
165    to_base36(new_id)
166}
167
168pub trait Quote {
169    fn quote(&self) -> String;
170}
171
172impl Quote for String {
173    fn quote(&self) -> String {
174        format!("\"\"\"\n{}\n\"\"\"", self)
175    }
176}
177
178pub trait Unquote {
179    fn unquote(&self) -> String;
180}
181
182impl Unquote for String {
183    fn unquote(&self) -> String {
184        self.trim_matches('\"').trim_matches('\n').to_string()
185    }
186}
187
188#[cfg(test)]
189mod test {
190    use crate::{ToBase36, optional_data_line, mock, segments_from_str, Quote, Unquote, new_unique_id, try_id};
191
192    #[test]
193    fn to_base36() {
194        assert_eq!((37 as u64).to_base36(), "11");
195    }
196
197    #[test]
198    fn test_optional_data_line() {
199        let output = optional_data_line("NAME", mock::item().name);
200        assert_eq!(output, "\nNAME door");
201    }
202
203    #[test]
204    fn string_to_segments() {
205        let output = segments_from_str(include_str!("./test-resources/segments"));
206
207        let expected = vec![
208            "\"\"\"\nthe first segment is a long bit of text\n\n\nit contains empty lines\n\n\"\"\"".to_string(),
209            "this is a new segment\nthis is still the second segment\nblah\nblah".to_string(),
210            "DLG SEGMENT_3\n\"\"\"\nthis is a short \"long\" bit of text\n\"\"\"".to_string(),
211            "this is the last segment".to_string(),
212        ];
213
214        assert_eq!(output, expected);
215    }
216
217    #[test]
218    fn quote() {
219        let output = "this is a string.\nIt has 2 lines".to_string().quote();
220        let expected = "\"\"\"\nthis is a string.\nIt has 2 lines\n\"\"\"";
221        assert_eq!(output, expected);
222    }
223
224    #[test]
225    fn unquote() {
226        let output = "\"\"\"\nwho the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off\n\"\"\"".to_string().unquote();
227        let expected = "who the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off";
228        assert_eq!(output, expected);
229    }
230
231    #[test]
232    fn test_try_id() {
233        // does a conflict generate a new ID?
234        assert_eq!(
235            try_id(&vec!["0".to_string(), "1".to_string()], &"1".to_string()),
236            "2"
237        );
238        // with no conflict, does the ID remain the same?
239        assert_eq!(
240            try_id(&vec!["0".to_string(), "1".to_string()], &"3".to_string()),
241            "3"
242        );
243    }
244
245    #[test]
246    fn test_new_unique_id() {
247        // start
248        assert_eq!(new_unique_id(vec!["1".to_string(), "z".to_string()]), "0".to_string());
249        // middle
250        assert_eq!(new_unique_id(vec!["0".to_string(), "2".to_string()]), "1".to_string());
251        // end
252        assert_eq!(new_unique_id(vec!["0".to_string(), "1".to_string()]), "2".to_string());
253        // check sorting
254        assert_eq!(new_unique_id(vec!["1".to_string(), "0".to_string()]), "2".to_string());
255        // check deduplication
256        assert_eq!(
257            new_unique_id(vec!["0".to_string(), "0".to_string(), "1".to_string()]),
258            "2".to_string()
259        );
260    }
261}