bitsy_script/
tokenizer.rs

1use crate::Val;
2use alloc::string::String;
3use alloc::string::ToString;
4use alloc::vec::Vec;
5use core::str::Chars;
6
7pub type ID = String;
8
9#[derive(Debug, Default, Copy, Clone, PartialEq)]
10pub enum TextEffect {
11    /// No effects.
12    #[default]
13    None,
14    /// {wvy} text in tags waves up and down.
15    Wavy,
16    /// {shk} text in tags shakes constantly.
17    Shaky,
18    /// {rbw} text in tags is rainbow colored.
19    Rainbow,
20    /// {clr} use a palette color for dialog text.
21    Color(u8),
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum Tag {
26    /// Line break.
27    Br,
28    /// Page break.
29    Pg,
30    /// Apply style effect to text.
31    Eff(TextEffect),
32    /// End the game.
33    End,
34    /// Print the result of expression.
35    Say(Expr),
36    /// Draw tile.
37    DrwT(ID),
38    /// Draw sprite.
39    DrwS(ID),
40    /// Draw item.
41    DrwI(ID),
42    /// Change room's current palette.
43    Pal(ID),
44    /// Make avatar look like the given sprite.
45    Ava(ID),
46    /// Move player to the given room.
47    Exit(ID, u8, u8),
48    /// Evaluate the expression and assign its result to the variable.
49    Set(String, Expr),
50    /// Unsupported tag.
51    Unknown(String, String),
52}
53
54#[derive(Debug, Clone, PartialEq)]
55pub enum Expr {
56    SimpleExpr(SimpleExpr),
57    BinOp(BinOp, SimpleExpr, SimpleExpr),
58}
59
60#[derive(Debug, Copy, Clone, PartialEq, Eq)]
61pub enum BinOp {
62    Mul,
63    Div,
64    Add,
65    Sub,
66    Lt,
67    Gt,
68    Lte,
69    Gte,
70    Eq,
71}
72
73#[derive(Debug, Clone, PartialEq)]
74pub enum SimpleExpr {
75    Var(String),
76    Item(String),
77    Val(Val),
78}
79
80#[derive(Debug, Clone, PartialEq)]
81pub enum Token {
82    OpenTag(Tag),
83    CloseTag(Tag),
84    Word(String),
85}
86
87pub struct Tokenizer<'a> {
88    buffer: Chars<'a>,
89    stash: Option<char>,
90}
91
92impl<'a> Tokenizer<'a> {
93    pub fn new(text: &'a str) -> Self {
94        Self {
95            buffer: text.chars(),
96            stash: None,
97        }
98    }
99}
100
101impl<'a> Iterator for Tokenizer<'a> {
102    type Item = Token;
103
104    fn next(&mut self) -> Option<Self::Item> {
105        let mut word = String::new();
106        let mut found_letter = false;
107        let mut open_tags: u8 = 0;
108        loop {
109            let ch = if let Some(stash) = self.stash.take() {
110                stash
111            } else if let Some(ch) = self.buffer.next() {
112                ch
113            } else {
114                break;
115            };
116            word.push(ch);
117            match ch {
118                '\n' => return Some(Token::OpenTag(Tag::Br)),
119                '{' => {
120                    if open_tags == 0 && found_letter {
121                        self.stash = Some('{');
122                        word.pop();
123                        break;
124                    }
125                    open_tags += 1;
126                }
127                '}' => {
128                    if open_tags != 0 {
129                        open_tags -= 1;
130                        if open_tags == 0 {
131                            return Some(parse_tag(&word));
132                        } else {
133                            found_letter = true
134                        }
135                    }
136                }
137                '\t' | '\x0C' | '\r' | ' ' => {
138                    if open_tags == 0 && found_letter {
139                        break;
140                    }
141                }
142                _ => found_letter = true,
143            }
144        }
145        if word.is_empty() {
146            return None;
147        }
148        Some(Token::Word(word))
149    }
150}
151
152fn parse_tag(word: &str) -> Token {
153    let word = &word[..word.len() - 1]; // remove "}" from the end.
154    let mut word = &word[1..]; // remove "{" from the beginning.
155    word = word.trim_ascii();
156    let is_closing = word.starts_with('/');
157    if is_closing {
158        word = &word[1..];
159        word = word.trim_ascii();
160    }
161    let tag = parse_tag_value(word);
162    if is_closing {
163        Token::CloseTag(tag)
164    } else {
165        Token::OpenTag(tag)
166    }
167}
168
169fn parse_tag_value(word: &str) -> Tag {
170    let (name, args) = word.split_once(' ').unwrap_or((word, ""));
171    let args = args.trim_ascii();
172    if args.is_empty() {
173        parse_tag_without_args(name)
174    } else {
175        parse_tag_with_args(name, args)
176    }
177}
178
179fn parse_tag_with_args(name: &str, args: &str) -> Tag {
180    if args.starts_with('=') {
181        return parse_assign(name, args);
182    }
183    match name {
184        "clr" => match args {
185            "0" => Tag::Eff(TextEffect::Color(1)),
186            "1" => Tag::Eff(TextEffect::Color(2)),
187            "2" => Tag::Eff(TextEffect::Color(3)),
188            _ => Tag::Eff(TextEffect::Color(1)),
189        },
190        "say" | "print" => Tag::Say(parse_expr(args)),
191        "drwt" | "printTile" => Tag::DrwT(unquote(args).to_string()),
192        "drws" | "printSprite" => Tag::DrwS(unquote(args).to_string()),
193        "drwi" | "printItem" => Tag::DrwI(unquote(args).to_string()),
194        "ava" => Tag::Ava(unquote(args).to_string()),
195        "pal" => Tag::Pal(unquote(args).to_string()),
196        "exit" => {
197            let (room, x, y) = parse_exit_args(args);
198            let room = room.to_string();
199            Tag::Exit(room, x, y)
200        }
201        _ => Tag::Unknown(name.to_string(), args.to_string()),
202    }
203}
204
205fn parse_tag_without_args(name: &str) -> Tag {
206    match name {
207        "br" => Tag::Br,
208        "pg" => Tag::Pg,
209        "clr1" => Tag::Eff(TextEffect::Color(1)),
210        "clr2" => Tag::Eff(TextEffect::Color(2)),
211        "clr3" => Tag::Eff(TextEffect::Color(3)),
212        "wvy" => Tag::Eff(TextEffect::Wavy),
213        "shk" => Tag::Eff(TextEffect::Shaky),
214        "rbw" => Tag::Eff(TextEffect::Rainbow),
215        "end" => Tag::End,
216        _ => Tag::Unknown(name.to_string(), "".to_string()),
217    }
218}
219
220fn parse_assign(name: &str, args: &str) -> Tag {
221    let args = &args[1..];
222    let expr = parse_expr(args);
223    Tag::Set(name.to_string(), expr)
224}
225
226fn parse_expr(args: &str) -> Expr {
227    let args = args.trim_ascii();
228    let args = args.replace("item ", "item@");
229    let parts: Vec<_> = args.split_ascii_whitespace().collect();
230    if let Some(part) = unwrap_vec_1(&parts) {
231        Expr::SimpleExpr(parse_simple_expr(part))
232    } else if let Some((left, op, right)) = unwrap_vec_3(&parts) {
233        let left = parse_simple_expr(left);
234        let right = parse_simple_expr(right);
235        let op = op.trim_ascii();
236        let op = match op {
237            "*" => BinOp::Mul,
238            "/" => BinOp::Div,
239            "+" => BinOp::Add,
240            "-" => BinOp::Sub,
241            "<" => BinOp::Lt,
242            ">" => BinOp::Gt,
243            "<=" => BinOp::Lte,
244            ">=" => BinOp::Gte,
245            "==" => BinOp::Eq,
246            _ => {
247                let val = Val::S(args.to_string());
248                return Expr::SimpleExpr(SimpleExpr::Val(val));
249            }
250        };
251        Expr::BinOp(op, left, right)
252    } else {
253        let val = Val::S(args.to_string());
254        Expr::SimpleExpr(SimpleExpr::Val(val))
255    }
256}
257
258fn parse_simple_expr(part: &str) -> SimpleExpr {
259    let part = part.trim_ascii();
260    if let Some(name) = part.strip_prefix("{item@") {
261        let name = name.strip_suffix('}').unwrap_or(name);
262        let name = name.trim_ascii();
263        let name = unquote(name);
264        return SimpleExpr::Item(name.to_string());
265    }
266    if part == "true" {
267        return SimpleExpr::Val(Val::I(1));
268    }
269    if part == "false" {
270        return SimpleExpr::Val(Val::I(0));
271    }
272    if let Ok(i) = part.parse::<i16>() {
273        return SimpleExpr::Val(Val::I(i));
274    }
275    if let Ok(f) = part.parse::<f32>() {
276        return SimpleExpr::Val(Val::F(f));
277    }
278    if part.starts_with('"') {
279        return SimpleExpr::Val(Val::S(unquote(part).to_string()));
280    }
281    if is_var(part) {
282        return SimpleExpr::Var(part.to_string());
283    }
284    SimpleExpr::Val(Val::S(part.to_string()))
285}
286
287fn is_var(part: &str) -> bool {
288    let mut first = true;
289    for ch in part.chars() {
290        if !first && ch.is_ascii_digit() {
291            return false;
292        }
293        first = false;
294        if ch.is_ascii_alphanumeric() {
295            continue;
296        }
297        if ch == '_' {
298            continue;
299        }
300        return false;
301    }
302    true
303}
304
305fn parse_exit_args(args: &str) -> (&str, u8, u8) {
306    let args = unquote(args);
307    let (room, args) = args.split_once(',').unwrap_or((args, "0,0"));
308    let room = unquote(room);
309    let (x, y) = args.split_once(',').unwrap_or(("0", "0"));
310    let x = x.trim_ascii();
311    let y = y.trim_ascii();
312    let x: u8 = x.parse().unwrap_or_default();
313    let y: u8 = y.parse().unwrap_or_default();
314    (room, x, y)
315}
316
317fn unquote(v: &str) -> &str {
318    let n_quotes = v.chars().filter(|ch| *ch == '"').count();
319    if n_quotes != 2 {
320        return v;
321    }
322    if v.starts_with('"') && v.ends_with('"') {
323        let v = &v[1..];
324        &v[..v.len() - 1]
325    } else {
326        v
327    }
328}
329
330fn unwrap_vec_1<'a>(items: &[&'a str]) -> Option<&'a str> {
331    if items.len() != 1 {
332        return None;
333    }
334    Some(items[0])
335}
336
337fn unwrap_vec_3<'a>(items: &[&'a str]) -> Option<(&'a str, &'a str, &'a str)> {
338    if items.len() != 3 {
339        return None;
340    }
341    Some((items[0], items[1], items[2]))
342}