1use std::fmt::Write as _;
4
5use simdnbt::owned::{NbtCompound, NbtTag};
6use text_components::{Modifier as _, TextComponent, format::Color};
7
8const MAX_DEPTH: usize = 64;
9const MAX_ARRAY_LENGTH: usize = 128;
10
11#[must_use]
16pub fn command_nbt_component(tag: &NbtTag, plain: bool) -> TextComponent {
17 let mut visitor = NbtComponentVisitor {
18 plain,
19 sort_keys: tracing::enabled!(tracing::Level::DEBUG),
20 result: TextComponent::new(),
21 };
22 visitor.visit(tag, 0);
23 visitor.result
24}
25
26struct NbtComponentVisitor {
27 plain: bool,
28 sort_keys: bool,
29 result: TextComponent,
30}
31
32impl NbtComponentVisitor {
33 fn visit(&mut self, tag: &NbtTag, depth: usize) {
34 match tag {
35 NbtTag::Byte(value) => {
36 self.number((*value).to_string());
37 self.number_type("b");
38 }
39 NbtTag::Short(value) => {
40 self.number(value.to_string());
41 self.number_type("s");
42 }
43 NbtTag::Int(value) => self.number(value.to_string()),
44 NbtTag::Long(value) => {
45 self.number(value.to_string());
46 self.number_type("L");
47 }
48 NbtTag::Float(value) => {
49 self.number(format!("{value:?}"));
50 self.number_type("f");
51 }
52 NbtTag::Double(value) => {
53 self.number(format!("{value:?}"));
54 self.number_type("d");
55 }
56 NbtTag::ByteArray(values) => self.byte_array(values),
57 NbtTag::String(value) => self.string(&value.to_string()),
58 NbtTag::List(values) => {
59 let values = values.as_nbt_tags();
60 self.list(&values, depth);
61 }
62 NbtTag::Compound(value) => self.compound(value, depth),
63 NbtTag::IntArray(values) => self.int_array(values),
64 NbtTag::LongArray(values) => self.long_array(values),
65 }
66 }
67
68 fn byte_array(&mut self, values: &[u8]) {
69 self.token("[");
70 self.number_type("B");
71 self.token(";");
72 for (index, value) in values.iter().take(MAX_ARRAY_LENGTH).enumerate() {
73 self.token(" ");
74 self.number((*value as i8).to_string());
75 self.number_type("b");
76 if index + 1 != values.len() {
77 self.token(",");
78 }
79 }
80 if values.len() > MAX_ARRAY_LENGTH {
81 self.folded();
82 }
83 self.token("]");
84 }
85
86 fn int_array(&mut self, values: &[i32]) {
87 self.token("[");
88 self.number_type("I");
89 self.token(";");
90 for (index, value) in values.iter().take(MAX_ARRAY_LENGTH).enumerate() {
91 self.token(" ");
92 self.number(value.to_string());
93 if index + 1 != values.len() {
94 self.token(",");
95 }
96 }
97 if values.len() > MAX_ARRAY_LENGTH {
98 self.folded();
99 }
100 self.token("]");
101 }
102
103 fn long_array(&mut self, values: &[i64]) {
104 self.token("[");
105 self.number_type("L");
106 self.token(";");
107 for (index, value) in values.iter().take(MAX_ARRAY_LENGTH).enumerate() {
108 self.token(" ");
109 self.number(value.to_string());
110 self.number_type("L");
111 if index + 1 != values.len() {
112 self.token(",");
113 }
114 }
115 if values.len() > MAX_ARRAY_LENGTH {
116 self.folded();
117 }
118 self.token("]");
119 }
120
121 fn list(&mut self, values: &[NbtTag], depth: usize) {
122 self.token("[");
123 if values.is_empty() {
124 self.token("]");
125 return;
126 }
127 if depth >= MAX_DEPTH {
128 self.folded();
129 self.token("]");
130 return;
131 }
132 for (index, value) in values.iter().enumerate() {
133 if index != 0 {
134 self.token(",");
135 self.token(" ");
136 }
137 self.visit(value, depth + 1);
138 }
139 self.token("]");
140 }
141
142 fn compound(&mut self, value: &NbtCompound, depth: usize) {
143 self.token("{");
144 if value.is_empty() {
145 self.token("}");
146 return;
147 }
148 if depth >= MAX_DEPTH {
149 self.folded();
150 self.token("}");
151 return;
152 }
153 let mut entries = value
154 .iter()
155 .map(|(key, tag)| (key.to_string(), tag))
156 .collect::<Vec<_>>();
157 if self.sort_keys {
158 entries.sort_by(|(left, _), (right, _)| left.encode_utf16().cmp(right.encode_utf16()));
159 }
160 for (index, (key, tag)) in entries.into_iter().enumerate() {
161 if index != 0 {
162 self.token(",");
163 self.token(" ");
164 }
165 self.key(&key);
166 self.token(":");
167 self.token(" ");
168 self.visit(tag, depth + 1);
169 }
170 self.token("}");
171 }
172
173 fn string(&mut self, value: &str) {
174 let (quote, escaped) = quote_and_escape(value);
175 self.token("e.to_string());
176 self.string_value(escaped);
177 self.token("e.to_string());
178 }
179
180 fn key(&mut self, value: &str) {
181 if is_simple_value(value) {
182 self.key_value(value.to_owned());
183 return;
184 }
185 let (quote, escaped) = quote_and_escape(value);
186 self.token("e.to_string());
187 self.key_value(escaped);
188 self.token("e.to_string());
189 }
190
191 fn token(&mut self, value: &str) {
192 self.result
193 .children
194 .push(TextComponent::plain(value.to_owned()));
195 }
196
197 fn folded(&mut self) {
198 self.styled("<...>".to_owned(), Color::Gray);
199 }
200
201 fn key_value(&mut self, value: String) {
202 self.styled(value, Color::Aqua);
203 }
204
205 fn string_value(&mut self, value: String) {
206 self.styled(value, Color::Green);
207 }
208
209 fn number(&mut self, value: String) {
210 self.styled(value, Color::Gold);
211 }
212
213 fn number_type(&mut self, value: &str) {
214 self.styled(value.to_owned(), Color::Red);
215 }
216
217 fn styled(&mut self, value: String, color: Color) {
218 let component = if self.plain {
219 TextComponent::plain(value)
220 } else {
221 TextComponent::plain(value).color(color)
222 };
223 self.result.children.push(component);
224 }
225}
226
227fn is_simple_value(value: &str) -> bool {
228 !value.is_empty()
229 && value
230 .bytes()
231 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'+' | b'-'))
232}
233
234fn quote_and_escape(value: &str) -> (char, String) {
235 let quote = value
236 .chars()
237 .find_map(|character| match character {
238 '"' => Some('\''),
239 '\'' => Some('"'),
240 _ => None,
241 })
242 .unwrap_or('"');
243 let mut escaped = String::new();
244 for character in value.chars() {
245 match character {
246 '\\' => escaped.push_str("\\\\"),
247 character if character == quote => {
248 escaped.push('\\');
249 escaped.push(character);
250 }
251 '\u{0008}' => escaped.push_str("\\b"),
252 '\t' => escaped.push_str("\\t"),
253 '\n' => escaped.push_str("\\n"),
254 '\u{000c}' => escaped.push_str("\\f"),
255 '\r' => escaped.push_str("\\r"),
256 character if character < ' ' => {
257 let _ = write!(escaped, "\\x{:02x}", u32::from(character));
258 }
259 character => escaped.push(character),
260 }
261 }
262 (quote, escaped)
263}
264
265#[cfg(test)]
266mod tests {
267 use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
268
269 use crate::text::DisplayResolutor;
270
271 use super::command_nbt_component;
272
273 fn plain(tag: &NbtTag) -> String {
274 command_nbt_component(tag, true).to_plain(&DisplayResolutor)
275 }
276
277 #[test]
278 fn renders_vanilla_suffixes_arrays_and_signed_bytes() {
279 assert_eq!(plain(&NbtTag::Long(7)), "7L");
280 assert_eq!(plain(&NbtTag::ByteArray(vec![255, 1])), "[B; -1b, 1b]");
281 assert_eq!(plain(&NbtTag::LongArray(vec![2, 3])), "[L; 2L, 3L]");
282 }
283
284 #[test]
285 fn quotes_and_escapes_strings_and_compound_keys() {
286 let mut compound = NbtCompound::new();
287 compound.insert("simple", NbtTag::String("can't \"stop\"\n".into()));
288 compound.insert("needs space", NbtTag::Int(1));
289
290 assert_eq!(
291 plain(&NbtTag::Compound(compound)),
292 "{simple: \"can't \\\"stop\\\"\\n\", \"needs space\": 1}"
293 );
294 }
295
296 #[test]
297 fn folds_nested_collections_at_vanillas_depth_limit() {
298 let mut tag = NbtTag::Compound(NbtCompound::new());
299 for _ in 0..=64 {
300 let mut parent = NbtCompound::new();
301 parent.insert("value", tag);
302 tag = NbtTag::Compound(parent);
303 }
304
305 assert!(plain(&tag).contains("{<...>}"));
306 }
307
308 #[test]
309 fn keeps_empty_collections_visible_at_vanillas_depth_limit() {
310 let mut tag = NbtTag::Compound(NbtCompound::new());
311 for _ in 0..64 {
312 let mut parent = NbtCompound::new();
313 parent.insert("value", tag);
314 tag = NbtTag::Compound(parent);
315 }
316
317 assert!(!plain(&tag).contains("<...>"));
318 }
319
320 #[test]
321 fn folds_arrays_after_vanillas_element_limit() {
322 let rendered = plain(&NbtTag::IntArray((0..129).collect()));
323
324 assert!(rendered.ends_with("127,<...>]"));
325 assert!(!rendered.contains(" 128"));
326 }
327
328 #[test]
329 fn renders_lists_with_vanillas_spacing() {
330 let tag = NbtTag::List(NbtList::String(vec!["one".into(), "two".into()]));
331
332 assert_eq!(plain(&tag), "[\"one\", \"two\"]");
333 }
334}