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 #[default]
12 None,
13 Wavy,
15 Shaky,
17 Rainbow,
19 Color(u8),
21}
22
23#[derive(Debug, Clone, PartialEq)]
24pub enum Tag {
25 Br,
27 Pg,
29 Eff(TextEffect),
31 End,
33 Say(Expr),
35 DrwT(ID),
37 DrwS(ID),
39 DrwI(ID),
41 Pal(ID),
43 Ava(ID),
45 Exit(ID, u8, u8),
47 Set(String, Expr),
49 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]; let mut word = &word[1..]; 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
241fn 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
269fn 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
289fn 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
327fn 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
346fn 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
361fn 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}