minecraft-datapack-language 15.4.28__py3-none-any.whl → 15.4.30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. minecraft_datapack_language/__init__.py +23 -2
  2. minecraft_datapack_language/_version.py +2 -2
  3. minecraft_datapack_language/ast_nodes.py +87 -59
  4. minecraft_datapack_language/cli.py +276 -139
  5. minecraft_datapack_language/mdl_compiler.py +470 -0
  6. minecraft_datapack_language/mdl_errors.py +14 -0
  7. minecraft_datapack_language/mdl_lexer.py +624 -0
  8. minecraft_datapack_language/mdl_parser.py +573 -0
  9. minecraft_datapack_language-15.4.30.dist-info/METADATA +266 -0
  10. minecraft_datapack_language-15.4.30.dist-info/RECORD +17 -0
  11. minecraft_datapack_language/cli_build.py +0 -1292
  12. minecraft_datapack_language/cli_check.py +0 -155
  13. minecraft_datapack_language/cli_colors.py +0 -264
  14. minecraft_datapack_language/cli_help.py +0 -508
  15. minecraft_datapack_language/cli_new.py +0 -300
  16. minecraft_datapack_language/cli_utils.py +0 -276
  17. minecraft_datapack_language/expression_processor.py +0 -352
  18. minecraft_datapack_language/linter.py +0 -409
  19. minecraft_datapack_language/mdl_lexer_js.py +0 -754
  20. minecraft_datapack_language/mdl_parser_js.py +0 -1049
  21. minecraft_datapack_language/pack.py +0 -758
  22. minecraft_datapack_language-15.4.28.dist-info/METADATA +0 -1274
  23. minecraft_datapack_language-15.4.28.dist-info/RECORD +0 -25
  24. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/WHEEL +0 -0
  25. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/entry_points.txt +0 -0
  26. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/licenses/LICENSE +0 -0
  27. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/top_level.txt +0 -0
@@ -1,1049 +0,0 @@
1
- """
2
- MDL Parser - Simplified JavaScript-style syntax with curly braces and semicolons
3
- Handles basic control structures and number variables only
4
- """
5
-
6
- from typing import List, Optional, Dict, Any, Union
7
- from .mdl_lexer_js import Token, TokenType, lex_mdl_js
8
- from .mdl_errors import MDLParserError, create_parser_error, MDLLexerError
9
- from .ast_nodes import (
10
- ASTNode, PackDeclaration, NamespaceDeclaration, FunctionDeclaration,
11
- VariableDeclaration, VariableAssignment, IfStatement, WhileLoop,
12
- FunctionCall, ExecuteStatement, RawText, Command, VariableExpression,
13
- VariableSubstitutionExpression, LiteralExpression, BinaryExpression,
14
- HookDeclaration, TagDeclaration, RecipeDeclaration, LootTableDeclaration,
15
- AdvancementDeclaration, PredicateDeclaration, ItemModifierDeclaration,
16
- StructureDeclaration
17
- )
18
-
19
-
20
- class MDLParser:
21
- """Parser for simplified MDL language."""
22
-
23
- def __init__(self, tokens: List[Token], source_file: str = None):
24
- self.tokens = tokens
25
- self.current = 0
26
- self.current_namespace = "mdl" # Track current namespace
27
- self.source_file = source_file
28
-
29
- def parse(self) -> Dict[str, Any]:
30
- """Parse tokens into AST."""
31
- ast = {
32
- 'pack': None,
33
- 'namespace': None,
34
- 'functions': [],
35
- 'hooks': [],
36
- 'tags': [],
37
- 'imports': [],
38
- 'exports': [],
39
- 'variables': [], # Add support for top-level variable declarations
40
- 'recipes': [],
41
- 'loot_tables': [],
42
- 'advancements': [],
43
- 'predicates': [],
44
- 'item_modifiers': [],
45
- 'structures': []
46
- }
47
-
48
- while not self._is_at_end():
49
- try:
50
- if self._peek().type == TokenType.PACK:
51
- ast['pack'] = self._parse_pack_declaration()
52
- elif self._peek().type == TokenType.NAMESPACE:
53
- namespace_decl = self._parse_namespace_declaration()
54
- # Store namespace in both places for compatibility
55
- ast['namespace'] = namespace_decl
56
- # Also collect all namespaces in a list
57
- if 'namespaces' not in ast:
58
- ast['namespaces'] = []
59
- ast['namespaces'].append(namespace_decl)
60
- self.current_namespace = namespace_decl['name'] # Update current namespace
61
- print(f"DEBUG: Parser updated current_namespace to: {self.current_namespace}")
62
- elif self._peek().type == TokenType.FUNCTION:
63
- # Check if this is a function call or declaration
64
- # Look ahead to see if there's a semicolon or brace
65
- function_token = self._peek()
66
- self._advance() # Consume 'function'
67
-
68
- # Look ahead to see what comes after the function name
69
- name_token = self._peek()
70
- if name_token.type != TokenType.STRING:
71
- raise create_parser_error(
72
- message="Expected function name after 'function'",
73
- file_path=self.source_file,
74
- line=name_token.line,
75
- column=name_token.column,
76
- line_content=name_token.value,
77
- suggestion="Provide a function name in quotes"
78
- )
79
-
80
- # Look ahead to see if there's a semicolon (function call) or brace (function declaration)
81
- self._advance() # Consume function name
82
- next_token = self._peek()
83
-
84
- if next_token.type == TokenType.SEMICOLON:
85
- # This is a function call
86
- self._advance() # Consume semicolon
87
- name = name_token.value.strip('"').strip("'")
88
- ast['functions'].append({"type": "function_call", "name": name})
89
- else:
90
- # This is a function declaration - reset and parse properly
91
- self.current -= 2 # Go back to 'function' token
92
- ast['functions'].append(self._parse_function_declaration())
93
- elif self._peek().type == TokenType.ON_LOAD:
94
- ast['hooks'].append(self._parse_hook_declaration())
95
- elif self._peek().type == TokenType.ON_TICK:
96
- ast['hooks'].append(self._parse_hook_declaration())
97
- elif self._peek().type == TokenType.TAG:
98
- ast['tags'].append(self._parse_tag_declaration())
99
- elif self._peek().type == TokenType.VAR:
100
- # Handle top-level variable declarations
101
- ast['variables'].append(self._parse_variable_declaration())
102
- elif self._peek().type == TokenType.RECIPE:
103
- print(f"DEBUG: Found RECIPE token, current_namespace: {self.current_namespace}")
104
- ast['recipes'].append(self._parse_recipe_declaration())
105
- elif self._peek().type == TokenType.LOOT_TABLE:
106
- ast['loot_tables'].append(self._parse_loot_table_declaration())
107
- elif self._peek().type == TokenType.ADVANCEMENT:
108
- ast['advancements'].append(self._parse_advancement_declaration())
109
- elif self._peek().type == TokenType.PREDICATE:
110
- ast['predicates'].append(self._parse_predicate_declaration())
111
- elif self._peek().type == TokenType.ITEM_MODIFIER:
112
- ast['item_modifiers'].append(self._parse_item_modifier_declaration())
113
- elif self._peek().type == TokenType.STRUCTURE:
114
- ast['structures'].append(self._parse_structure_declaration())
115
- else:
116
- # Skip unknown tokens
117
- self._advance()
118
- except MDLLexerError:
119
- # Re-raise lexer errors as they already have proper formatting
120
- raise
121
- except Exception as e:
122
- # Convert other exceptions to parser errors
123
- current_token = self._peek()
124
- raise create_parser_error(
125
- message=str(e),
126
- file_path=self.source_file,
127
- line=current_token.line,
128
- column=current_token.column,
129
- line_content=current_token.value,
130
- suggestion="Check the syntax and ensure all required tokens are present"
131
- )
132
-
133
- # Check for missing closing braces by looking for unmatched opening braces
134
- self._check_for_missing_braces()
135
-
136
- return ast
137
-
138
- def _parse_pack_declaration(self) -> PackDeclaration:
139
- """Parse pack declaration."""
140
- self._match(TokenType.PACK)
141
-
142
- # Parse pack name
143
- name_token = self._match(TokenType.STRING)
144
- name = name_token.value.strip('"').strip("'")
145
-
146
- # Parse description
147
- description_token = self._match(TokenType.STRING)
148
- description = description_token.value.strip('"').strip("'")
149
-
150
- # Parse pack_format
151
- pack_format_token = self._match(TokenType.NUMBER)
152
- pack_format = int(pack_format_token.value)
153
-
154
- self._match(TokenType.SEMICOLON)
155
-
156
- return {"type": "pack_declaration", "name": name, "description": description, "pack_format": pack_format}
157
-
158
- def _parse_namespace_declaration(self) -> NamespaceDeclaration:
159
- """Parse namespace declaration."""
160
- self._match(TokenType.NAMESPACE)
161
-
162
- name_token = self._match(TokenType.STRING)
163
- name = name_token.value.strip('"').strip("'")
164
-
165
- self._match(TokenType.SEMICOLON)
166
-
167
- return {"type": "namespace_declaration", "name": name}
168
-
169
- def _parse_function_declaration(self) -> FunctionDeclaration:
170
- """Parse function declaration."""
171
- self._match(TokenType.FUNCTION)
172
-
173
- name_token = self._match(TokenType.STRING)
174
- name = name_token.value.strip('"').strip("'")
175
-
176
- self._match(TokenType.LBRACE)
177
-
178
- body = []
179
- while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
180
- body.append(self._parse_statement())
181
-
182
- if self._is_at_end():
183
- raise create_parser_error(
184
- message="Missing closing brace for function",
185
- file_path=self.source_file,
186
- line=self._peek().line,
187
- column=self._peek().column,
188
- line_content=self._peek().value,
189
- suggestion="Add a closing brace (}) to match the opening brace"
190
- )
191
-
192
- self._match(TokenType.RBRACE)
193
-
194
- return {"type": "function_declaration", "name": name, "body": body}
195
-
196
- def _parse_recipe_declaration(self) -> RecipeDeclaration:
197
- """Parse recipe declaration."""
198
- self._match(TokenType.RECIPE)
199
-
200
- name_token = self._match(TokenType.STRING)
201
- name = name_token.value.strip('"').strip("'")
202
-
203
- # Expect a JSON file path
204
- json_file_token = self._match(TokenType.STRING)
205
- json_file = json_file_token.value.strip('"').strip("'")
206
-
207
- self._match(TokenType.SEMICOLON)
208
-
209
- # Store reference to JSON file and current namespace
210
- data = {"json_file": json_file}
211
-
212
- result = {"name": name, "data": data, "_source_namespace": self.current_namespace}
213
- print(f"DEBUG: Recipe '{name}' declared with namespace: {self.current_namespace}")
214
- return result
215
-
216
- def _parse_loot_table_declaration(self) -> LootTableDeclaration:
217
- """Parse loot table declaration."""
218
- self._match(TokenType.LOOT_TABLE)
219
-
220
- name_token = self._match(TokenType.STRING)
221
- name = name_token.value.strip('"').strip("'")
222
-
223
- json_file_token = self._match(TokenType.STRING)
224
- json_file = json_file_token.value.strip('"').strip("'")
225
-
226
- self._match(TokenType.SEMICOLON)
227
-
228
- data = {"json_file": json_file}
229
- return {"name": name, "data": data, "_source_namespace": self.current_namespace}
230
-
231
- def _parse_advancement_declaration(self) -> AdvancementDeclaration:
232
- """Parse advancement declaration."""
233
- self._match(TokenType.ADVANCEMENT)
234
-
235
- name_token = self._match(TokenType.STRING)
236
- name = name_token.value.strip('"').strip("'")
237
-
238
- json_file_token = self._match(TokenType.STRING)
239
- json_file = json_file_token.value.strip('"').strip("'")
240
-
241
- self._match(TokenType.SEMICOLON)
242
-
243
- data = {"json_file": json_file}
244
- return {"name": name, "data": data, "_source_namespace": self.current_namespace}
245
-
246
- def _parse_predicate_declaration(self) -> PredicateDeclaration:
247
- """Parse predicate declaration."""
248
- self._match(TokenType.PREDICATE)
249
-
250
- name_token = self._match(TokenType.STRING)
251
- name = name_token.value.strip('"').strip("'")
252
-
253
- json_file_token = self._match(TokenType.STRING)
254
- json_file = json_file_token.value.strip('"').strip("'")
255
-
256
- self._match(TokenType.SEMICOLON)
257
-
258
- data = {"json_file": json_file}
259
- return {"name": name, "data": data, "_source_namespace": self.current_namespace}
260
-
261
- def _parse_item_modifier_declaration(self) -> ItemModifierDeclaration:
262
- """Parse item modifier declaration."""
263
- self._match(TokenType.ITEM_MODIFIER)
264
-
265
- name_token = self._match(TokenType.STRING)
266
- name = name_token.value.strip('"').strip("'")
267
-
268
- json_file_token = self._match(TokenType.STRING)
269
- json_file = json_file_token.value.strip('"').strip("'")
270
-
271
- self._match(TokenType.SEMICOLON)
272
-
273
- data = {"json_file": json_file}
274
- return {"name": name, "data": data, "_source_namespace": self.current_namespace}
275
-
276
- def _parse_structure_declaration(self) -> StructureDeclaration:
277
- """Parse structure declaration."""
278
- self._match(TokenType.STRUCTURE)
279
-
280
- name_token = self._match(TokenType.STRING)
281
- name = name_token.value.strip('"').strip("'")
282
-
283
- json_file_token = self._match(TokenType.STRING)
284
- json_file = json_file_token.value.strip('"').strip("'")
285
-
286
- self._match(TokenType.SEMICOLON)
287
-
288
- data = {"json_file": json_file}
289
- return {"name": name, "data": data, "_source_namespace": self.current_namespace}
290
-
291
- def _parse_statement(self) -> ASTNode:
292
- """Parse a statement."""
293
- if self._peek().type == TokenType.VAR:
294
- return self._parse_variable_declaration()
295
- elif self._peek().type == TokenType.IF:
296
- return self._parse_if_statement()
297
- elif self._peek().type == TokenType.WHILE:
298
- return self._parse_while_loop()
299
- elif self._peek().type == TokenType.FUNCTION:
300
- return self._parse_function_call()
301
- elif self._peek().type == TokenType.EXECUTE:
302
- return self._parse_execute_command()
303
- elif self._peek().type == TokenType.RAW_START:
304
- return self._parse_raw_text()
305
- elif self._peek().type == TokenType.SAY:
306
- return self._parse_say_command()
307
- elif self._peek().type == TokenType.IDENTIFIER:
308
- # Check for for loops (which are no longer supported)
309
- if self._peek().value == "for":
310
- raise create_parser_error(
311
- message="For loops are no longer supported in MDL. Use while loops instead.",
312
- file_path=self.source_file,
313
- line=self._peek().line,
314
- column=self._peek().column,
315
- line_content=self._peek().value,
316
- suggestion="Replace 'for' with 'while' and adjust the loop structure"
317
- )
318
-
319
- # Check if this is a variable assignment (identifier followed by =)
320
- # Need to handle scope selectors like: identifier<scope> = ...
321
- if (self.current + 1 < len(self.tokens) and
322
- self.tokens[self.current + 1].type == TokenType.ASSIGN):
323
- return self._parse_variable_assignment()
324
- elif (self.current + 1 < len(self.tokens) and
325
- self.tokens[self.current + 1].type == TokenType.LANGLE):
326
- # This might be a scoped variable assignment: identifier<scope> = ...
327
- # Look ahead to see if there's an assignment after the scope selector
328
- temp_current = self.current + 1
329
- while (temp_current < len(self.tokens) and
330
- self.tokens[temp_current].type != TokenType.RANGLE and
331
- self.tokens[temp_current].type != TokenType.ASSIGN):
332
- temp_current += 1
333
-
334
- if (temp_current < len(self.tokens) and
335
- self.tokens[temp_current].type == TokenType.RANGLE and
336
- temp_current + 1 < len(self.tokens) and
337
- self.tokens[temp_current + 1].type == TokenType.ASSIGN):
338
- # This is a scoped variable assignment
339
- return self._parse_variable_assignment()
340
- else:
341
- # This is a command with scope selector
342
- return self._parse_command()
343
- else:
344
- # Assume it's a command
345
- return self._parse_command()
346
- else:
347
- # Assume it's a command
348
- return self._parse_command()
349
-
350
- def _parse_variable_declaration(self) -> VariableDeclaration:
351
- """Parse variable declaration."""
352
- self._match(TokenType.VAR)
353
- self._match(TokenType.NUM)
354
-
355
- name_token = self._match(TokenType.IDENTIFIER)
356
- name = name_token.value
357
-
358
- # Check for scope selector after variable name
359
- scope = None
360
- if not self._is_at_end() and self._peek().type == TokenType.LANGLE:
361
- self._match(TokenType.LANGLE) # consume '<'
362
-
363
- # Parse scope selector content
364
- scope_parts = []
365
- while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
366
- scope_parts.append(self._peek().value)
367
- self._advance()
368
-
369
- if self._is_at_end():
370
- raise create_parser_error(
371
- message="Unterminated scope selector",
372
- file_path=self.source_file,
373
- line=self._peek().line,
374
- column=self._peek().column,
375
- line_content=self._peek().value,
376
- suggestion="Add a closing '>' to terminate the scope selector"
377
- )
378
-
379
- self._match(TokenType.RANGLE) # consume '>'
380
- scope = ''.join(scope_parts)
381
- # Update the name to include the scope selector
382
- name = f"{name}<{scope}>"
383
-
384
- self._match(TokenType.ASSIGN)
385
-
386
- # Parse the value (could be a number or expression)
387
- value = self._parse_expression()
388
-
389
- self._match(TokenType.SEMICOLON)
390
-
391
- return {"type": "variable_declaration", "name": name, "scope": scope, "value": value}
392
-
393
- def _parse_variable_assignment(self) -> VariableAssignment:
394
- """Parse variable assignment."""
395
- name_token = self._match(TokenType.IDENTIFIER)
396
- name = name_token.value
397
-
398
- # Check for scope selector after variable name
399
- scope = None
400
- if not self._is_at_end() and self._peek().type == TokenType.LANGLE:
401
- self._match(TokenType.LANGLE) # consume '<'
402
-
403
- # Parse scope selector content
404
- scope_parts = []
405
- while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
406
- scope_parts.append(self._peek().value)
407
- self._advance()
408
-
409
- if self._is_at_end():
410
- raise create_parser_error(
411
- message="Unterminated scope selector",
412
- file_path=self.source_file,
413
- line=self._peek().line,
414
- column=self._peek().column,
415
- line_content=self._peek().value,
416
- suggestion="Add a closing '>' to terminate the scope selector"
417
- )
418
-
419
- self._match(TokenType.RANGLE) # consume '>'
420
- scope = ''.join(scope_parts)
421
- # Update the name to include the scope selector
422
- name = f"{name}<{scope}>"
423
-
424
- self._match(TokenType.ASSIGN)
425
-
426
- # Parse the value (could be a number or expression)
427
- value = self._parse_expression()
428
-
429
- self._match(TokenType.SEMICOLON)
430
-
431
- return {"type": "variable_assignment", "name": name, "scope": scope, "value": value}
432
-
433
- def _parse_if_statement(self) -> IfStatement:
434
- """Parse if statement."""
435
- self._match(TokenType.IF)
436
-
437
- # Parse condition
438
- condition_token = self._match(TokenType.STRING)
439
- condition = condition_token.value.strip('"').strip("'")
440
-
441
- self._match(TokenType.LBRACE)
442
-
443
- # Parse then body
444
- then_body = []
445
- while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
446
- then_body.append(self._parse_statement())
447
-
448
- if self._is_at_end():
449
- raise create_parser_error(
450
- message="Missing closing brace for if statement",
451
- file_path=self.source_file,
452
- line=self._peek().line,
453
- column=self._peek().column,
454
- line_content=self._peek().value,
455
- suggestion="Add a closing brace (}) to match the opening brace"
456
- )
457
-
458
- self._match(TokenType.RBRACE)
459
-
460
- # Check for else or else if
461
- else_body = None
462
- if not self._is_at_end() and self._peek().type == TokenType.ELSE:
463
- self._match(TokenType.ELSE)
464
-
465
- # Check if this is an else if statement
466
- if not self._is_at_end() and self._peek().type == TokenType.IF:
467
- # This is an else if statement - parse it as a nested if
468
- self._match(TokenType.IF)
469
-
470
- # Parse the else if condition
471
- condition_token = self._match(TokenType.STRING)
472
- condition = condition_token.value.strip('"').strip("'")
473
-
474
- self._match(TokenType.LBRACE)
475
-
476
- # Parse the else if body
477
- else_body = []
478
- while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
479
- else_body.append(self._parse_statement())
480
-
481
- if self._is_at_end():
482
- raise create_parser_error(
483
- message="Missing closing brace for else if statement",
484
- file_path=self.source_file,
485
- line=self._peek().line,
486
- column=self._peek().column,
487
- line_content=self._peek().value,
488
- suggestion="Add a closing brace (}) to match the opening brace"
489
- )
490
-
491
- self._match(TokenType.RBRACE)
492
-
493
- # Recursively check for more else if or else statements
494
- if not self._is_at_end() and self._peek().type == TokenType.ELSE:
495
- # Parse the remaining else/else if chain
496
- remaining_else = self._parse_else_chain()
497
- if remaining_else:
498
- # Combine the else if body with the remaining else chain
499
- else_body.extend(remaining_else)
500
- else:
501
- # This is a regular else statement
502
- self._match(TokenType.LBRACE)
503
-
504
- else_body = []
505
- while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
506
- else_body.append(self._parse_statement())
507
-
508
- if self._is_at_end():
509
- raise create_parser_error(
510
- message="Missing closing brace for else statement",
511
- file_path=self.source_file,
512
- line=self._peek().line,
513
- column=self._peek().column,
514
- line_content=self._peek().value,
515
- suggestion="Add a closing brace (}) to match the opening brace"
516
- )
517
-
518
- self._match(TokenType.RBRACE)
519
-
520
- return {"type": "if_statement", "condition": condition, "then_body": then_body, "else_body": else_body}
521
-
522
- def _parse_else_chain(self) -> List[Any]:
523
- """Parse a chain of else if statements and final else statement."""
524
- statements = []
525
-
526
- while not self._is_at_end() and self._peek().type == TokenType.ELSE:
527
- self._match(TokenType.ELSE)
528
-
529
- # Check if this is an else if statement
530
- if not self._is_at_end() and self._peek().type == TokenType.IF:
531
- # This is an else if statement
532
- self._match(TokenType.IF)
533
-
534
- # Parse the else if condition
535
- condition_token = self._match(TokenType.STRING)
536
- condition = condition_token.value.strip('"').strip("'")
537
-
538
- self._match(TokenType.LBRACE)
539
-
540
- # Parse the else if body
541
- else_if_body = []
542
- while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
543
- else_if_body.append(self._parse_statement())
544
-
545
- if self._is_at_end():
546
- raise create_parser_error(
547
- message="Missing closing brace for else if statement",
548
- file_path=self.source_file,
549
- line=self._peek().line,
550
- column=self._peek().column,
551
- line_content=self._peek().value,
552
- suggestion="Add a closing brace (}) to match the opening brace"
553
- )
554
-
555
- self._match(TokenType.RBRACE)
556
-
557
- # Add the else if statement to the chain
558
- statements.append({
559
- "type": "else_if_statement",
560
- "condition": condition,
561
- "body": else_if_body
562
- })
563
- else:
564
- # This is a final else statement
565
- self._match(TokenType.LBRACE)
566
-
567
- else_body = []
568
- while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
569
- else_body.append(self._parse_statement())
570
-
571
- if self._is_at_end():
572
- raise create_parser_error(
573
- message="Missing closing brace for else statement",
574
- file_path=self.source_file,
575
- line=self._peek().line,
576
- column=self._peek().column,
577
- line_content=self._peek().value,
578
- suggestion="Add a closing brace (}) to match the opening brace"
579
- )
580
-
581
- self._match(TokenType.RBRACE)
582
-
583
- # Add the final else statement to the chain
584
- statements.append({
585
- "type": "else_statement",
586
- "body": else_body
587
- })
588
- break # Final else statement ends the chain
589
-
590
- return statements
591
-
592
- def _parse_while_loop(self) -> WhileLoop:
593
- """Parse while loop."""
594
- self._match(TokenType.WHILE)
595
-
596
- # Parse condition
597
- condition_token = self._match(TokenType.STRING)
598
- condition = condition_token.value.strip('"').strip("'")
599
-
600
- # Check for method parameter
601
- method = None
602
- if not self._is_at_end() and self._peek().type == TokenType.IDENTIFIER and self._peek().value == "method":
603
- self._match(TokenType.IDENTIFIER) # consume "method"
604
- self._match(TokenType.ASSIGN)
605
- method_token = self._match(TokenType.STRING)
606
- method = method_token.value.strip('"').strip("'")
607
-
608
- self._match(TokenType.LBRACE)
609
-
610
- # Parse body
611
- body = []
612
- while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
613
- body.append(self._parse_statement())
614
-
615
- if self._is_at_end():
616
- raise create_parser_error(
617
- message="Missing closing brace for while loop",
618
- file_path=self.source_file,
619
- line=self._peek().line,
620
- column=self._peek().column,
621
- line_content=self._peek().value,
622
- suggestion="Add a closing brace (}) to match the opening brace"
623
- )
624
-
625
- self._match(TokenType.RBRACE)
626
-
627
- return {"type": "while_statement", "condition": condition, "method": method, "body": body}
628
-
629
- def _parse_function_call(self) -> FunctionCall:
630
- """Parse function call."""
631
- self._match(TokenType.FUNCTION)
632
-
633
- name_token = self._match(TokenType.STRING)
634
- name = name_token.value.strip('"').strip("'")
635
-
636
- # Check for scope selector after function name
637
- scope = None
638
- if '<' in name and name.endswith('>'):
639
- # Extract scope selector from function name
640
- parts = name.split('<', 1)
641
- if len(parts) == 2:
642
- function_name = parts[0]
643
- scope_selector = parts[1][:-1] # Remove closing >
644
- scope = scope_selector
645
- name = function_name
646
- else:
647
- # Malformed scope selector
648
- raise create_parser_error(
649
- message="Malformed scope selector in function call",
650
- file_path=self.source_file,
651
- line=self._peek().line,
652
- column=self._peek().column,
653
- line_content=name,
654
- suggestion="Use format: function \"namespace:function_name<@selector>\""
655
- )
656
-
657
- # Extract function name from namespace:function_name format
658
- if ':' in name:
659
- namespace_parts = name.split(':', 1)
660
- if len(namespace_parts) == 2:
661
- namespace_name = namespace_parts[0]
662
- function_name = namespace_parts[1]
663
- # Store both namespace and function name
664
- return {"type": "function_call", "name": function_name, "scope": scope, "namespace": namespace_name}
665
-
666
- self._match(TokenType.SEMICOLON)
667
-
668
- return {"type": "function_call", "name": name, "scope": scope}
669
-
670
- def _parse_execute_statement(self) -> ExecuteStatement:
671
- """Parse execute statement."""
672
- self._match(TokenType.EXECUTE)
673
-
674
- # Parse the command
675
- command_parts = []
676
- while not self._is_at_end() and self._peek().type != TokenType.SEMICOLON:
677
- command_parts.append(self._peek().value)
678
- self._advance()
679
-
680
- if self._is_at_end():
681
- raise create_parser_error(
682
- message="Missing semicolon after execute statement",
683
- file_path=self.source_file,
684
- line=self._peek().line,
685
- column=self._peek().column,
686
- line_content=self._peek().value,
687
- suggestion="Add a semicolon (;) at the end of the execute statement"
688
- )
689
-
690
- self._match(TokenType.SEMICOLON)
691
-
692
- command = _smart_join_command_parts(command_parts)
693
- return {"type": "command", "command": command}
694
-
695
- def _parse_raw_text(self) -> RawText:
696
- """Parse raw text block."""
697
- self._match(TokenType.RAW_START)
698
-
699
- # Parse the raw content
700
- content_parts = []
701
- while not self._is_at_end() and self._peek().type != TokenType.RAW_END:
702
- if self._peek().type == TokenType.RAW:
703
- # Add raw content
704
- content_parts.append(self._peek().value)
705
- self._advance()
706
- else:
707
- # Skip other tokens (shouldn't happen in raw mode)
708
- self._advance()
709
-
710
- if self._is_at_end():
711
- raise create_parser_error(
712
- message="Missing closing 'raw!$' for raw text block",
713
- file_path=self.source_file,
714
- line=self._peek().line,
715
- column=self._peek().column,
716
- line_content=self._peek().value,
717
- suggestion="Add 'raw!$' to close the raw text block"
718
- )
719
-
720
- self._match(TokenType.RAW_END)
721
-
722
- content = "".join(content_parts)
723
- # Split content into individual commands by newlines
724
- # Raw blocks contain raw Minecraft commands, not MDL commands with semicolons
725
- commands = [cmd.strip() for cmd in content.split('\n') if cmd.strip()]
726
- return {"type": "raw_text", "commands": commands}
727
-
728
- def _parse_command(self) -> Command:
729
- """Parse a command."""
730
- command_parts = []
731
- while not self._is_at_end() and self._peek().type != TokenType.SEMICOLON:
732
- current_token = self._peek()
733
-
734
- # Check if this is an identifier that might be followed by a scope selector
735
- if current_token.type == TokenType.IDENTIFIER:
736
- identifier_name = current_token.value
737
- command_parts.append(identifier_name)
738
- self._advance() # consume the identifier
739
-
740
- # Look ahead to see if there's a scope selector
741
- if not self._is_at_end() and self._peek().type == TokenType.LANGLE:
742
- # This is a scoped variable - parse the scope selector
743
- self._match(TokenType.LANGLE) # consume '<'
744
-
745
- # Parse scope selector content
746
- scope_parts = []
747
- while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
748
- scope_parts.append(self._peek().value)
749
- self._advance()
750
-
751
- if self._is_at_end():
752
- raise create_parser_error(
753
- message="Unterminated scope selector in command",
754
- file_path=self.source_file,
755
- line=self._peek().line,
756
- column=self._peek().column,
757
- line_content=self._peek().value,
758
- suggestion="Add a closing '>' to terminate the scope selector"
759
- )
760
-
761
- self._match(TokenType.RANGLE) # consume '>'
762
- scope_selector = ''.join(scope_parts)
763
-
764
- # Add the scope selector to the command parts
765
- command_parts.append(f"<{scope_selector}>")
766
- else:
767
- # No scope selector, continue with next token
768
- continue
769
- else:
770
- # Regular token, just add it
771
- command_parts.append(current_token.value)
772
- self._advance()
773
-
774
- if self._is_at_end():
775
- raise create_parser_error(
776
- message="Missing semicolon after command",
777
- file_path=self.source_file,
778
- line=self._peek().line,
779
- column=self._peek().column,
780
- line_content=self._peek().value,
781
- suggestion="Add a semicolon (;) at the end of the command"
782
- )
783
-
784
- self._match(TokenType.SEMICOLON)
785
-
786
- command = _smart_join_command_parts(command_parts)
787
- return {"type": "command", "command": command}
788
-
789
- def _parse_say_command(self) -> Command:
790
- """Parse a say command."""
791
- say_token = self._match(TokenType.SAY)
792
- content = say_token.value
793
-
794
- # The lexer no longer includes the semicolon, so we need to consume it here
795
- self._match(TokenType.SEMICOLON)
796
-
797
- return {"type": "command", "command": f"say {content}"}
798
-
799
- def _parse_execute_command(self) -> Command:
800
- """Parse an execute command."""
801
- execute_token = self._match(TokenType.EXECUTE)
802
- content = execute_token.value
803
-
804
- # The semicolon should already be consumed by the lexer
805
- # But let's make sure we have it
806
- if not self._is_at_end() and self._peek().type == TokenType.SEMICOLON:
807
- self._match(TokenType.SEMICOLON)
808
-
809
- return {"type": "command", "command": content}
810
-
811
- def _parse_hook_declaration(self) -> HookDeclaration:
812
- """Parse hook declaration."""
813
- if self._peek().type == TokenType.ON_TICK:
814
- self._match(TokenType.ON_TICK)
815
- hook_type = "tick"
816
- else:
817
- self._match(TokenType.ON_LOAD)
818
- hook_type = "load"
819
-
820
- function_name_token = self._match(TokenType.STRING)
821
- function_name = function_name_token.value.strip('"').strip("'")
822
-
823
- self._match(TokenType.SEMICOLON)
824
-
825
- return {"type": "hook_declaration", "hook_type": hook_type, "function_name": function_name}
826
-
827
- def _parse_tag_declaration(self) -> TagDeclaration:
828
- """Parse tag declaration."""
829
- self._match(TokenType.TAG)
830
-
831
- # Parse tag type
832
- tag_type_token = self._match(TokenType.IDENTIFIER)
833
- tag_type = tag_type_token.value
834
-
835
- # Parse tag name
836
- name_token = self._match(TokenType.STRING)
837
- name = name_token.value.strip('"').strip("'")
838
-
839
- self._match(TokenType.LBRACE)
840
-
841
- # Parse tag values
842
- values = []
843
- while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
844
- if self._peek().type == TokenType.STRING:
845
- value_token = self._match(TokenType.STRING)
846
- values.append(value_token.value.strip('"').strip("'"))
847
- else:
848
- # Skip non-string tokens
849
- self._advance()
850
-
851
- if self._is_at_end():
852
- raise create_parser_error(
853
- message="Missing closing brace for tag declaration",
854
- file_path=self.source_file,
855
- line=self._peek().line,
856
- column=self._peek().column,
857
- line_content=self._peek().value,
858
- suggestion="Add a closing brace (}) to match the opening brace"
859
- )
860
-
861
- self._match(TokenType.RBRACE)
862
-
863
- return {"type": "tag_declaration", "tag_type": tag_type, "name": name, "values": values}
864
-
865
- def _parse_expression(self) -> Any:
866
- """Parse an expression with operator precedence."""
867
- return self._parse_addition()
868
-
869
- def _parse_addition(self) -> Any:
870
- """Parse addition and subtraction."""
871
- expr = self._parse_multiplication()
872
-
873
- while not self._is_at_end() and self._peek().type in [TokenType.PLUS, TokenType.MINUS]:
874
- operator = self._peek().type
875
- self._advance() # consume operator
876
- right = self._parse_multiplication()
877
- expr = BinaryExpression(expr, operator, right)
878
-
879
- return expr
880
-
881
- def _parse_multiplication(self) -> Any:
882
- """Parse multiplication, division, and modulo."""
883
- expr = self._parse_primary()
884
-
885
- while not self._is_at_end() and self._peek().type in [TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO]:
886
- operator = self._peek().type
887
- self._advance() # consume operator
888
- right = self._parse_primary()
889
- expr = BinaryExpression(expr, operator, right)
890
-
891
- return expr
892
-
893
- def _parse_primary(self) -> Any:
894
- """Parse primary expressions (numbers, strings, variables, parenthesized expressions)."""
895
- token = self._peek()
896
-
897
- if token.type == TokenType.NUMBER:
898
- self._advance()
899
- return LiteralExpression(token.value, "number")
900
- elif token.type == TokenType.STRING:
901
- self._advance()
902
- return LiteralExpression(token.value.strip('"').strip("'"), "string")
903
- elif token.type == TokenType.VARIABLE_SUB:
904
- self._advance()
905
- variable_name = token.value
906
-
907
- # Check if the variable contains a scope selector
908
- if '<' in variable_name and variable_name.endswith('>'):
909
- # Extract variable name and scope selector
910
- parts = variable_name.split('<', 1)
911
- if len(parts) == 2:
912
- var_name = parts[0]
913
- scope_selector = parts[1][:-1] # Remove the closing >
914
- return VariableSubstitutionExpression(var_name, scope_selector)
915
-
916
- # Regular variable substitution without scope
917
- return VariableSubstitutionExpression(variable_name, None)
918
- elif token.type == TokenType.IDENTIFIER:
919
- identifier_name = token.value
920
- self._advance() # consume the identifier
921
-
922
- # Check if this identifier is followed by a scope selector
923
- if not self._is_at_end() and self._peek().type == TokenType.LANGLE:
924
- # This is a scoped variable - parse the scope selector
925
- self._match(TokenType.LANGLE) # consume '<'
926
-
927
- # Parse scope selector content
928
- scope_parts = []
929
- while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
930
- scope_parts.append(self._peek().value)
931
- self._advance()
932
-
933
- if self._is_at_end():
934
- raise create_parser_error(
935
- message="Unterminated scope selector in expression",
936
- file_path=self.source_file,
937
- line=self._peek().line,
938
- column=self._peek().column,
939
- line_content=self._peek().value,
940
- suggestion="Add a closing '>' to terminate the scope selector"
941
- )
942
-
943
- self._match(TokenType.RANGLE) # consume '>'
944
- scope_selector = ''.join(scope_parts)
945
-
946
- # Create a scoped variable expression
947
- full_name = f"{identifier_name}<{scope_selector}>"
948
- return VariableExpression(full_name)
949
-
950
- # Regular variable expression without scope
951
- return VariableExpression(identifier_name)
952
- elif token.type == TokenType.LPAREN:
953
- self._advance() # consume (
954
- expr = self._parse_expression()
955
- self._match(TokenType.RPAREN)
956
- return expr
957
- else:
958
- # Unknown token - create a literal expression
959
- self._advance()
960
- return LiteralExpression(token.value, "unknown")
961
-
962
- def _match(self, expected_type: TokenType) -> Token:
963
- """Match and consume a token of the expected type."""
964
- if self._is_at_end():
965
- raise create_parser_error(
966
- message=f"Unexpected end of input, expected {expected_type}",
967
- file_path=self.source_file,
968
- line=self._peek().line,
969
- column=self._peek().column,
970
- line_content=self._peek().value,
971
- suggestion="Check for missing tokens or incomplete statements"
972
- )
973
-
974
- token = self._peek()
975
- if token.type == expected_type:
976
- return self._advance()
977
- else:
978
- raise create_parser_error(
979
- message=f"Expected {expected_type}, got {token.type}",
980
- file_path=self.source_file,
981
- line=token.line,
982
- column=token.column,
983
- line_content=token.value,
984
- suggestion=f"Replace '{token.value}' with the expected {expected_type}"
985
- )
986
-
987
- def _advance(self) -> Token:
988
- """Advance to the next token."""
989
- if not self._is_at_end():
990
- self.current += 1
991
- return self.tokens[self.current - 1]
992
-
993
- def _peek(self) -> Token:
994
- """Peek at the current token."""
995
- if self._is_at_end():
996
- return self.tokens[-1] # Return EOF token
997
- return self.tokens[self.current]
998
-
999
- def _is_at_end(self) -> bool:
1000
- """Check if we're at the end of the tokens."""
1001
- return self.current >= len(self.tokens)
1002
-
1003
- def _check_for_missing_braces(self):
1004
- """Check for missing closing braces in the source code."""
1005
- # This is a simple check - in a more robust implementation,
1006
- # we would track brace matching during parsing
1007
- # For now, we'll rely on the existing error handling in the parser
1008
- pass
1009
-
1010
-
1011
- def _smart_join_command_parts(parts: List[str]) -> str:
1012
- """Smart join command parts with proper spacing."""
1013
- if not parts:
1014
- return ""
1015
-
1016
- result = parts[0]
1017
-
1018
- for i in range(1, len(parts)):
1019
- prev_part = parts[i - 1]
1020
- curr_part = parts[i]
1021
-
1022
- # Special case: don't add space when previous part ends with a namespace (like minecraft)
1023
- # and current part starts with a colon (like :iron_ingot)
1024
- if curr_part.startswith(':'):
1025
- # Don't add space for namespace:item patterns
1026
- result += curr_part
1027
- else:
1028
- # Add space if needed
1029
- if (prev_part and curr_part and
1030
- not prev_part.endswith('[') and not prev_part.endswith('{') and
1031
- not curr_part.startswith(']') and not curr_part.startswith('}') and
1032
- not curr_part.startswith(',') and not curr_part.startswith(':') and
1033
- not prev_part.endswith('"') and not curr_part.startswith('"')):
1034
- result += " "
1035
-
1036
- # Special case: add space after 'say' before quoted string
1037
- if prev_part == 'say' and curr_part.startswith('"'):
1038
- result += " "
1039
-
1040
- result += curr_part
1041
-
1042
- return result
1043
-
1044
-
1045
- def parse_mdl_js(source: str, source_file: str = None) -> Dict[str, Any]:
1046
- """Parse JavaScript-style MDL source code into AST."""
1047
- tokens = lex_mdl_js(source, source_file)
1048
- parser = MDLParser(tokens, source_file)
1049
- return parser.parse()