bitsy_script/
tokenizer.rs

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