bolt-native-macros 0.2.3__tar.gz

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.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.3
2
+ Name: bolt-native-macros
3
+ Version: 0.2.3
4
+ Summary: Add your description here
5
+ Requires-Dist: beet>=0.112.2
6
+ Requires-Dist: bolt>=0.49.2
7
+ Requires-Dist: mecha>=0.101.0
8
+ Requires-Dist: bolt-expressions>=0.17.0 ; extra == 'bolt-expressions'
9
+ Requires-Python: >=3.12
10
+ Provides-Extra: bolt-expressions
11
+ Description-Content-Type: text/markdown
12
+
File without changes
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "bolt-native-macros"
3
+ version = "0.2.3"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "beet>=0.112.2",
9
+ "bolt>=0.49.2",
10
+ "mecha>=0.101.0",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ bolt-expressions = [
15
+ "bolt-expressions>=0.17.0",
16
+ ]
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "lectern>=0.34.0",
21
+ "pytest>=9.0.2",
22
+ "pytest-insta>=0.4.1",
23
+ "python-semantic-release>=9.21.0",
24
+ "ruff>=0.14.13",
25
+ ]
26
+
27
+ [tool.semantic_release]
28
+ major_on_zero = false
29
+ build_command = "uv build"
30
+
31
+ version_variables = ["src/bolt_native_macros/__init__.py:__version__"]
32
+ version_toml = ["pyproject.toml:project.version"]
33
+ commit_author = "github-actions <action@github.com>"
34
+
35
+ [tool.semantic_release.publish]
36
+ # This ensures it uses the build_command defined above
37
+ dist_glob_patterns = ["dist/*"]
38
+ upload_to_vcs_release = true
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.9.26,<0.10.0"]
42
+ build-backend = "uv_build"
@@ -0,0 +1,3 @@
1
+ __version__ = "0.2.3"
2
+
3
+ from .plugin import beet_default as beet_default
@@ -0,0 +1,97 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Literal
3
+
4
+ from beet.core.utils import required_field
5
+ from mecha import AstGreedy, AstMessage, AstNbtValue, AstNode, AstString, AstWord
6
+
7
+ from .typing import MacroRepresentation, MacroTag
8
+
9
+ __all__ = [
10
+ "AstMacroArgument",
11
+ "AstMacroNbtArgument",
12
+ "AstMacroCoordinateArgument",
13
+ "AstMacroNbtPathKeyArgument",
14
+ "AstMacroNbtPathArgument",
15
+ "AstMacroRange",
16
+ "AstMacroStringWrapper",
17
+ "AstNbtValueWithMacro",
18
+ "AstStringWithMacro",
19
+ "AstGreedyWithMacro",
20
+ "AstWordWithMacro",
21
+ "AstMessageWithMacro",
22
+ ]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class AstMacroArgument(AstNode, MacroRepresentation):
27
+ name: str = required_field()
28
+ parser: str | None = required_field()
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class AstMacroNbtArgument(AstMacroArgument):
33
+ def evaluate(self):
34
+ return MacroTag(self.name, self.parser)
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class AstMacroCoordinateArgument(AstMacroArgument):
39
+ type: Literal["absolute", "local", "relative"] = required_field()
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class AstMacroNbtPathKeyArgument(AstMacroArgument): ...
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class AstMacroNbtPathArgument(AstMacroArgument): ...
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class AstMacroExpression(AstMacroArgument): ...
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class AstMacroRange(AstNode):
56
+ min: int | float | AstMacroArgument | None = field(default=None)
57
+ max: int | float | AstMacroArgument | None = field(default=None)
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class AstMacroStringWrapper[N](AstNode):
62
+ child: N = required_field()
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class AstNbtValueWithMacro(AstNbtValue, MacroRepresentation):
67
+ @classmethod
68
+ def from_value(cls, value: Any) -> "AstNbtValueWithMacro":
69
+ return cls(value=value)
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class AstStringWithMacro(AstString, MacroRepresentation):
74
+ @classmethod
75
+ def from_value(cls, value: Any) -> "AstStringWithMacro":
76
+ return AstStringWithMacro(value=str(value))
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class AstGreedyWithMacro(AstGreedy, MacroRepresentation):
81
+ @classmethod
82
+ def from_value(cls, value: Any) -> "AstGreedyWithMacro":
83
+ return cls(value=AstGreedy.from_value(value).value)
84
+
85
+
86
+ @dataclass(frozen=True, slots=True)
87
+ class AstWordWithMacro(AstWord, MacroRepresentation):
88
+ @classmethod
89
+ def from_value(cls, value: Any) -> "AstWordWithMacro":
90
+ return cls(value=AstWord.from_value(value).value)
91
+
92
+
93
+ @dataclass(frozen=True, slots=True)
94
+ class AstMessageWithMacro(AstMessage, MacroRepresentation):
95
+ @classmethod
96
+ def from_value(cls, value: Any) -> "AstMessageWithMacro":
97
+ return cls(fragments=AstMessage.from_value(value).fragments)
@@ -0,0 +1,52 @@
1
+ from dataclasses import dataclass
2
+ from typing import Generator, List, Optional
3
+
4
+ from bolt import Accumulator, visit_generic, visit_single
5
+ from mecha import AstNode, Visitor, rule
6
+
7
+ from .ast import AstMacroArgument, AstMacroExpression, AstMacroStringWrapper
8
+ from .typing import MacroTag, StringWithMacro
9
+
10
+
11
+ def ast_to_macro(macro: AstMacroArgument):
12
+ return MacroTag(macro.name, macro.parser)
13
+
14
+
15
+ def make_macro_string():
16
+ """
17
+ Returns the type `StringWithMacro`, this is to add the type to the scope w/o making it globally accessible.
18
+
19
+ Kind of hacky but works well
20
+ """
21
+ return StringWithMacro
22
+
23
+
24
+ @dataclass
25
+ class MacroCodegen(Visitor):
26
+ @rule(AstMacroExpression)
27
+ def macro(
28
+ self, node: AstMacroExpression, acc: Accumulator
29
+ ) -> Generator[AstNode, Optional[List[str]], Optional[List[str]]]:
30
+ # This allows for macros to be used as literals
31
+ result = yield from visit_generic(node, acc)
32
+
33
+ if result is None:
34
+ result = acc.make_ref(node)
35
+
36
+ result = acc.helper(ast_to_macro.__name__, result)
37
+
38
+ return [result]
39
+
40
+ @rule(AstMacroStringWrapper)
41
+ def wrapper(
42
+ self, node: AstMacroStringWrapper, acc: Accumulator
43
+ ) -> Generator[AstNode, Optional[List[str]], Optional[List[str]]]:
44
+ # Codegen the underlying child and get its result
45
+ child = yield from visit_single(node.child, required=True)
46
+
47
+ # Create a variable and assign it to a new instance of StringWithMacro
48
+ result = acc.make_variable()
49
+ # make_macro_string returns the **type** StringWithMacro, you must manually create the instance afterwards
50
+ acc.statement(f"{result} = {acc.helper(make_macro_string.__name__)}({child})")
51
+
52
+ return [result]
@@ -0,0 +1,248 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, List, cast
3
+
4
+ from bolt import AstInterpolation
5
+ from mecha import (
6
+ NUMBER_PATTERN,
7
+ AlternativeParser,
8
+ AstChildren,
9
+ AstMacroLineVariable,
10
+ AstNbtPath,
11
+ AstNbtPathKey,
12
+ NbtPathParser,
13
+ Parser,
14
+ delegate,
15
+ )
16
+ from mecha.utils import string_to_number
17
+ from tokenstream import InvalidSyntax, TokenStream, set_location
18
+
19
+ from .ast import (
20
+ AstMacroArgument,
21
+ AstMacroCoordinateArgument,
22
+ AstMacroNbtPathArgument,
23
+ AstMacroNbtPathKeyArgument,
24
+ AstMacroRange,
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class MacroParser:
30
+ """
31
+ Used to parse typed_macro's and create the proper AstNode's
32
+ """
33
+
34
+ parser: str | tuple[str, ...]
35
+ node_type: type[AstMacroArgument]
36
+
37
+ def __call__(self, stream: TokenStream):
38
+ macro: AstMacroArgument = delegate("typed_macro", stream)
39
+
40
+ if macro.parser:
41
+ # Implements type checking where it makes sense, this helps to prevent unintended macro injections
42
+ if isinstance(self.parser, str) and macro.parser != self.parser:
43
+ raise ValueError(
44
+ f"Invalid macro type, received {macro.parser} expected {self.parser}"
45
+ )
46
+ elif macro.parser not in self.parser:
47
+ raise ValueError(
48
+ f"Invalid macro type, received {macro.parser} expected one of {', '.join(self.parser)}"
49
+ )
50
+
51
+ parser = macro.parser
52
+
53
+ # If there was no parser just use one of the intended ones for this MacroParser
54
+ if isinstance(self.parser, tuple) and parser is None:
55
+ parser = self.parser[0]
56
+ elif isinstance(self.parser, str):
57
+ parser = self.parser
58
+
59
+ # Creates the proper instance of node_type
60
+ if not isinstance(macro, self.node_type):
61
+ return self.node_type(name=macro.name, parser=parser)
62
+
63
+ return macro
64
+
65
+
66
+ @dataclass
67
+ class MacroNbtPathParser(NbtPathParser):
68
+ """Parser for nbt paths."""
69
+
70
+ def __call__(self, stream: TokenStream) -> AstNbtPath:
71
+ components: List[Any] = []
72
+
73
+ with stream.syntax(
74
+ dot=r"\.",
75
+ curly=r"\{|\}",
76
+ bracket=r"\[|\]",
77
+ quoted_string=r'"(?:\\.|[^\\\n])*?"' "|" r"'(?:\\.|[^\\\n])*?'",
78
+ string=r"(?:[0-9a-z_\-]+:)?[a-zA-Z0-9_+-]+",
79
+ ):
80
+ components.extend(self.parse_modifiers(stream))
81
+
82
+ while not components or stream.get("dot"):
83
+ with stream.checkpoint() as commit:
84
+ macro: AstMacroArgument = delegate("typed_macro", stream)
85
+
86
+ if not macro.parser or macro.parser == "string":
87
+ components.append(
88
+ set_location(
89
+ AstMacroNbtPathKeyArgument(
90
+ name=macro.name, parser="string"
91
+ ),
92
+ macro,
93
+ )
94
+ )
95
+ elif macro.parser == "nbt":
96
+ components.append(
97
+ set_location(
98
+ AstMacroNbtPathArgument(name=macro.name, parser="nbt"),
99
+ macro,
100
+ )
101
+ )
102
+
103
+ commit()
104
+
105
+ if commit.rollback:
106
+ quoted_string, string = stream.expect("quoted_string", "string")
107
+
108
+ if quoted_string:
109
+ component_node = AstNbtPathKey(
110
+ value=self.quote_helper.unquote_string(quoted_string),
111
+ )
112
+ components.append(set_location(component_node, quoted_string))
113
+ elif string:
114
+ component_node = AstNbtPathKey(value=string.value)
115
+ components.append(set_location(component_node, string))
116
+
117
+ components.extend(self.parse_modifiers(stream))
118
+
119
+ if not components:
120
+ raise stream.emit_error(InvalidSyntax("Empty nbt path not allowed."))
121
+
122
+ node = AstNbtPath(components=AstChildren(components))
123
+ return set_location(node, components[0], components[-1])
124
+
125
+
126
+ class MacroRangeParser:
127
+ def get_bound(self, stream: TokenStream) -> int | float | AstMacroArgument | None:
128
+ if number := stream.get("number"):
129
+ return string_to_number(number.value)
130
+
131
+ with stream.checkpoint() as commit:
132
+ macro: AstMacroArgument = delegate("typed_macro", stream)
133
+
134
+ if macro.parser and macro.parser != "numeric":
135
+ raise ValueError(
136
+ f"Invalid macro type, received {macro.parser} expected numeric"
137
+ )
138
+
139
+ commit()
140
+
141
+ if not commit.rollback:
142
+ return macro
143
+
144
+ return None
145
+
146
+ def __call__(self, stream: TokenStream):
147
+ with stream.syntax(range=r"\.\.", number=NUMBER_PATTERN):
148
+ lower_bound = self.get_bound(stream)
149
+ range = stream.get("range")
150
+ upper_bound = self.get_bound(stream)
151
+
152
+ return set_location(
153
+ AstMacroRange(min=lower_bound, max=upper_bound),
154
+ lower_bound or range or upper_bound,
155
+ upper_bound or range or lower_bound,
156
+ )
157
+
158
+
159
+ def macro(
160
+ parsers: dict[str, Parser],
161
+ type: str | tuple[str],
162
+ priority=False,
163
+ node_type: type[AstMacroArgument] = AstMacroArgument,
164
+ ):
165
+ """
166
+ Creates the proper AlternativeParser
167
+
168
+ :param parsers: The current set of parsers from mecha
169
+ :type parsers: dict[str, Parser]
170
+ :param type: The parser to create an alternative for
171
+ :type type: str | tuple[str]
172
+ :param priority: Should a macro be checked for before the original parser is used
173
+ :param node_type: The kind of node to be created by the parser
174
+ :type node_type: type[AstMacroArgument]
175
+ """
176
+ parser_type = type
177
+ if isinstance(type, tuple):
178
+ parser_type = type[0]
179
+
180
+ if not priority:
181
+ return AlternativeParser(
182
+ [parsers[cast(str, parser_type)], MacroParser(type, node_type)]
183
+ )
184
+ return AlternativeParser(
185
+ [MacroParser(type, node_type), parsers[cast(str, parser_type)]]
186
+ )
187
+
188
+
189
+ def parse_typed_macro(stream: TokenStream):
190
+ """
191
+ Parses macros with a parser type
192
+ Ex: $(foo: numeric)
193
+
194
+ :param stream: The instance of TokenStream
195
+ :type stream: TokenStream
196
+ """
197
+ with stream.syntax(
198
+ open_variable=r"\$\(", close_variable=r"\)", parser=r"\w+", colon=r":\s*"
199
+ ):
200
+ open_variable = stream.expect("open_variable")
201
+ node: AstMacroLineVariable | AstInterpolation = delegate(
202
+ "macro_line_variable", stream
203
+ )
204
+
205
+ parser = None
206
+ if isinstance(node, AstMacroLineVariable):
207
+ name = node.value
208
+
209
+ if stream.get("colon"):
210
+ parser = stream.expect("parser").value
211
+
212
+ closed_variable = stream.expect("close_variable")
213
+ return set_location(
214
+ AstMacroArgument(name=name, parser=parser), open_variable, closed_variable
215
+ )
216
+
217
+
218
+ def parse_coordinate(stream: TokenStream):
219
+ """
220
+ Parses coordinates with support for macros
221
+
222
+ :param stream: The TokenStream instance
223
+ :type stream: TokenStream
224
+ """
225
+ with stream.syntax(modifier="[~^]"):
226
+ modifier_token = stream.get("modifier")
227
+
228
+ modifier = "absolute"
229
+
230
+ if modifier_token and modifier_token.value == "~":
231
+ modifier = "relative"
232
+ elif modifier_token and modifier_token.value == "^":
233
+ modifier = "local"
234
+
235
+ macro: AstMacroArgument = delegate("typed_macro", stream)
236
+
237
+ if macro.parser and macro.parser != "numeric":
238
+ raise ValueError(
239
+ f"Invalid macro type, received {macro.parser} expected numeric"
240
+ )
241
+
242
+ return set_location(
243
+ AstMacroCoordinateArgument(
244
+ name=macro.name, type=modifier, parser="numeric"
245
+ ),
246
+ modifier_token or macro.location,
247
+ macro.end_location,
248
+ )
@@ -0,0 +1,27 @@
1
+ from typing import Any
2
+
3
+ from nbtlib import Serializer as NbtSerializer
4
+
5
+ from .serialize import serialize_macro
6
+ from .typing import MacroTag
7
+
8
+
9
+ def apply_patches():
10
+ NbtSerializer.serialize_macro = serialize_macro # type: ignore
11
+
12
+ try:
13
+ import bolt_expressions.typing
14
+
15
+ convert_tag = bolt_expressions.typing.convert_tag
16
+
17
+ def convert_tag_with_macro(value: Any):
18
+ match value:
19
+ case MacroTag():
20
+ return value
21
+ case _:
22
+ return convert_tag(value)
23
+
24
+ bolt_expressions.typing.convert_tag = convert_tag_with_macro
25
+
26
+ except ImportError:
27
+ ...
@@ -0,0 +1,87 @@
1
+ from beet import Context
2
+ from bolt import Runtime
3
+ from mecha import AlternativeParser, Mecha, Parser
4
+
5
+ from .ast import (
6
+ AstGreedyWithMacro,
7
+ AstMacroExpression,
8
+ AstMacroNbtArgument,
9
+ AstMessageWithMacro,
10
+ AstNbtValueWithMacro,
11
+ AstStringWithMacro,
12
+ AstWordWithMacro,
13
+ )
14
+ from .codegen import MacroCodegen, ast_to_macro, make_macro_string
15
+ from .parse import (
16
+ MacroNbtPathParser,
17
+ MacroParser,
18
+ MacroRangeParser,
19
+ macro,
20
+ parse_coordinate,
21
+ parse_typed_macro,
22
+ )
23
+ from .patches import apply_patches
24
+ from .serialize import CommandSerializer, MacroConverter, MacroMutator
25
+
26
+ def get_parsers(parsers: dict[str, Parser]):
27
+ parse_nbt: Parser = parsers["nbt"]
28
+
29
+ def make_nbt_parser(parser: Parser):
30
+ return AlternativeParser(
31
+ [MacroParser(("nbt", "string"), AstMacroNbtArgument), parser]
32
+ )
33
+
34
+ new_parsers = {
35
+ "typed_macro": parse_typed_macro,
36
+ "bool": macro(parsers, "bool"),
37
+ "numeric": macro(parsers, "numeric"),
38
+ "coordinate": AlternativeParser([parsers["coordinate"], parse_coordinate]),
39
+ "time": macro(parsers, "time"),
40
+ "word": macro(parsers, "word", priority=True),
41
+ "phrase": macro(parsers, "phrase", priority=True),
42
+ "greedy": macro(parsers, "greedy", priority=True),
43
+ "entity": macro(parsers, "entity", priority=True),
44
+ "nbt": make_nbt_parser(parsers["nbt"]),
45
+ "nbt_compound_entry": make_nbt_parser(parsers["nbt_compound_entry"]),
46
+ "nbt_list_or_array_element": make_nbt_parser(
47
+ parsers["nbt_list_or_array_element"]
48
+ ),
49
+ "nbt_compound": make_nbt_parser(parsers["nbt_compound"]),
50
+ "nbt_path": AlternativeParser(
51
+ [parsers["nbt_path"], MacroNbtPathParser(nbt_compound_parser=parse_nbt)]
52
+ ),
53
+ "range": AlternativeParser([parsers["range"], MacroRangeParser()]),
54
+ "bolt:literal": macro(parsers, "bolt:literal", node_type=AstMacroExpression),
55
+ }
56
+
57
+ return new_parsers
58
+
59
+
60
+ conversions = {
61
+ "interpolate_phrase": AstStringWithMacro,
62
+ "interpolate_word": AstWordWithMacro,
63
+ "interpolate_greedy": AstGreedyWithMacro,
64
+ "interpolate_nbt": AstNbtValueWithMacro,
65
+ "interpolate_message": AstMessageWithMacro,
66
+ }
67
+
68
+
69
+ def beet_default(ctx: Context):
70
+ apply_patches()
71
+
72
+ mc = ctx.inject(Mecha)
73
+
74
+ mc.spec.parsers.update(get_parsers(mc.spec.parsers))
75
+ mc.serialize.extend(CommandSerializer(spec=mc.spec))
76
+ mc.steps.insert(0, MacroMutator())
77
+
78
+ runtime = ctx.inject(Runtime)
79
+
80
+ runtime.modules.codegen.extend(MacroCodegen())
81
+ runtime.helpers[ast_to_macro.__name__] = ast_to_macro
82
+ runtime.helpers[make_macro_string.__name__] = make_macro_string
83
+
84
+ for conversion, node_type in conversions.items():
85
+ runtime.helpers[conversion] = MacroConverter(
86
+ runtime.helpers[conversion], node_type
87
+ )
@@ -0,0 +1,172 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Callable, List
3
+
4
+ from beet.core.utils import required_field
5
+ from bolt import AstFormatString
6
+ from mecha import (
7
+ AstCommand,
8
+ AstNbtPath,
9
+ AstNbtPathKey,
10
+ AstNode,
11
+ CommandSpec,
12
+ MutatingReducer,
13
+ Visitor,
14
+ rule,
15
+ )
16
+ from mecha.utils import number_to_string
17
+ from nbtlib import Serializer as NbtSerializer
18
+ from tokenstream import set_location
19
+
20
+ from .ast import (
21
+ AstMacroArgument,
22
+ AstMacroCoordinateArgument,
23
+ AstMacroNbtArgument,
24
+ AstMacroNbtPathArgument,
25
+ AstMacroNbtPathKeyArgument,
26
+ AstMacroRange,
27
+ AstMacroStringWrapper,
28
+ )
29
+ from .typing import MacroRepresentation, MacroTag, StringWithMacro
30
+
31
+
32
+ def serialize_macro(_self: NbtSerializer, tag: MacroTag):
33
+ if tag.parser == "string":
34
+ return f'"$({tag.name})"'
35
+
36
+ return f"$({tag.name})"
37
+
38
+
39
+ @dataclass
40
+ class MacroConverter:
41
+ """
42
+ Used to convert interpolated strings with contain macros to the proper AstXWithMacro nodes
43
+ """
44
+
45
+ base_converter: Callable[[Any, AstNode], AstNode]
46
+ node_type: type
47
+
48
+ def __call__(self, obj: Any, node: AstNode) -> AstNode:
49
+ if isinstance(obj, StringWithMacro):
50
+ return self.node_type.from_value(obj)
51
+ return self.base_converter(obj, node)
52
+
53
+
54
+ @dataclass
55
+ class CommandSerializer(Visitor):
56
+ spec: CommandSpec = required_field()
57
+
58
+ @rule(AstCommand)
59
+ def command(self, node: AstCommand, result: list[str]):
60
+ prototype = self.spec.prototypes[node.identifier]
61
+ argument_index = 0
62
+
63
+ sep = ""
64
+
65
+ start_index = 0
66
+ # Scan backwards until we find the start of the current line
67
+ for i in range(len(result) - 1, -1, -1):
68
+ if result[i] == "\n":
69
+ start_index = i + 1
70
+ break
71
+
72
+ for token in prototype.signature:
73
+ result.append(sep)
74
+ sep = " "
75
+
76
+ # If token is a string then we can move on, literals can't contain macros
77
+ if isinstance(token, str):
78
+ result.append(token)
79
+ else:
80
+ argument = node.arguments[argument_index]
81
+
82
+ # Scan the argument for any MacroRepresentations
83
+ for child in argument.walk():
84
+ if isinstance(child, MacroRepresentation):
85
+ result[start_index] = "$"
86
+ break
87
+
88
+ yield argument
89
+ argument_index += 1
90
+
91
+ if result[start_index] == "$":
92
+ return
93
+
94
+ for i in range(start_index, len(result)):
95
+ if result[i] == "$(" and result[i + 2] == ")":
96
+ result[start_index] = "$"
97
+ break
98
+
99
+ def default(self, argument: AstMacroArgument, result: list[str]):
100
+ string = argument.parser == "string"
101
+ if string:
102
+ result.append('"')
103
+
104
+ result.append("$(")
105
+ result.append(argument.name)
106
+ result.append(")")
107
+
108
+ if string:
109
+ result.append('"')
110
+
111
+ @rule(AstMacroArgument, AstMacroNbtPathArgument, AstMacroNbtArgument)
112
+ def macro(self, argument: AstMacroArgument, result: list[str]):
113
+ self.default(argument, result)
114
+
115
+ @rule(AstMacroCoordinateArgument)
116
+ def coordinate(self, argument: AstMacroCoordinateArgument, result: list[str]):
117
+ if argument.type == "local":
118
+ result.append("^")
119
+ elif argument.type == "relative":
120
+ result.append("~")
121
+
122
+ self.default(argument, result)
123
+
124
+ @rule(AstMacroNbtPathKeyArgument)
125
+ def macro_path_key(self, argument: AstMacroNbtPathKeyArgument, result: list[str]):
126
+ self.default(argument, result)
127
+
128
+ @rule(AstNbtPath)
129
+ def nbt_path(self, node: AstNbtPath, result: List[str]):
130
+ sep = ""
131
+ for component in node.components:
132
+ if isinstance(
133
+ component,
134
+ (AstNbtPathKey, AstMacroNbtPathKeyArgument, AstMacroNbtPathArgument),
135
+ ):
136
+ result.append(sep)
137
+ sep = "."
138
+ yield component
139
+
140
+ @rule(AstMacroRange)
141
+ def range(self, node: AstMacroRange, result: list[str]):
142
+ if node.min == node.max and node.min is not None:
143
+ if isinstance(node.min, AstMacroArgument):
144
+ yield node.min
145
+ else:
146
+ result.append(number_to_string(node.min))
147
+ else:
148
+ if node.min is not None:
149
+ if isinstance(node.min, AstMacroArgument):
150
+ yield node.min
151
+ else:
152
+ result.append(number_to_string(node.min))
153
+
154
+ result.append("..")
155
+
156
+ if node.max is not None:
157
+ if isinstance(node.max, AstMacroArgument):
158
+ yield node.max
159
+ else:
160
+ result.append(number_to_string(node.max))
161
+
162
+
163
+ @dataclass
164
+ class MacroMutator(MutatingReducer):
165
+ @rule(AstFormatString)
166
+ def format_string(self, node: AstFormatString):
167
+ if any(map(lambda v: isinstance(v, AstMacroArgument), node.values)):
168
+ return set_location(
169
+ AstMacroStringWrapper(child=node), node.location, node.end_location
170
+ )
171
+
172
+ return node
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+
3
+ from beet.core.utils import required_field
4
+ from nbtlib import Base
5
+
6
+
7
+ class MacroRepresentation: ...
8
+
9
+
10
+ class StringWithMacro(str): ...
11
+
12
+
13
+ @dataclass
14
+ class MacroTag(Base):
15
+ name: str = required_field()
16
+ parser: str | None = required_field()
17
+
18
+ def __post_init__(self):
19
+ self.serializer = "macro"
20
+
21
+ def __str__(self):
22
+ return f"$({self.name})"