minecraft-datapack-language 15.4.41__py3-none-any.whl → 15.4.43__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.
- minecraft_datapack_language/_version.py +2 -2
- minecraft_datapack_language/ast_nodes.py +9 -0
- minecraft_datapack_language/mdl_compiler.py +30 -5
- minecraft_datapack_language/mdl_lexer.py +36 -8
- minecraft_datapack_language/mdl_parser.py +36 -4
- {minecraft_datapack_language-15.4.41.dist-info → minecraft_datapack_language-15.4.43.dist-info}/METADATA +1 -1
- minecraft_datapack_language-15.4.43.dist-info/RECORD +18 -0
- minecraft_datapack_language-15.4.41.dist-info/RECORD +0 -18
- {minecraft_datapack_language-15.4.41.dist-info → minecraft_datapack_language-15.4.43.dist-info}/WHEEL +0 -0
- {minecraft_datapack_language-15.4.41.dist-info → minecraft_datapack_language-15.4.43.dist-info}/entry_points.txt +0 -0
- {minecraft_datapack_language-15.4.41.dist-info → minecraft_datapack_language-15.4.43.dist-info}/licenses/LICENSE +0 -0
- {minecraft_datapack_language-15.4.41.dist-info → minecraft_datapack_language-15.4.43.dist-info}/top_level.txt +0 -0
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '15.4.
|
32
|
-
__version_tuple__ = version_tuple = (15, 4,
|
31
|
+
__version__ = version = '15.4.43'
|
32
|
+
__version_tuple__ = version_tuple = (15, 4, 43)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
@@ -74,6 +74,9 @@ class FunctionCall(ASTNode):
|
|
74
74
|
namespace: str
|
75
75
|
name: str
|
76
76
|
scope: Optional[str] # Optional scope for the function call
|
77
|
+
# Macro invocation support (Minecraft function macros)
|
78
|
+
macro_json: Optional[str] = None # Inline compound JSON string (including braces)
|
79
|
+
with_clause: Optional[str] = None # Raw "with <data source> [path]" clause (without leading 'with')
|
77
80
|
|
78
81
|
|
79
82
|
@dataclass
|
@@ -132,6 +135,12 @@ class ScoreboardCommand(ASTNode):
|
|
132
135
|
command: str
|
133
136
|
|
134
137
|
|
138
|
+
@dataclass
|
139
|
+
class MacroLine(ASTNode):
|
140
|
+
"""A raw macro line for mcfunction starting with '$' and containing $(vars)."""
|
141
|
+
content: str
|
142
|
+
|
143
|
+
|
135
144
|
# Expression nodes
|
136
145
|
@dataclass
|
137
146
|
class BinaryExpression(ASTNode):
|
@@ -11,7 +11,7 @@ from typing import Dict, List, Any, Optional
|
|
11
11
|
from .ast_nodes import (
|
12
12
|
Program, PackDeclaration, NamespaceDeclaration, TagDeclaration,
|
13
13
|
VariableDeclaration, VariableAssignment, VariableSubstitution, FunctionDeclaration,
|
14
|
-
FunctionCall, IfStatement, WhileLoop, HookDeclaration, RawBlock,
|
14
|
+
FunctionCall, IfStatement, WhileLoop, HookDeclaration, RawBlock, MacroLine,
|
15
15
|
SayCommand, BinaryExpression, LiteralExpression, ParenthesizedExpression
|
16
16
|
)
|
17
17
|
from .dir_map import get_dir_map, DirMap
|
@@ -166,11 +166,27 @@ class MDLCompiler:
|
|
166
166
|
for statement in func.body:
|
167
167
|
cmd = self._statement_to_command(statement)
|
168
168
|
if cmd:
|
169
|
-
lines.append(cmd)
|
169
|
+
lines.append(self._ensure_macro_prefix(cmd))
|
170
170
|
# Done routing temp commands for this function body
|
171
171
|
self._temp_sink_stack.pop()
|
172
172
|
|
173
173
|
return "\n".join(lines)
|
174
|
+
|
175
|
+
def _ensure_macro_prefix(self, text: str) -> str:
|
176
|
+
"""Ensure any line containing a macro placeholder $(var) starts with '$'.
|
177
|
+
Handles multi-line text by processing each line independently.
|
178
|
+
"""
|
179
|
+
import re
|
180
|
+
macro_re = re.compile(r"\$\([A-Za-z_][A-Za-z0-9_]*\)")
|
181
|
+
def process_line(line: str) -> str:
|
182
|
+
if macro_re.search(line):
|
183
|
+
stripped = line.lstrip()
|
184
|
+
if not stripped.startswith('$'):
|
185
|
+
return '$' + line
|
186
|
+
return line
|
187
|
+
if '\n' in text:
|
188
|
+
return '\n'.join(process_line(ln) for ln in text.split('\n'))
|
189
|
+
return process_line(text)
|
174
190
|
|
175
191
|
def _compile_hooks(self, hooks: List[HookDeclaration], namespace_dir: Path):
|
176
192
|
"""Compile hook declarations."""
|
@@ -325,6 +341,8 @@ class MDLCompiler:
|
|
325
341
|
return self._say_command_to_command(statement)
|
326
342
|
elif isinstance(statement, RawBlock):
|
327
343
|
return statement.content
|
344
|
+
elif isinstance(statement, MacroLine):
|
345
|
+
return statement.content
|
328
346
|
elif isinstance(statement, IfStatement):
|
329
347
|
return self._if_statement_to_command(statement)
|
330
348
|
elif isinstance(statement, WhileLoop):
|
@@ -616,10 +634,17 @@ class MDLCompiler:
|
|
616
634
|
|
617
635
|
def _function_call_to_command(self, func_call: FunctionCall) -> str:
|
618
636
|
"""Convert function call to execute command."""
|
637
|
+
# Build base function invocation, possibly with macro args
|
638
|
+
suffix = ""
|
639
|
+
if func_call.macro_json:
|
640
|
+
suffix = f" {func_call.macro_json}"
|
641
|
+
elif func_call.with_clause:
|
642
|
+
suffix = f" with {func_call.with_clause}"
|
643
|
+
|
644
|
+
base = f"function {func_call.namespace}:{func_call.name}{suffix}"
|
619
645
|
if func_call.scope:
|
620
|
-
return f"execute as {func_call.scope.strip('<>')} run
|
621
|
-
|
622
|
-
return f"function {func_call.namespace}:{func_call.name}"
|
646
|
+
return f"execute as {func_call.scope.strip('<>')} run {base}"
|
647
|
+
return base
|
623
648
|
|
624
649
|
def _expression_to_value(self, expression: Any) -> str:
|
625
650
|
"""Convert expression to a value string."""
|
@@ -79,6 +79,7 @@ class TokenType:
|
|
79
79
|
QUOTE = "QUOTE" # " (string literal delimiter)
|
80
80
|
EXCLAMATION = "EXCLAMATION" # ! (for raw blocks)
|
81
81
|
RANGE = "RANGE" # .. (range operator)
|
82
|
+
DOT = "DOT" # . (for paths in with-clause)
|
82
83
|
|
83
84
|
# Literals
|
84
85
|
IDENTIFIER = "IDENTIFIER" # Variable names, function names, etc.
|
@@ -89,6 +90,7 @@ class TokenType:
|
|
89
90
|
EOF = "EOF"
|
90
91
|
COMMENT = "COMMENT" # Comments (ignored during parsing)
|
91
92
|
RAW_CONTENT = "RAW_CONTENT" # Raw content inside raw blocks
|
93
|
+
MACRO_LINE = "MACRO_LINE" # Entire macro line starting with '$' at line-begin
|
92
94
|
|
93
95
|
|
94
96
|
class MDLLexer:
|
@@ -167,9 +169,9 @@ class MDLLexer:
|
|
167
169
|
self._scan_multi_line_comment()
|
168
170
|
return
|
169
171
|
|
170
|
-
# Handle strings (quotes)
|
171
|
-
if char == '"':
|
172
|
-
self._scan_string()
|
172
|
+
# Handle strings (quotes) - support both ' and "
|
173
|
+
if char == '"' or char == "'":
|
174
|
+
self._scan_string(quote_char=char)
|
173
175
|
return
|
174
176
|
|
175
177
|
# Handle raw block markers
|
@@ -180,6 +182,11 @@ class MDLLexer:
|
|
180
182
|
|
181
183
|
|
182
184
|
|
185
|
+
# Handle macro line: '$' as first non-space on the line (not $!raw)
|
186
|
+
if char == '$' and self._is_line_start_nonspace():
|
187
|
+
self._scan_macro_line()
|
188
|
+
return
|
189
|
+
|
183
190
|
# Handle variable substitution
|
184
191
|
if char == '$':
|
185
192
|
self._scan_variable_substitution()
|
@@ -264,7 +271,7 @@ class MDLLexer:
|
|
264
271
|
# Unterminated comment
|
265
272
|
self._error("Unterminated multi-line comment", "Add */ to close the comment")
|
266
273
|
|
267
|
-
def _scan_string(self):
|
274
|
+
def _scan_string(self, quote_char='"'):
|
268
275
|
"""Scan a string literal (quoted text)."""
|
269
276
|
# Skip opening quote
|
270
277
|
self.current += 1
|
@@ -275,7 +282,7 @@ class MDLLexer:
|
|
275
282
|
|
276
283
|
# Scan until closing quote
|
277
284
|
while (self.current < len(self.source) and
|
278
|
-
self.source[self.current] !=
|
285
|
+
self.source[self.current] != quote_char):
|
279
286
|
if self.source[self.current] == '\n':
|
280
287
|
self._error("Unterminated string literal", "Add a closing quote")
|
281
288
|
|
@@ -295,14 +302,34 @@ class MDLLexer:
|
|
295
302
|
self.column += 1
|
296
303
|
|
297
304
|
# Generate QUOTE token for the opening quote
|
298
|
-
self.tokens.append(Token(TokenType.QUOTE,
|
305
|
+
self.tokens.append(Token(TokenType.QUOTE, quote_char, start_line, start_column))
|
299
306
|
|
300
307
|
# Generate IDENTIFIER token for the string content
|
301
308
|
string_content = self.source[self.start + 1:self.current - 1]
|
302
309
|
self.tokens.append(Token(TokenType.IDENTIFIER, string_content, start_line, start_column + 1))
|
303
310
|
|
304
311
|
# Generate QUOTE token for the closing quote
|
305
|
-
self.tokens.append(Token(TokenType.QUOTE,
|
312
|
+
self.tokens.append(Token(TokenType.QUOTE, quote_char, self.line, self.column - 1))
|
313
|
+
|
314
|
+
def _is_line_start_nonspace(self) -> bool:
|
315
|
+
"""Return True if current position is at the first non-space character in the line."""
|
316
|
+
# Find beginning of current line
|
317
|
+
idx = self.current - 1
|
318
|
+
while idx >= 0 and self.source[idx] != '\n':
|
319
|
+
if not self.source[idx].isspace():
|
320
|
+
return False
|
321
|
+
idx -= 1
|
322
|
+
return True
|
323
|
+
|
324
|
+
def _scan_macro_line(self):
|
325
|
+
"""Scan a full macro line starting with '$' as first non-space char."""
|
326
|
+
# Capture from current to end of line (excluding trailing newline)
|
327
|
+
line_start = self.current
|
328
|
+
while self.current < len(self.source) and self.source[self.current] != '\n':
|
329
|
+
self.current += 1
|
330
|
+
self.column += 1
|
331
|
+
content = self.source[line_start:self.current]
|
332
|
+
self.tokens.append(Token(TokenType.MACRO_LINE, content, self.line, 1))
|
306
333
|
|
307
334
|
def _scan_raw_block_start(self):
|
308
335
|
"""Scan the start of a raw block ($!raw)."""
|
@@ -548,7 +575,8 @@ class MDLLexer:
|
|
548
575
|
'{': TokenType.LBRACE,
|
549
576
|
'}': TokenType.RBRACE,
|
550
577
|
'[': TokenType.LBRACKET,
|
551
|
-
']': TokenType.RBRACKET
|
578
|
+
']': TokenType.RBRACKET,
|
579
|
+
'.': TokenType.DOT
|
552
580
|
}
|
553
581
|
|
554
582
|
if char in token_map:
|
@@ -9,7 +9,7 @@ from .mdl_errors import MDLParserError
|
|
9
9
|
from .ast_nodes import (
|
10
10
|
ASTNode, Program, PackDeclaration, NamespaceDeclaration, TagDeclaration,
|
11
11
|
VariableDeclaration, VariableAssignment, VariableSubstitution, FunctionDeclaration,
|
12
|
-
FunctionCall, IfStatement, WhileLoop, HookDeclaration, RawBlock,
|
12
|
+
FunctionCall, IfStatement, WhileLoop, HookDeclaration, RawBlock, MacroLine,
|
13
13
|
SayCommand, TellrawCommand, ExecuteCommand, ScoreboardCommand,
|
14
14
|
BinaryExpression, UnaryExpression, ParenthesizedExpression, LiteralExpression,
|
15
15
|
ScopeSelector
|
@@ -257,7 +257,7 @@ class MDLParser:
|
|
257
257
|
)
|
258
258
|
|
259
259
|
def _parse_function_call(self) -> FunctionCall:
|
260
|
-
"""Parse function call: exec namespace:name<scope
|
260
|
+
"""Parse function call: exec namespace:name<scope>? [ '{json}' | with <data source> [path] ] ;"""
|
261
261
|
self._expect(TokenType.EXEC, "Expected 'exec' keyword")
|
262
262
|
|
263
263
|
# Parse namespace:name
|
@@ -269,13 +269,41 @@ class MDLParser:
|
|
269
269
|
scope = None
|
270
270
|
if self._peek().type == TokenType.LANGLE:
|
271
271
|
scope = self._parse_scope_selector()
|
272
|
-
|
272
|
+
|
273
|
+
# Optional macro arguments (inline JSON in a quoted string)
|
274
|
+
macro_json = None
|
275
|
+
with_clause = None
|
276
|
+
if self._peek().type == TokenType.QUOTE:
|
277
|
+
self._advance() # opening quote
|
278
|
+
if self._peek().type == TokenType.IDENTIFIER:
|
279
|
+
macro_json = self._peek().value
|
280
|
+
self._advance()
|
281
|
+
self._expect(TokenType.QUOTE, "Expected closing quote for macro JSON")
|
282
|
+
elif self._peek().type == TokenType.IDENTIFIER and self._peek().value == 'with':
|
283
|
+
# Capture everything after 'with' up to the semicolon as raw clause
|
284
|
+
self._advance() # consume 'with'
|
285
|
+
# Expect a data source spec like: storage <identifier> <path-with-dots>
|
286
|
+
# Accumulate tokens until semicolon, inserting spaces only between identifiers/numbers
|
287
|
+
built: List[str] = []
|
288
|
+
prev_type = None
|
289
|
+
while not self._is_at_end() and self._peek().type != TokenType.SEMICOLON:
|
290
|
+
t = self._advance()
|
291
|
+
# Insert a space between adjacent identifiers/numbers
|
292
|
+
if built and (prev_type in (TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.RBRACE, TokenType.RBRACKET)
|
293
|
+
and t.type in (TokenType.IDENTIFIER, TokenType.NUMBER)):
|
294
|
+
built.append(" ")
|
295
|
+
built.append(t.value)
|
296
|
+
prev_type = t.type
|
297
|
+
with_clause = "".join(built).strip()
|
298
|
+
|
273
299
|
self._expect(TokenType.SEMICOLON, "Expected semicolon after function call")
|
274
300
|
|
275
301
|
return FunctionCall(
|
276
302
|
namespace=namespace,
|
277
303
|
name=name,
|
278
|
-
scope=scope
|
304
|
+
scope=scope,
|
305
|
+
macro_json=macro_json,
|
306
|
+
with_clause=with_clause
|
279
307
|
)
|
280
308
|
|
281
309
|
def _parse_if_statement(self) -> IfStatement:
|
@@ -498,6 +526,10 @@ class MDLParser:
|
|
498
526
|
statements.append(self._parse_while_loop())
|
499
527
|
elif self._peek().type == TokenType.EXEC:
|
500
528
|
statements.append(self._parse_function_call())
|
529
|
+
elif self._peek().type == TokenType.MACRO_LINE:
|
530
|
+
# Preserve macro line exactly as-is
|
531
|
+
statements.append(MacroLine(content=self._peek().value))
|
532
|
+
self._advance()
|
501
533
|
elif self._peek().type == TokenType.DOLLAR and self._peek(1).type == TokenType.EXCLAMATION:
|
502
534
|
statements.append(self._parse_raw_block())
|
503
535
|
elif self._peek().type == TokenType.IDENTIFIER:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: minecraft-datapack-language
|
3
|
-
Version: 15.4.
|
3
|
+
Version: 15.4.43
|
4
4
|
Summary: Compile MDL language with explicit scoping into a Minecraft datapack (1.21+ ready). Features variables, control flow, error handling, and VS Code extension.
|
5
5
|
Project-URL: Homepage, https://www.mcmdl.com
|
6
6
|
Project-URL: Documentation, https://www.mcmdl.com/docs
|
@@ -0,0 +1,18 @@
|
|
1
|
+
minecraft_datapack_language/__init__.py,sha256=0KVXBE4ScRaRUrf83aA2tVB-y8A_jplyaxVvtHH6Uw0,1199
|
2
|
+
minecraft_datapack_language/_version.py,sha256=6f2gaPXMU8K3LcbQn-JSmdlistnzJIqRlovy7echBYs,708
|
3
|
+
minecraft_datapack_language/ast_nodes.py,sha256=UzUxKLkjBisUd5Gu7sAiNXIIPIjNoRzELq4LfIFcnSY,4290
|
4
|
+
minecraft_datapack_language/cli.py,sha256=R4QZYtox-Da9B8pr_kCg_9qc9aI-ORTah7kMkhsI5tw,10373
|
5
|
+
minecraft_datapack_language/dir_map.py,sha256=HmxFkuvWGkzHF8o_GFb4BpuMCRc6QMw8UbmcAI8JVdY,1788
|
6
|
+
minecraft_datapack_language/mdl_compiler.py,sha256=9o9NXd9_szEKEmmZ0HPanFA9W8UOPOVA5Jbw-9XyM1w,42414
|
7
|
+
minecraft_datapack_language/mdl_errors.py,sha256=r0Gu3KhoX1YLPAVW_iO7Q_fPgaf_Dv9tOGSOdKNSzmw,16114
|
8
|
+
minecraft_datapack_language/mdl_lexer.py,sha256=dVwdgUmdQI7EJaFtoKZpq6rj5156YVjt44mMee-MDIs,23388
|
9
|
+
minecraft_datapack_language/mdl_linter.py,sha256=z85xoAglENurCh30bR7kEHZ_JeMxcYaLDcGNRAl-RAI,17253
|
10
|
+
minecraft_datapack_language/mdl_parser.py,sha256=axyjdrcgqeOpbmWolasiIZAjV_RpFaP5QiafLPXAhms,25223
|
11
|
+
minecraft_datapack_language/python_api.py,sha256=Iao1jbdeW6ekeA80BZG6gNqHVjxQJEheB3DbpVsuTZQ,12304
|
12
|
+
minecraft_datapack_language/utils.py,sha256=Aq0HAGlXqj9BUTEjaEilpvzEW0EtZYYMMwOqG9db6dE,684
|
13
|
+
minecraft_datapack_language-15.4.43.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
14
|
+
minecraft_datapack_language-15.4.43.dist-info/METADATA,sha256=ZIf4a5lzXA4fLPfsIZOC38wgK9JjpnNscdutjZC3rZE,8344
|
15
|
+
minecraft_datapack_language-15.4.43.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
16
|
+
minecraft_datapack_language-15.4.43.dist-info/entry_points.txt,sha256=c6vjBeCiyQflvPHBRyBk2nJCSfYt3Oc7Sc9V87ySi_U,108
|
17
|
+
minecraft_datapack_language-15.4.43.dist-info/top_level.txt,sha256=ADtFI476tbKLLxEAA-aJQAfg53MA3k_DOb0KTFiggfw,28
|
18
|
+
minecraft_datapack_language-15.4.43.dist-info/RECORD,,
|
@@ -1,18 +0,0 @@
|
|
1
|
-
minecraft_datapack_language/__init__.py,sha256=0KVXBE4ScRaRUrf83aA2tVB-y8A_jplyaxVvtHH6Uw0,1199
|
2
|
-
minecraft_datapack_language/_version.py,sha256=7O183XaGv-j8PcITk4W9KCJ0Sx7gIXLD8SvmgNMUBnU,708
|
3
|
-
minecraft_datapack_language/ast_nodes.py,sha256=nbWrRz137MGMRpmnq8QkXNzrtlaCgyPEknytbkrS_M8,3899
|
4
|
-
minecraft_datapack_language/cli.py,sha256=R4QZYtox-Da9B8pr_kCg_9qc9aI-ORTah7kMkhsI5tw,10373
|
5
|
-
minecraft_datapack_language/dir_map.py,sha256=HmxFkuvWGkzHF8o_GFb4BpuMCRc6QMw8UbmcAI8JVdY,1788
|
6
|
-
minecraft_datapack_language/mdl_compiler.py,sha256=CaIHmsv4cjZa70PpderdgPlH6yNaB86WQhGyIaB0040,41396
|
7
|
-
minecraft_datapack_language/mdl_errors.py,sha256=r0Gu3KhoX1YLPAVW_iO7Q_fPgaf_Dv9tOGSOdKNSzmw,16114
|
8
|
-
minecraft_datapack_language/mdl_lexer.py,sha256=CjbEUpuuF4eU_ucA_WIhw6wSMcHGk2BchtQ0bLAGvwg,22033
|
9
|
-
minecraft_datapack_language/mdl_linter.py,sha256=z85xoAglENurCh30bR7kEHZ_JeMxcYaLDcGNRAl-RAI,17253
|
10
|
-
minecraft_datapack_language/mdl_parser.py,sha256=aQPKcmATM9BOMzO7vCXmMdxU1qjOJNLCSAKJopu5h3g,23429
|
11
|
-
minecraft_datapack_language/python_api.py,sha256=Iao1jbdeW6ekeA80BZG6gNqHVjxQJEheB3DbpVsuTZQ,12304
|
12
|
-
minecraft_datapack_language/utils.py,sha256=Aq0HAGlXqj9BUTEjaEilpvzEW0EtZYYMMwOqG9db6dE,684
|
13
|
-
minecraft_datapack_language-15.4.41.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
14
|
-
minecraft_datapack_language-15.4.41.dist-info/METADATA,sha256=8VRf590kEqLu2Te-AHafQxQjpT0pFhz-VEay36-olQ0,8344
|
15
|
-
minecraft_datapack_language-15.4.41.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
16
|
-
minecraft_datapack_language-15.4.41.dist-info/entry_points.txt,sha256=c6vjBeCiyQflvPHBRyBk2nJCSfYt3Oc7Sc9V87ySi_U,108
|
17
|
-
minecraft_datapack_language-15.4.41.dist-info/top_level.txt,sha256=ADtFI476tbKLLxEAA-aJQAfg53MA3k_DOb0KTFiggfw,28
|
18
|
-
minecraft_datapack_language-15.4.41.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|