From fe46a259bc9a7166a0c62751d13502ac0f2dcbe6 Mon Sep 17 00:00:00 2001 From: Robert Miles Date: Fri, 30 Jan 2026 03:21:15 -0500 Subject: [PATCH 1/2] fix: properly encode unicode codepoints outside BMP The BMP (Basic Multilingual Plane) consists of all Unicode codepoints from U+0000 to U+FFFF. For characters in the BMP, you can encode them with a `\uXXXX` escape in both JSON and SNBT. However, characters outside of the BMP are encoded differently. In SNBT, you can use `\Uxxxxxxxx` (`\U` followed by 8 hex characters); in JSON, you need to encode them as UTF-16 surrogates. With this change, mecha can now properly encode these characters. Fixes #496. --- packages/mecha/src/mecha/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/mecha/src/mecha/utils.py b/packages/mecha/src/mecha/utils.py index 72ef53cc2..6abd37775 100644 --- a/packages/mecha/src/mecha/utils.py +++ b/packages/mecha/src/mecha/utils.py @@ -130,6 +130,9 @@ def handle_substitution(self, token: Token, match: "re.Match[str]") -> str: return chr(int(unicode_hex, 16)) return super().handle_substitution(token, match) + def encode_non_bmp_char(self, codepoint: int) -> str: + return f"\\U{codepoint:08x}" + def handle_quoting(self, value: str) -> str: value = super().handle_quoting(value) @@ -137,6 +140,8 @@ def escape_char(char: str) -> str: codepoint = ord(char) if codepoint < 128: return char + if codepoint > 65535: + return self.encode_non_bmp_char(codepoint) return f"\\u{codepoint:04x}" return "".join(escape_char(c) for c in value) @@ -156,6 +161,11 @@ class JsonQuoteHelper(QuoteHelperWithUnicode): } ) + def encode_non_bmp_char(self, codepoint: int) -> str: + low_surrogate = 0xD800 + ((codepoint - 0x10000) >> 10) + high_surrogate = 0xDC00 + (codepoint & 0x3FF) + return f"\\u{low_surrogate:04x}\\u{high_surrogate:04x}" + @dataclass class NbtQuoteHelper(QuoteHelperWithUnicode): From 45293f673bac999b7ca52d6d0aa161858d86811e Mon Sep 17 00:00:00 2001 From: edayot Date: Sun, 1 Feb 2026 12:19:23 +0100 Subject: [PATCH 2/2] adding a test --- packages/bolt/examples/weird_char/beet.yaml | 11 ++++ .../data/example/function/say.mcfunction | 24 +++++++ .../examples__build_weird_char__0.pack.md | 63 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 packages/bolt/examples/weird_char/beet.yaml create mode 100644 packages/bolt/examples/weird_char/data/example/function/say.mcfunction create mode 100644 packages/bolt/tests/snapshots/examples__build_weird_char__0.pack.md diff --git a/packages/bolt/examples/weird_char/beet.yaml b/packages/bolt/examples/weird_char/beet.yaml new file mode 100644 index 000000000..06ae5765e --- /dev/null +++ b/packages/bolt/examples/weird_char/beet.yaml @@ -0,0 +1,11 @@ + +data_pack: + load: . + +output: build + +require: + - bolt + +pipeline: + - mecha diff --git a/packages/bolt/examples/weird_char/data/example/function/say.mcfunction b/packages/bolt/examples/weird_char/data/example/function/say.mcfunction new file mode 100644 index 000000000..f040148b7 --- /dev/null +++ b/packages/bolt/examples/weird_char/data/example/function/say.mcfunction @@ -0,0 +1,24 @@ +say "🗡 " +tellraw @a {text: "🗡"} + + +recipe example:sharp_diamond { + "type":"minecraft:crafting_shapeless", + "ingredients":[["minecraft:diamond"]], + "result":{ + "id":"minecraft:diamond", + "components":{"minecraft:item_name":"🗡"} + } +} +TEAM_1 = "duels.team_1" +TEAM_2 = "duels.team_2" + +execute function ./load: + team add TEAM_1 "Duels Team 1" + team add TEAM_2 "Duels Team 2" + + for team in [TEAM_1, TEAM_2]: + team modify team friendlyFire false + team modify team prefix "🗡 " + team modify team seeFriendlyInvisibles false + diff --git a/packages/bolt/tests/snapshots/examples__build_weird_char__0.pack.md b/packages/bolt/tests/snapshots/examples__build_weird_char__0.pack.md new file mode 100644 index 000000000..88c4ea703 --- /dev/null +++ b/packages/bolt/tests/snapshots/examples__build_weird_char__0.pack.md @@ -0,0 +1,63 @@ +# Lectern snapshot + +## Data pack + +`@data_pack pack.mcmeta` + +```json +{ + "pack": { + "min_format": [ + 94, + 1 + ], + "max_format": [ + 94, + 1 + ], + "description": "" + } +} +``` + +### example + +`@function example:say` + +```mcfunction +say "🗡 " +tellraw @a {text: "\U0001f5e1"} +function example:load +``` + +`@function example:load` + +```mcfunction +team add duels.team_1 "Duels Team 1" +team add duels.team_2 "Duels Team 2" +team modify duels.team_1 friendlyFire false +team modify duels.team_1 prefix "\U0001f5e1 " +team modify duels.team_1 seeFriendlyInvisibles false +team modify duels.team_2 friendlyFire false +team modify duels.team_2 prefix "\U0001f5e1 " +team modify duels.team_2 seeFriendlyInvisibles false +``` + +`@recipe example:sharp_diamond` + +```json +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + [ + "minecraft:diamond" + ] + ], + "result": { + "id": "minecraft:diamond", + "components": { + "minecraft:item_name": "\ud83d\udde1" + } + } +} +```