Skip to content
This repository was archived by the owner on Dec 8, 2025. It is now read-only.

Commit 6002605

Browse files
authored
feat: 1.21.5 text component and SNBT changes (#306)
1 parent e42cfe5 commit 6002605

File tree

44 files changed

+390
-251
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+390
-251
lines changed

mecha/ast.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,7 @@ class AstNbtByteArray(AstNbt):
832832
elements: AstChildren[AstNbt] = required_field()
833833

834834
parser = None
835+
array_prefix = "B"
835836

836837
def evaluate(self) -> Any:
837838
return ByteArray([element.evaluate() for element in self.elements]) # type: ignore
@@ -844,6 +845,7 @@ class AstNbtIntArray(AstNbt):
844845
elements: AstChildren[AstNbt] = required_field()
845846

846847
parser = None
848+
array_prefix = "I"
847849

848850
def evaluate(self) -> Any:
849851
return IntArray([element.evaluate() for element in self.elements]) # type: ignore
@@ -856,6 +858,7 @@ class AstNbtLongArray(AstNbt):
856858
elements: AstChildren[AstNbt] = required_field()
857859

858860
parser = None
861+
array_prefix = "L"
859862

860863
def evaluate(self) -> Any:
861864
return LongArray([element.evaluate() for element in self.elements]) # type: ignore

mecha/parse.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@
184184
from .config import CommandTree
185185
from .error import MechaError
186186
from .spec import CommandSpec, Parser
187-
from .utils import JsonQuoteHelper, QuoteHelper, string_to_number
187+
from .utils import JsonQuoteHelper, NbtQuoteHelper, QuoteHelper, string_to_number
188188

189189
NUMBER_PATTERN: str = r"-?(?:\d+\.?\d*|\.\d+)"
190190

@@ -493,8 +493,8 @@ def get_default_parsers() -> Dict[str, Parser]:
493493
]
494494
),
495495
"command:argument:minecraft:column_pos": delegate("column_pos"),
496-
"command:argument:minecraft:component": MultilineParser(delegate("json")),
497-
"command:argument:minecraft:style": MultilineParser(delegate("json")),
496+
"command:argument:minecraft:component": MultilineParser(delegate("nbt")),
497+
"command:argument:minecraft:style": MultilineParser(delegate("nbt")),
498498
"command:argument:minecraft:dimension": delegate("resource_location"),
499499
"command:argument:minecraft:entity": delegate("entity"),
500500
"command:argument:minecraft:entity_anchor": delegate("entity_anchor"),
@@ -583,6 +583,12 @@ def get_parsers(version: VersionNumber = LATEST_MINECRAFT_VERSION) -> Dict[str,
583583
if version < (1, 20):
584584
parsers["scoreboard_slot"] = BasicLiteralParser(AstLegacyScoreboardSlot)
585585

586+
if version < (1, 21):
587+
parsers["command:argument:minecraft:component"] = MultilineParser(
588+
delegate("json")
589+
)
590+
parsers["command:argument:minecraft:style"] = MultilineParser(delegate("json"))
591+
586592
return parsers
587593

588594

@@ -1165,13 +1171,7 @@ class NbtParser:
11651171
}
11661172
)
11671173

1168-
quote_helper: QuoteHelper = field(
1169-
default_factory=lambda: QuoteHelper(
1170-
escape_sequences={
1171-
r"\\": "\\",
1172-
}
1173-
)
1174-
)
1174+
quote_helper: QuoteHelper = field(default_factory=NbtQuoteHelper)
11751175

11761176
def __post_init__(self):
11771177
self.compound_entry_parser = self.parse_compound_entry
@@ -1234,24 +1234,16 @@ def __call__(self, stream: TokenStream) -> AstNbt:
12341234
node = AstNbtLongArray(elements=AstChildren(elements))
12351235
element_type = Long # type: ignore
12361236
msg = "Expected all elements to be long integers."
1237+
1238+
node = set_location(node, array, stream.current)
1239+
1240+
for element in node.elements:
1241+
if isinstance(element, AstNbtValue):
1242+
if type(element.value) is not element_type:
1243+
raise element.emit_error(InvalidSyntax(msg))
12371244
else:
12381245
node = AstNbtList(elements=AstChildren(elements))
1239-
element_type = None
1240-
msg = "Expected all elements to have the same type."
1241-
1242-
node = set_location(node, bracket or array, stream.current)
1243-
1244-
for element in node.elements:
1245-
if isinstance(element, AstNbtValue):
1246-
if not element_type:
1247-
element_type = type(element.value)
1248-
elif type(element.value) is not element_type:
1249-
raise element.emit_error(InvalidSyntax(msg))
1250-
elif isinstance(element, AstNbt): # type: ignore
1251-
if not element_type:
1252-
element_type = type(element)
1253-
elif type(element) is not element_type:
1254-
raise element.emit_error(InvalidSyntax(msg))
1246+
node = set_location(node, bracket, stream.current)
12551247

12561248
return node
12571249

mecha/serialize.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,17 @@
3232
AstMacroLineText,
3333
AstMacroLineVariable,
3434
AstMessage,
35-
AstNbt,
3635
AstNbtBool,
36+
AstNbtByteArray,
3737
AstNbtCompound,
38+
AstNbtCompoundKey,
39+
AstNbtIntArray,
40+
AstNbtList,
41+
AstNbtLongArray,
3842
AstNbtPath,
3943
AstNbtPathKey,
4044
AstNbtPathSubscript,
45+
AstNbtValue,
4146
AstNode,
4247
AstNumber,
4348
AstParticle,
@@ -61,9 +66,10 @@
6166
from .database import CompilationDatabase
6267
from .dispatch import Visitor, rule
6368
from .spec import CommandSpec
64-
from .utils import QuoteHelper, number_to_string
69+
from .utils import NbtQuoteHelper, QuoteHelper, number_to_string
6570

6671
REGEX_COMMENTS = re.compile(r"^(?:(\s*#.*)|.+)", re.MULTILINE)
72+
UNQUOTED_COMPOUND_KEY = re.compile(r"^[a-zA-Z0-9._+-]+$")
6773

6874

6975
class FormattingOptions(BaseModel):
@@ -97,6 +103,7 @@ class Serializer(Visitor):
97103
}
98104
)
99105
)
106+
nbt_quote_helper: QuoteHelper = field(default_factory=NbtQuoteHelper)
100107

101108
def __call__(self, node: AstNode, **kwargs: Any) -> str: # type: ignore
102109
result: List[str] = []
@@ -237,14 +244,68 @@ def json(self, node: AstJson, result: List[str]):
237244
)
238245
)
239246

240-
@rule(AstNbt)
241-
def nbt(self, node: AstNbt, result: List[str]):
242-
result.append(node.evaluate().snbt(compact=self.formatting.nbt_compact))
247+
@rule(AstNbtValue)
248+
def nbt_value(self, node: AstNbtValue, result: List[str]):
249+
if isinstance(node.value, str):
250+
result.append(self.nbt_quote_helper.quote_string(node.value))
251+
else:
252+
result.append(node.evaluate().snbt(compact=self.formatting.nbt_compact))
243253

244254
@rule(AstNbtBool)
245255
def nbt_bool(self, node: AstNbtBool, result: List[str]):
246256
result.append("true" if node.value else "false")
247257

258+
@rule(AstNbtList)
259+
def nbt_list(self, node: AstNbtList, result: List[str]):
260+
result.append("[")
261+
comma = "," if self.formatting.nbt_compact else ", "
262+
sep = ""
263+
for element in node.elements:
264+
result.append(sep)
265+
sep = comma
266+
yield element
267+
result.append("]")
268+
269+
@rule(AstNbtCompoundKey)
270+
def nbt_compound_key(self, node: AstNbtCompoundKey, result: List[str]):
271+
if UNQUOTED_COMPOUND_KEY.match(node.value):
272+
result.append(node.value)
273+
else:
274+
result.append(self.nbt_quote_helper.quote_string(node.value))
275+
276+
@rule(AstNbtCompound)
277+
def nbt_compound(self, node: AstNbtCompound, result: List[str]):
278+
result.append("{")
279+
comma, colon = (",", ":") if self.formatting.nbt_compact else (", ", ": ")
280+
sep = ""
281+
for entry in node.entries:
282+
result.append(sep)
283+
sep = comma
284+
yield entry.key
285+
result.append(colon)
286+
yield entry.value
287+
result.append("}")
288+
289+
@rule(AstNbtByteArray)
290+
@rule(AstNbtIntArray)
291+
@rule(AstNbtLongArray)
292+
def nbt_array(
293+
self,
294+
node: Union[AstNbtByteArray, AstNbtIntArray, AstNbtLongArray],
295+
result: List[str],
296+
):
297+
result.append("[")
298+
result.append(node.array_prefix)
299+
semicolon = ";" if self.formatting.nbt_compact else "; "
300+
result.append(semicolon)
301+
comma = "," if self.formatting.nbt_compact else ", "
302+
sep = ""
303+
for element in node.elements:
304+
result.append(sep)
305+
sep = comma
306+
yield element
307+
result.append("]")
308+
248309
@rule(AstResourceLocation)
249310
def resource_location(self, node: AstResourceLocation, result: List[str]):
250311
result.append(node.get_value())

mecha/utils.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"QuoteHelper",
33
"QuoteHelperWithUnicode",
44
"JsonQuoteHelper",
5+
"NbtQuoteHelper",
56
"InvalidEscapeSequence",
67
"normalize_whitespace",
78
"string_to_number",
@@ -14,7 +15,7 @@
1415
import re
1516
from dataclasses import dataclass, field
1617
from pathlib import Path
17-
from typing import Any, Dict, Optional, Union
18+
from typing import Any, Dict, List, Optional, Union
1819

1920
from beet import File
2021
from beet.core.utils import FileSystemPath
@@ -25,6 +26,7 @@
2526
ESCAPE_REGEX = re.compile(r"\\.")
2627
UNICODE_ESCAPE_REGEX = re.compile(r"\\(?:u([0-9a-fA-F]{4})|.)")
2728
AVOID_QUOTES_REGEX = re.compile(r"^[0-9A-Za-z_\.\+\-]+$")
29+
QUOTE_REGEX = re.compile(r"\"|'")
2830

2931
WHITESPACE_REGEX = re.compile(r"\s+")
3032

@@ -70,10 +72,15 @@ class QuoteHelper:
7072
avoid_quotes_regex: "re.Pattern[str]" = AVOID_QUOTES_REGEX
7173

7274
escape_sequences: Dict[str, str] = field(default_factory=dict)
75+
unquote_only_escape_characters: List[str] = field(default_factory=list)
7376
escape_characters: Dict[str, str] = field(init=False)
7477

7578
def __post_init__(self):
76-
self.escape_characters = {v: k for k, v in self.escape_sequences.items()}
79+
self.escape_characters = {
80+
v: k
81+
for k, v in self.escape_sequences.items()
82+
if not k in self.unquote_only_escape_characters
83+
}
7784

7885
def unquote_string(self, token: Token) -> str:
7986
"""Remove quotes and substitute escaped characters."""
@@ -106,9 +113,14 @@ def quote_string(self, value: str, quote: str = '"') -> str:
106113
"""Wrap the string in quotes if it can't be represented unquoted."""
107114
if self.avoid_quotes_regex.match(value):
108115
return value
116+
value = self.handle_quoting(value)
117+
return quote + value.replace(quote, "\\" + quote) + quote
118+
119+
def handle_quoting(self, value: str) -> str:
120+
"""Handle escape characters during quoting."""
109121
for match, seq in self.escape_characters.items():
110122
value = value.replace(match, seq)
111-
return quote + value.replace(quote, "\\" + quote) + quote
123+
return value
112124

113125

114126
@dataclass
@@ -122,6 +134,17 @@ def handle_substitution(self, token: Token, match: "re.Match[str]") -> str:
122134
return chr(int(unicode_hex, 16))
123135
return super().handle_substitution(token, match)
124136

137+
def handle_quoting(self, value: str) -> str:
138+
value = super().handle_quoting(value)
139+
140+
def escape_char(char: str) -> str:
141+
codepoint = ord(char)
142+
if codepoint < 128:
143+
return char
144+
return f"\\u{codepoint:04x}"
145+
146+
return "".join(escape_char(c) for c in value)
147+
125148

126149
@dataclass
127150
class JsonQuoteHelper(QuoteHelperWithUnicode):
@@ -138,6 +161,31 @@ class JsonQuoteHelper(QuoteHelperWithUnicode):
138161
)
139162

140163

164+
@dataclass
165+
class NbtQuoteHelper(QuoteHelperWithUnicode):
166+
"""Quote helper used for snbt."""
167+
168+
escape_sequences: Dict[str, str] = field(
169+
default_factory=lambda: {
170+
r"\\": "\\",
171+
r"\b": "\b",
172+
r"\f": "\f",
173+
r"\n": "\n",
174+
r"\r": "\r",
175+
r"\s": " ",
176+
r"\t": "\t",
177+
}
178+
)
179+
unquote_only_escape_characters: List[str] = field(default_factory=lambda: [r"\s"])
180+
181+
def quote_string(self, value: str, quote: Optional[str] = None) -> str:
182+
if not quote:
183+
found = QUOTE_REGEX.search(value)
184+
quote = "'" if found and found.group() == '"' else '"'
185+
value = super().handle_quoting(value)
186+
return quote + value.replace(quote, "\\" + quote) + quote
187+
188+
141189
def underline_code(
142190
source: str,
143191
location: SourceLocation,

tests/resources/argument_examples.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,13 @@
145145
"{\"text\":\"hello world\"}",
146146
"[\"\"]",
147147
"\"\\u2588\"",
148-
"{\"\\u2588\\\"\\n\":[123]}"
148+
"{\"\\u2588\\\"\\n\":[123]}",
149+
"wat"
149150
]
150151
},
151152
{
152153
"invalid": true,
153-
"examples": ["wat", "{\"hey\": ]}"]
154+
"examples": ["{\"hey\": ]}"]
154155
}
155156
],
156157
"minecraft:dimension": [
@@ -415,18 +416,18 @@
415416
"[L;1l,2l]",
416417
"[a, b, c]",
417418
"[[], [1], [foo]]",
418-
"[[],[[[[[[[[[[],[[[[5,1]],[]]]]]]]]]]],[[[[]]]]]]"
419+
"[[],[[[[[[[[[[],[[[[5,1]],[]]]]]]]]]]],[[[[]]]]]]",
420+
"\"\\n\"",
421+
"[a,1]",
422+
"[[],[],1b]"
419423
]
420424
},
421425
{
422426
"invalid": true,
423427
"examples": [
424428
"\"\\\"",
425-
"\"\\n\"",
426429
"\"\\\\\\\"",
427430
"{\"\\\":1}",
428-
"[a,1]",
429-
"[[],[],1b]",
430431
"[B;5l,4l,3]",
431432
"[I;5l,4l,3]",
432433
"[L;5l,4l,3]",

tests/snapshots/examples__build_basic_formatting__0.pack.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
```mcfunction
2121
say this is a test
2222
execute as @a[nbt={SelectedItem:{id:"minecraft:diamond",Count:64b}}] at @s run setblock ~ ~ ~ repeater[delay=3,facing=south]
23-
tellraw @a ["",{"text":"hello","color":"red"},"not ascii: \u00b6"]
23+
tellraw @a ["",{text:"hello",color:"red"},"not ascii: \u00b6"]
2424
say goodbye
2525
```
2626

@@ -29,7 +29,7 @@ say goodbye
2929
```mcfunction
3030
say this is a test
3131
execute as @a[nbt={SelectedItem: {id: "minecraft:diamond", Count: 64b}}] at @s run setblock ~ ~ ~ repeater[delay=3, facing=south]
32-
tellraw @a ["", {"text": "hello", "color": "red"}, "not ascii: \u00b6"]
32+
tellraw @a ["", {text: "hello", color: "red"}, "not ascii: \u00b6"]
3333
say goodbye
3434
```
3535

@@ -44,7 +44,7 @@ execute as @a[nbt={SelectedItem: {id: "minecraft:diamond", Count: 64b}}] at @s r
4444
4545
4646
47-
tellraw @a ["", {"text": "hello", "color": "red"}, "not ascii: \u00b6"]
47+
tellraw @a ["", {text: "hello", color: "red"}, "not ascii: \u00b6"]
4848
4949
5050
@@ -63,7 +63,7 @@ execute as @a[nbt={SelectedItem:{id:"minecraft:diamond",Count:64b}}] at @s run s
6363
6464
6565
66-
tellraw @a ["", {"color": "red", "text": "hello"}, "not ascii: "]
66+
tellraw @a ["",{text:"hello",color:"red"},"not ascii: \u00b6"]
6767
6868
6969

0 commit comments

Comments
 (0)