22 "QuoteHelper" ,
33 "QuoteHelperWithUnicode" ,
44 "JsonQuoteHelper" ,
5+ "NbtQuoteHelper" ,
56 "InvalidEscapeSequence" ,
67 "normalize_whitespace" ,
78 "string_to_number" ,
1415import re
1516from dataclasses import dataclass , field
1617from pathlib import Path
17- from typing import Any , Dict , Optional , Union
18+ from typing import Any , Dict , List , Optional , Union
1819
1920from beet import File
2021from beet .core .utils import FileSystemPath
2526ESCAPE_REGEX = re .compile (r"\\." )
2627UNICODE_ESCAPE_REGEX = re .compile (r"\\(?:u([0-9a-fA-F]{4})|.)" )
2728AVOID_QUOTES_REGEX = re .compile (r"^[0-9A-Za-z_\.\+\-]+$" )
29+ QUOTE_REGEX = re .compile (r"\"|'" )
2830
2931WHITESPACE_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
127150class 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+
141189def underline_code (
142190 source : str ,
143191 location : SourceLocation ,
0 commit comments