1use crate::types::Range;
2
3use crate::lexer::{RsmlLexer, TOKEN_KIND_CONSTRUCT_DELIMITERS, Token, TokenKind};
4use crate::list::TokenKindList;
5use crate::range_from_span::RangeFromSpan;
6use crate::{node_token_matches, token_kind_list};
7
8mod advance;
9mod datatype;
10mod declaration;
11mod parse_error;
12mod rule;
13pub mod types;
14
15use parse_error::{ParseError, ParseErrorMessage};
16pub use types::*;
17
18#[macro_export]
19macro_rules! node_token_matches {
20 ($node:ident, Some($( $name:ident )|*)) => {
21 matches!($node, Some($crate::parser::types::Node { token: $crate::lexer::SpannedToken (_, $( $crate::lexer::Token::$name )|*, _), .. }))
22 };
23
24 ($node:ident, $( $name:ident )|*) => {
25 matches!($node, $crate::parser::types::Node { token: $crate::lexer::SpannedToken (_, $( $crate::lexer::Token::$name )|*, _), .. })
26 };
27
28 ($node:ident, Some($( $name:ident($( $args:pat ),*) )|*)) => {
29 matches!($node, Some($crate::parser::types::Node { token: $crate::lexer::SpannedToken(_, $( $crate::lexer::Token::$name($( $args ),*) )|*, _), .. }))
30 };
31
32 ($node:ident, $( $name:ident($( $args:pat ),*) )|*) => {
33 matches!($node, $crate::parser::types::Node { token: $crate::lexer::SpannedToken(_, $( $crate::lexer::Token::$name($( $args ),*) )|*, _), .. })
34 };
35}
36
37pub struct RsmlParser<'a> {
38 pub lexer: RsmlLexer<'a>,
39 pub(crate) last_token_end: usize,
40
41 pub ast: Vec<Construct<'a>>,
42 pub ast_errors: AstErrors,
43
44 pub did_advance: bool,
45
46 pub directives: Directives,
47 pub(crate) pending_node: Option<Node<'a>>,
48 pub(crate) directives_phase_done: bool,
49}
50
51impl<'a> RsmlParser<'a> {
52 pub fn new(lexer: RsmlLexer<'a>) -> ParsedRsml<'a> {
53 let mut parser = Self {
54 lexer,
55 last_token_end: 0,
56
57 ast: Vec::new(),
58 ast_errors: AstErrors::new(),
59
60 did_advance: false,
61
62 directives: Directives::default(),
63 pending_node: None,
64 directives_phase_done: false,
65 };
66
67 parser.parse_directives();
68
69 parser.parse_loop(|parser, mut node| {
70 node = parser.parse_macro(node).handle_construct(&mut parser.ast)?;
71 node = parser
72 .parse_macro_call(node)
73 .handle_construct(&mut parser.ast)?;
74
75 node = parser
76 .parse_derive(node)
77 .handle_construct(&mut parser.ast)?;
78
79 node = parser
80 .parse_priority(node)
81 .handle_construct(&mut parser.ast)?;
82
83 node = parser.parse_tween(node).handle_construct(&mut parser.ast)?;
84
85 node = parser
86 .parse_static_token_assignment(node)
87 .handle_construct(&mut parser.ast)?;
88
89 node = parser
90 .parse_token_assignment(node)
91 .handle_construct(&mut parser.ast)?;
92
93 node = parser
94 .parse_property_assignment_or_rule_scope(node)
95 .handle_construct(&mut parser.ast)?;
96 node = parser
97 .parse_rule_scope_selector_begin(node)
98 .handle_construct(&mut parser.ast)?;
99
100 node = parser.parse_none(node).handle_construct(&mut parser.ast)?;
101
102 Some(node)
103 });
104
105 ParsedRsml {
106 ast: parser.ast,
107 ast_errors: parser.ast_errors,
108 directives: parser.directives,
109 rope: parser.lexer.rope,
110 }
111 }
112
113 pub fn from_source(source: &'a str) -> ParsedRsml<'a> {
114 Self::new(RsmlLexer::new(source))
115 }
116
117 pub fn range_from_span(&self, span: (usize, usize)) -> Range {
118 Range::from_span(&self.lexer.rope, span)
119 }
120
121 fn parse_assignment(&mut self, node: Node<'a>) -> Parsed<'a> {
122 let middle_node =
123 match self.advance_until(token_kind_list![Equals], &TOKEN_KIND_CONSTRUCT_DELIMITERS) {
124 Some(Ok(node)) => node,
125 Some(Err(node)) => return Parsed(Some(node), None),
126 None => return Parsed(None, None),
127 };
128
129 let left_node = node;
130
131 let node = self.advance_without_flags();
132 self.did_advance = true;
133
134 let (node_status, body_nodes) = self.parse_datatype(node, TOKEN_KIND_CONSTRUCT_DELIMITERS);
135 let body_nodes = body_nodes.map(|x| Box::new(x));
136
137 let terminator = match node_status {
138 NodeStatus::Exists => match self.advance_until(
139 token_kind_list![SemiColon],
140 &TOKEN_KIND_CONSTRUCT_DELIMITERS,
141 ) {
142 Some(Ok(node)) => node,
143 Some(Err(node)) => {
144 return Parsed(
145 Some(node),
146 Some(Construct::Assignment {
147 left: left_node,
148 middle: Some(middle_node),
149 right: body_nodes,
150 terminator: None,
151 }),
152 );
153 }
154 None => {
155 return Parsed(
156 None,
157 Some(Construct::Assignment {
158 left: left_node,
159 middle: Some(middle_node),
160 right: body_nodes,
161 terminator: None,
162 }),
163 );
164 }
165 },
166
167 NodeStatus::Err(node) => {
168 if node_token_matches!(node, SemiColon) {
169 node
170 } else {
171 let construct = Construct::Assignment {
172 left: left_node,
173 middle: Some(middle_node),
174 right: body_nodes,
175 terminator: None,
176 };
177
178 self.ast_errors.push(
179 ParseError::MissingToken {
180 msg: Some(ParseErrorMessage::Expected(TokenKind::SemiColon.name())),
181 },
182 self.range_from_span(clamp_span_to_end(construct.end())),
183 );
184
185 return Parsed(Some(node), Some(construct));
186 }
187 }
188
189 NodeStatus::None => {
190 let construct = Construct::Assignment {
191 left: left_node,
192 middle: Some(middle_node),
193 right: body_nodes,
194 terminator: None,
195 };
196
197 self.ast_errors.push(
198 ParseError::MissingToken {
199 msg: Some(ParseErrorMessage::Expected(TokenKind::SemiColon.name())),
200 },
201 self.range_from_span(clamp_span_to_end(construct.end())),
202 );
203
204 return Parsed(None, Some(construct));
205 }
206 };
207
208 Parsed(
209 self.advance(),
210 Some(Construct::Assignment {
211 left: left_node,
212 middle: Some(middle_node),
213 right: body_nodes,
214 terminator: Some(terminator),
215 }),
216 )
217 }
218
219 pub(crate) fn parse_static_token_assignment(&mut self, node: Node<'a>) -> Parsed<'a> {
220 if !node_token_matches!(node, StaticTokenIdentifier(_)) {
221 return Parsed(Some(node), None);
222 };
223 self.parse_assignment(node)
224 }
225
226 pub(crate) fn parse_token_assignment(&mut self, node: Node<'a>) -> Parsed<'a> {
227 if !node_token_matches!(node, TokenIdentifier(_)) {
228 return Parsed(Some(node), None);
229 };
230 self.parse_assignment(node)
231 }
232
233 pub(crate) fn parse_none(&mut self, node: Node<'a>) -> Parsed<'a> {
234 if !node_token_matches!(node, None) {
235 return Parsed(Some(node), None);
236 };
237
238 Parsed(self.advance(), Some(Construct::None { node }))
239 }
240
241 pub(crate) fn parse_loop<F: Fn(&mut Self, Node<'a>) -> Option<Node<'a>>>(
242 &mut self,
243 routine: F,
244 ) -> Option<Node<'a>> {
245 let mut node = self.advance_without_flags().update_last_token_end(self)?;
246 let token = &node.token;
247
248 let mut error_span: Option<(usize, usize)> = if matches!(token.value(), Token::Error) {
249 Some((token.start(), token.end()))
250 } else {
251 None
252 };
253
254 loop {
255 let Some(next_node) = routine(self, node) else {
256 break;
257 };
258 node = next_node;
259
260 if self.did_advance {
261 self.did_advance = false;
262
263 if let Some((error_span_start, error_span_end)) = error_span {
264 self.ast_errors.push(
265 ParseError::UnexpectedTokens { msg: None },
266 self.range_from_span((error_span_start, error_span_end)),
267 );
268 }
269 } else {
270 let token = &node.token;
271
272 if let Some((error_span_start, _)) = error_span {
273 error_span = Some((error_span_start, token.end()))
274 } else {
275 error_span = Some((token.start(), token.end()))
276 }
277
278 let Some(next_node) = self.advance_without_flags().update_last_token_end(self)
279 else {
280 break;
281 };
282
283 node = next_node;
284 }
285 }
286
287 if let Some((error_span_start, error_span_end)) = error_span {
288 self.ast_errors.push(
289 ParseError::UnexpectedTokens { msg: None },
290 self.range_from_span((error_span_start, error_span_end)),
291 );
292 }
293
294 None
295 }
296
297 #[cfg(test)]
298 pub fn parse_source(source: &'a str) -> ParsedRsml<'a> {
299 let lexer = crate::lexer::RsmlLexer::new(source);
300 Self::new(lexer)
301 }
302
303 pub(crate) fn parse_loop_inner<F: FnMut(&mut Self, Node<'a>) -> Option<(Node<'a>, bool)>>(
304 &mut self,
305 mut node: Node<'a>,
306 mut routine: F,
307 ) -> (Option<Node<'a>>, ParseEndedReason) {
308 let last_did_advance = self.did_advance;
309 self.did_advance = false;
310
311 let mut error_span: Option<(usize, usize)> = None;
312
313 loop {
314 let Some(parsed) = routine(self, node) else {
315 self.did_advance = last_did_advance;
316 return (None, ParseEndedReason::Eof);
317 };
318 node = parsed.0;
319
320 if self.did_advance {
321 self.did_advance = false;
322
323 if let Some((error_span_start, error_span_end)) = error_span {
324 self.ast_errors.push(
325 ParseError::UnexpectedTokens { msg: None },
326 self.range_from_span((error_span_start, error_span_end)),
327 );
328 }
329
330 if parsed.1 {
331 return (Some(node), ParseEndedReason::Manual);
332 }
333 } else {
334 if parsed.1 {
335 if let Some((error_span_start, error_span_end)) = error_span {
336 self.ast_errors.push(
337 ParseError::UnexpectedTokens { msg: None },
338 self.range_from_span((error_span_start, error_span_end)),
339 );
340 }
341
342 return (Some(node), ParseEndedReason::Manual);
343 }
344
345 let token = &node.token;
346 if let Some((error_span_start, _)) = error_span {
347 error_span = Some((error_span_start, token.end()))
348 } else {
349 error_span = Some((token.start(), token.end()))
350 }
351
352 let Some(next_node) = self.advance_without_flags().update_last_token_end(self)
353 else {
354 break;
355 };
356 node = next_node;
357 }
358 }
359
360 if let Some((error_span_start, error_span_end)) = error_span {
361 self.ast_errors.push(
362 ParseError::UnexpectedTokens { msg: None },
363 self.range_from_span((error_span_start, error_span_end)),
364 );
365 }
366
367 self.did_advance = last_did_advance;
368
369 (Some(node), ParseEndedReason::Eof)
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use crate::parser::*;
376 use crate::compiler::RsmlCompiler;
377
378 macro_rules! parser_test {
379 ($name:ident, $source:expr) => {
380 #[test]
381 fn $name() {
382 let parsed = RsmlParser::parse_source($source);
383 insta::assert_debug_snapshot!(parsed.ast);
384 }
385
386 paste::paste! {
387 #[test]
388 fn [<compiler_ $name>]() {
389 let parsed = RsmlParser::parse_source($source);
390 let compiled = RsmlCompiler::new(parsed);
391 insta::assert_debug_snapshot!(compiled);
392 }
393 }
394 };
395 }
396
397 #[test]
398 fn unary_minus_in_udim2_expression() {
399 let source = r#"$Size = udim2(-20px + 100%, -20px + 100%);"#;
400 let parsed = RsmlParser::parse_source(source);
401
402 assert!(parsed.ast_errors.0.is_empty(), "Expected no parse errors, got: {:?}", parsed.ast_errors.0);
403 insta::assert_debug_snapshot!(parsed.ast);
404 }
405
406 parser_test!(derive_string, r#"@derive "some-module";"#);
407 parser_test!(derive_missing_semicolon, r#"@derive "module""#);
408 parser_test!(derive_missing_body, r#"@derive"#);
409 parser_test!(priority_number, r#"@priority 10;"#);
410 parser_test!(priority_missing_semicolon, r#"@priority 10"#);
411 parser_test!(tween_simple, r#"@tween MyTween 0.5;"#);
412 parser_test!(tween_string_value, r#"@tween Slide "ease-in";"#);
413 parser_test!(tween_missing_name, r#"@tween ;"#);
414 parser_test!(tween_missing_semicolon, r#"@tween MyTween 0.5"#);
415
416 parser_test!(assign_property_string, r#"Text = "hello";"#);
417 parser_test!(assign_property_number, r#"Size = 42;"#);
418 parser_test!(assign_property_boolean, r#"Visible = true;"#);
419 parser_test!(assign_property_nil, r#"Parent = nil;"#);
420 parser_test!(assign_property_missing_value, r#"Text = ;"#);
421 parser_test!(assign_property_missing_semicolon, r#"Text = "hello""#);
422 parser_test!(assign_token, r#"$Size = 100;"#);
423 parser_test!(assign_token_annotated_table, r#"$Size = udim2(1, 0, 1, 0);"#);
424 parser_test!(assign_token_missing_semicolon, r#"$Size = 100"#);
425 parser_test!(assign_static_token, r#"$!Padding = 10px;"#);
426 parser_test!(assign_static_token_missing_value, r#"$!Padding = ;"#);
427
428 parser_test!(value_number_scale, r#"Size = 100%;"#);
429 parser_test!(value_number_offset, r#"Size = 20px;"#);
430 parser_test!(value_string_double, r#"Text = "hello world";"#);
431 parser_test!(value_string_single, r#"Text = 'hello world';"#);
432 parser_test!(value_string_multi, r#"Text = [[multi line]];"#);
433 parser_test!(value_color_hex, r#"Color = #ff00ff;"#);
434 parser_test!(value_color_tailwind, r#"Color = tw:red:500;"#);
435 parser_test!(value_color_css, r#"Color = css:tomato;"#);
436 parser_test!(value_color_brick, r#"Color = bc:red;"#);
437 parser_test!(value_rbx_asset, r#"Image = rbxassetid://12345;"#);
438 parser_test!(value_rbx_content, r#"Image = contentid://12345;"#);
439 parser_test!(value_enum, r#"SortOrder = Enum.SortOrder.LayoutOrder;"#);
440 parser_test!(value_enum_missing_variant, r#"SortOrder = Enum.SortOrder;"#);
441
442 parser_test!(annotated_table_udim2, r#"$Size = udim2(1, 0, 1, 0);"#);
443 parser_test!(annotated_table_no_args, r#"$Val = empty();"#);
444 parser_test!(annotated_table_nested, r#"$Val = outer(inner(1, 2));"#);
445 parser_test!(annotated_table_missing_close, r#"$Val = udim2(1, 0;"#);
446 parser_test!(table_bare, r#"$Val = (1, 2, 3);"#);
447 parser_test!(table_empty, r#"$Val = ();"#);
448 parser_test!(table_nested, r#"$Val = ((1, 2), (3, 4));"#);
449 parser_test!(table_missing_close, r#"$Val = (1, 2"#);
450
451 parser_test!(math_add, r#"$Val = 10 + 20;"#);
452 parser_test!(math_sub, r#"$Val = 10 - 5;"#);
453 parser_test!(math_mult, r#"$Val = 10 * 5;"#);
454 parser_test!(math_div, r#"$Val = 10 / 5;"#);
455 parser_test!(math_floor_div, r#"$Val = 10 // 3;"#);
456 parser_test!(math_mod, r#"$Val = 10 % 3;"#);
457 parser_test!(math_pow, r#"$Val = 2 ^ 8;"#);
458 parser_test!(math_precedence_add_mult, r#"$Val = 1 + 2 * 3;"#);
459 parser_test!(math_precedence_mult_add, r#"$Val = 1 * 2 + 3;"#);
460 parser_test!(math_chained_add_sub, r#"$Val = 1 + 2 - 3 + 4;"#);
461 parser_test!(unary_minus_simple, r#"$Val = -10;"#);
462 parser_test!(unary_minus_in_expression, r#"$Val = -10 + 20;"#);
463 parser_test!(math_udim_mixed, r#"$Size = 50% + 10px;"#);
464 parser_test!(math_missing_right_operand, r#"$Val = 10 +;"#);
465
466 parser_test!(rule_identifier, r#"Frame { }"#);
467 parser_test!(rule_name_selector, r#"#MyFrame { }"#);
468 parser_test!(rule_tag_selector, r#".tagged { }"#);
469 parser_test!(rule_state_selector, r#":hover { }"#);
470 parser_test!(rule_pseudo_selector, r#"::UIPadding { }"#);
471 parser_test!(rule_children_selector, r#"Frame > TextLabel { }"#);
472 parser_test!(rule_descendants_selector, r#"Frame >> TextLabel { }"#);
473 parser_test!(rule_comma_selectors, r#"Frame, TextLabel { }"#);
474 parser_test!(rule_comma_three, r#"Frame, TextLabel, ImageLabel { }"#);
475 parser_test!(rule_compound, r#"Frame .tag :hover { }"#);
476 parser_test!(rule_with_assignment, r#"Frame { Size = 100; }"#);
477 parser_test!(rule_with_multiple_assignments, "Frame {\n Size = 100;\n Visible = true;\n}");
478 parser_test!(rule_nested, r#"Frame { TextLabel { Text = "hi"; } }"#);
479 parser_test!(rule_deeply_nested, r#".tag { :hover { Color = #f00; } }"#);
480 parser_test!(rule_missing_close_brace, r#"Frame { Size = 100;"#);
481 parser_test!(rule_children_missing_part, r#"Frame > { }"#);
482 parser_test!(rule_macro_call_selector, r#"sel!(10px) { Size = 100; }"#);
483 parser_test!(rule_macro_call_comma, r#"sel!(1), Frame { }"#);
484
485 parser_test!(macro_construct_return, r#"@macro MyMacro -> Construct { Size = 100; }"#);
486 parser_test!(macro_args_construct_return, r#"@macro MyMacro(&v) -> Construct { Size = &v; }"#);
487 parser_test!(macro_datatype_return, r#"@macro MyColor -> Datatype { #ff0000 }"#);
488 parser_test!(macro_datatype_return_args, r#"@macro Scale(&x) -> Datatype { &x }"#);
489 parser_test!(macro_selector_return, r#"@macro MySel -> Selector { Frame .tag }"#);
490 parser_test!(macro_selector_return_args, r#"@macro MySel(&c) -> Selector { Frame .tag :hover }"#);
491 parser_test!(macro_nested_rule, r#"@macro Theme(&c) -> Construct { Frame { Color = &c; } }"#);
492 parser_test!(macro_empty_body, r#"@macro MyMacro -> Construct { }"#);
493 parser_test!(macro_missing_name, r#"@macro { }"#);
494 parser_test!(macro_missing_body, r#"@macro MyMacro"#);
495 parser_test!(macro_missing_close_brace, r#"@macro M -> Construct { Size = 1;"#);
496 parser_test!(macro_invalid_return_type, r#"@macro M -> Invalid { }"#);
497 parser_test!(macro_args_missing_comma, r#"@macro M(&a &b) -> Construct { }"#);
498
499 parser_test!(macro_call_no_args, r#"MyMacro!();"#);
500 parser_test!(macro_call_with_args, r#"MyMacro!(10px, 20px);"#);
501 parser_test!(macro_call_complex_args, r#"Apply!(#ff0000, 10px, "hello");"#);
502 parser_test!(macro_call_missing_semicolon, r#"MyMacro!()"#);
503 parser_test!(macro_call_missing_close_paren, r#"MyMacro!(10px;"#);
504
505 parser_test!(builtin_padding_one_arg, r#"Frame { Padding!(10px); }"#);
506 parser_test!(builtin_padding_two_args, r#"Frame { Padding!(10px, 20px); }"#);
507 parser_test!(builtin_padding_three_args, r#"Frame { Padding!(10px, 20px, 30px); }"#);
508 parser_test!(builtin_padding_four_args, r#"Frame { Padding!(10px, 20px, 30px, 40px); }"#);
509 parser_test!(builtin_corner_radius, r#"Frame { CornerRadius!(8px); }"#);
510 parser_test!(builtin_scale, r#"Frame { Scale!(1.5); }"#);
511 parser_test!(macro_call_math_arg, r#"Frame { Padding!(0% + .5); }"#);
512
513 parser_test!(comment_before_assign, "-- comment\nSize = 100;");
514 parser_test!(comment_multi_before_assign, r#"--[[comment]] Size = 100;"#);
515 parser_test!(comment_multi_nested, r#"--[==[comment]==] Size = 100;"#);
516 parser_test!(comment_leading_trivia, "-- a\n-- b\nSize = 100;");
517
518 parser_test!(directive_nobuiltins_alone, "--!nobuiltins");
519 parser_test!(directive_nobuiltins_then_code, "--!nobuiltins\nSize = 100;");
520 parser_test!(directive_nobuiltins_blocks_builtin_expansion, "--!nobuiltins\nFrame { Padding!(10px); }");
521 parser_test!(directive_strict_alone, "--!strict");
522 parser_test!(directive_nonstrict_alone, "--!nonstrict");
523 parser_test!(directive_after_comment, "-- preface\n--!nobuiltins\nSize = 100;");
524 parser_test!(directive_unknown, "--!foo\nSize = 100;");
525 parser_test!(directive_empty, "--!\nSize = 100;");
526 parser_test!(directive_after_code, "Size = 1;\n--!nobuiltins\nSize = 2;");
527
528 #[test]
529 fn directive_sets_nobuiltins_flag() {
530 let parsed = RsmlParser::parse_source("--!nobuiltins\nSize = 100;");
531 assert!(parsed.directives.nobuiltins);
532 }
533
534 #[test]
535 fn no_directive_leaves_flag_unset() {
536 let parsed = RsmlParser::parse_source("Size = 100;");
537 assert!(!parsed.directives.nobuiltins);
538 }
539
540 #[test]
541 fn directive_sets_strict_language_mode() {
542 use crate::types::LanguageMode;
543 let parsed = RsmlParser::parse_source("--!strict\nSize = 100;");
544 assert_eq!(parsed.directives.language_mode, Some(LanguageMode::Strict));
545 }
546
547 #[test]
548 fn directive_sets_nonstrict_language_mode() {
549 use crate::types::LanguageMode;
550 let parsed = RsmlParser::parse_source("--!nonstrict\nSize = 100;");
551 assert_eq!(parsed.directives.language_mode, Some(LanguageMode::Nonstrict));
552 }
553
554 #[test]
555 fn no_directive_leaves_language_mode_unset() {
556 let parsed = RsmlParser::parse_source("Size = 100;");
557 assert_eq!(parsed.directives.language_mode, None);
558 }
559
560 #[test]
561 fn directive_after_code_emits_error() {
562 let parsed = RsmlParser::parse_source("Size = 1;\n--!nobuiltins");
563 assert!(!parsed.directives.nobuiltins);
564 assert!(parsed.ast_errors.0.iter().any(|d| d.code == "DIRECTIVE_NOT_AT_TOP"));
565 }
566
567 #[test]
568 fn unknown_directive_emits_error() {
569 let parsed = RsmlParser::parse_source("--!foo\nSize = 1;");
570 assert!(parsed.ast_errors.0.iter().any(|d| d.code == "UNKNOWN_DIRECTIVE"));
571 }
572
573 #[test]
574 fn empty_directive_emits_error() {
575 let parsed = RsmlParser::parse_source("--!\nSize = 1;");
576 assert!(parsed.ast_errors.0.iter().any(|d| d.code == "EMPTY_DIRECTIVE"));
577 }
578
579 parser_test!(query_selector, r#"@media { }"#);
580 parser_test!(query_selector_unknown, r#"@foobar { }"#);
581
582 parser_test!(empty_source, r#""#);
583 parser_test!(multiple_top_level, "@priority 5;\nFrame { Size = 100; }");
584 parser_test!(full_stylesheet, r#"
585@derive "base";
586@priority 5;
587@tween Fade 0.3;
588
589$!PrimaryColor = #3498db;
590$Padding = 10px;
591
592@macro Highlight(&color) -> Construct {
593 BackgroundColor3 = &color;
594}
595
596Frame {
597 BackgroundColor3 = $!PrimaryColor;
598 Size = udim2(1, -$Padding * 2, 1, -$Padding * 2);
599
600 TextLabel {
601 Text = "Hello";
602 TextColor3 = css:white;
603 }
604
605 :hover {
606 BackgroundColor3 = tw:blue:600;
607 }
608}
609
610#Sidebar, .panel {
611 Size = udim2(0, 200px, 1, 0);
612}
613"#);
614 parser_test!(macro_def_and_call, "@macro P(&v) -> Construct { $!P = &v; }\nP!(10px);");
615 parser_test!(
616 macro_user_nested_expansion,
617 "@macro Inner(&v) -> Construct { ::UIPadding { PaddingTop = &v; } }\n@macro Outer(&v) -> Construct { Inner!(&v); }\nFrame { Outer!(10px); }"
618 );
619 parser_test!(
620 macro_recursion_guard,
621 "@macro Recur() -> Construct { Recur!(); }\nFrame { Recur!(); }"
622 );
623 parser_test!(
624 macro_overload_cross_call_not_blocked,
625 "@macro Foo() -> Construct { Foo!(10px); }\n@macro Foo(&v) -> Construct { ::Inner { X = &v; } }\nFrame { Foo!(); }"
626 );
627 parser_test!(
628 macro_overload_by_arg_count,
629 "@macro Set(&a) -> Construct { ::Inner { X = &a; } }\n@macro Set(&a, &b) -> Construct { ::Inner { Y = &b; } }\nFrame { Set!(1px); Set!(2px, 3px); }"
630 );
631 parser_test!(
632 macro_selector_expansion,
633 "@macro Foo -> Selector { TextButton }\nFoo!(), Frame { }"
634 );
635 parser_test!(
636 macro_selector_overload,
637 "@macro Sel -> Selector { A }\n@macro Sel(&x) -> Selector { B }\nSel!() { }\nSel!(1) { }"
638 );
639 parser_test!(
640 macro_selector_recursion_guard,
641 "@macro Loop -> Selector { Loop!() }\nLoop!() { }"
642 );
643 parser_test!(
644 macro_selector_overload_cross_call_not_blocked,
645 "@macro Sel -> Selector { Sel!(1) }\n@macro Sel(&x) -> Selector { TextButton }\nSel!() { }"
646 );
647 parser_test!(
648 macro_selector_recursion_inline_comma,
649 "@macro Foo -> Selector { TextButton, Foo!() }\nFoo!(), Frame { }"
650 );
651 parser_test!(
652 macro_selector_undefined_dropped,
653 "Missing!(), Frame { }"
654 );
655 parser_test!(
656 macro_indirect_recursion_typechecker_error,
657 "@macro A() -> Construct { B!(); }\n@macro B() -> Construct { A!(); }\nFrame { A!(); }"
658 );
659}