snbt-js 1.0.0

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,24 @@
1
+ name: Node.js CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+
9
+ strategy:
10
+ matrix:
11
+ node-version: [14.x, 16.x, 18.x]
12
+
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - name: Use Node.js ${{ matrix.node-version }}
16
+ uses: actions/setup-node@v3
17
+ with:
18
+ node-version: ${{ matrix.node-version }}
19
+ - run: |
20
+ if [ ! -f package-lock.json ]; then
21
+ npm install
22
+ fi
23
+ - run: npm ci
24
+ - run: npm test
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 myworldzycpc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # SNBT-JS
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![npm version](https://img.shields.io/npm/v/snbt-js.svg)](https://www.npmjs.com/package/snbt-js)
5
+ [![Build Status](https://github.com/myworldzycpc/snbt-js/actions/workflows/tests.yml/badge.svg)](https://github.com/myworldzycpc/snbt-js/actions)
6
+
7
+ *(Formerly known as nbtcoder.js)*
8
+
9
+ A robust JavaScript library for parsing, manipulating, and serializing Minecraft's NBT (Named Binary Tag) format.
10
+ Supports SNBT (Stringified NBT) with zero dependencies.
11
+
12
+ ## Features
13
+
14
+ - 🚀 Full NBT specification implementation (Tags 1-12)
15
+ - ✨ Intuitive API for creating and modifying NBT structures
16
+ - 🔍 Path-based access using dot/bracket notation (`player.inventory[0].id`)
17
+ - 📝 Syntax-highlighted SNBT output for debugging
18
+ - ⚠️ Strict type validation and error handling
19
+ - 🌐 Browser and Node.js compatible
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install nbt-js
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Parsing SNBT
30
+ ```javascript
31
+ const { parseNbtString } = require('nbt-js');
32
+
33
+ const nbtData = parseNbtString(`
34
+ {
35
+ player: {
36
+ name: "Steve",
37
+ health: 20.0f,
38
+ inventory: [
39
+ { id: "minecraft:diamond_sword", Count: 1b },
40
+ { id: "minecraft:cooked_beef", Count: 8b }
41
+ ],
42
+ position: [I; 125, 64, -312]
43
+ }
44
+ }`);
45
+
46
+ console.log(nbtData.get('player.name').value); // "Steve"
47
+ ```
48
+
49
+ ### Creating NBT Programmatically
50
+ ```javascript
51
+ const { NbtObject, NbtList, NbtString, NbtNumber } = require('nbt-js');
52
+
53
+ const player = new NbtObject({
54
+ name: new NbtString('Alex'),
55
+ health: new NbtNumber(15.5, 'f'),
56
+ skills: new NbtList([
57
+ new NbtString('mining'),
58
+ new NbtString('farming')
59
+ ])
60
+ });
61
+
62
+ // Add armor items
63
+ player.addChild('armor', new NbtList([
64
+ new NbtObject({ id: new NbtString('minecraft:iron_helmet') }),
65
+ new NbtObject({ id: new NbtString('minecraft:diamond_chestplate') })
66
+ ]));
67
+
68
+ console.log(player.text(true)); // Pretty-printed with syntax highlighting
69
+ ```
70
+
71
+ ### Modifying NBT
72
+ ```javascript
73
+ // Update player position
74
+ player.set(['position', 0], new NbtNumber(130)); // X coordinate
75
+ player.set(['position', 1], new NbtNumber(65)); // Y coordinate
76
+ player.set(['position', 2], new NbtNumber(-305)); // Z coordinate
77
+
78
+ // Add new item to inventory
79
+ const inventory = player.get('inventory');
80
+ inventory.addChild(new NbtObject({
81
+ id: new NbtString('minecraft:golden_apple'),
82
+ Count: new NbtNumber(3, 'b')
83
+ }));
84
+ ```
85
+
86
+ ## API Documentation
87
+
88
+ [View full API documentation](./docs/api-reference.md)
89
+
90
+ ## Contributing
91
+
92
+ Contributions are welcome! Please read the [contribution guidelines](CONTRIBUTING.md) before submitting pull requests.
93
+
94
+ ## License
95
+
96
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
97
+
98
+ ## Related Projects
99
+
100
+ - [Player Data Analyser](https://github.com/myworldzycpc/player-data-analyser) - Web-based player data analyser using this library
101
+ - [Bio Generator](https://github.com/TheRedMaker/theredmaker.github.io/tree/main/Biogenerator) - Summon command generator utilizing snbt-js (Formerly known as nbtcoder.js)
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "snbt-js",
3
+ "version": "1.0.0",
4
+ "description": "JavaScript library for parsing and manipulating Minecraft SNBT data",
5
+ "main": "src/snbt.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "mocha test/*.js"
9
+ },
10
+ "keywords": [
11
+ "minecraft",
12
+ "nbt",
13
+ "snbt",
14
+ "parser",
15
+ "javascript"
16
+ ],
17
+ "author": "myworldzycpc",
18
+ "license": "MIT",
19
+ "devDependencies": {
20
+ "mocha": "^10.0.0",
21
+ "chai": "^4.3.7"
22
+ }
23
+ }
package/src/snbt.js ADDED
@@ -0,0 +1,781 @@
1
+ const gettype = Object.prototype.toString;
2
+
3
+ export function changeToFloat(val) {
4
+ if ((val / 1).toString().includes(".")) {
5
+ return (val / 1);
6
+ } else {
7
+ return (val / 1).toFixed(1);
8
+ };
9
+ };
10
+
11
+ export function highlightCode(text, type) {
12
+ if (type == "key") {
13
+ return `<span style="color:aqua">${text}</span>`
14
+ } else if (type == "str") {
15
+ return `<span style="color:rgb(84,351,84)">${text}</span>`
16
+ } else if (type == "num") {
17
+ return `<span style="color:orange">${text}</span>`
18
+ } else if (type == "bool") {
19
+ return `<span style="color:yellow">${text}</span>`
20
+ } else if (type == "unit") {
21
+ return `<span style="color:red">${text}</span>`
22
+ };
23
+
24
+ }
25
+
26
+ export class NbtObject {
27
+ constructor(childsal) {
28
+ this.childs = {};
29
+ this.addChild = function (key, value) {
30
+ this.childs[key] = value;
31
+ };
32
+ this.isempty = function () {
33
+ return (Object.keys(this.childs).length == 0 ? true : false);
34
+ };
35
+ this.get = function () {
36
+ var args = Array.prototype.slice.call(arguments);
37
+ if (gettype.call(args[0]) == "[object Array]") {
38
+ args = args[0];
39
+ };
40
+ if (args.length == 1) {
41
+ args = parsePath(args[0])
42
+ }
43
+ if (args.length == 1) {
44
+ return this.childs[args[0]];
45
+ } else if (args.length > 1) {
46
+ return this.childs[args[0]].get(args.slice(1));
47
+ } else {
48
+ return this;
49
+ };
50
+ };
51
+ this.set = function (index, value) {
52
+ if (["[object String]", "[object Number]"].includes(gettype.call(index))) {
53
+ this.childs[index] = value;
54
+ } else if (gettype.call(index) == "[object Array]") {
55
+ if (index.length == 1) {
56
+ this.childs[index[0]] = value;
57
+ } else if (index.length > 1) {
58
+ this.get(index.slice(0, -1)).set(index.slice(-1), value);
59
+ };
60
+ };
61
+ };
62
+ this.text = function (ispretty) {
63
+ var tl = [];
64
+ for (var i in this.childs) {
65
+ tl.push(`${ispretty ? highlightCode(i, "key") : i}: ${this.childs[i].text(ispretty)}`);
66
+ };
67
+ return `{${tl.join(", ")}}`;
68
+ };
69
+ if (childsal) {
70
+ for (var index in childsal) {
71
+ this.addChild(index, childsal[index])
72
+ };
73
+ };
74
+ };
75
+ };
76
+
77
+ export class NbtList {
78
+ constructor(childsal) {
79
+ this.childs = [];
80
+ this.addChild = function (value) {
81
+ this.childs.push(value)
82
+ };
83
+ this.isempty = function () {
84
+ return (this.childs.length == 0 ? true : false);
85
+ };
86
+ this.get = function () {
87
+ var args = Array.prototype.slice.call(arguments);
88
+ if (gettype.call(args[0]) == "[object Array]") {
89
+ args = args[0];
90
+ };
91
+ if (args.length == 1) {
92
+ args = parsePath(args[0])
93
+ }
94
+ if (args.length == 1) {
95
+ return this.childs[args[0]];
96
+ } else if (args.length > 1) {
97
+ return this.childs[args[0]].get(args.slice(1));
98
+ } else {
99
+ return this;
100
+ };
101
+ };
102
+ this.set = function (index, value) {
103
+ if (["[object String]", "[object Number]"].includes(gettype.call(index))) {
104
+ this.childs[index] = value;
105
+ } else if (gettype.call(index) == "[object Array]") {
106
+ if (index.length == 1) {
107
+ this.childs[index[0]] = value;
108
+ } else if (index.length > 1) {
109
+ this.get(index.slice(0, -1)).set(index.slice(-1), value);
110
+ };
111
+ };
112
+ };
113
+ this.text = function (ispretty) {
114
+ var tl = [];
115
+ for (var i = 0; i < this.childs.length; i++) {
116
+ tl.push(this.childs[i].text(ispretty));
117
+ }
118
+ return `[${tl.join(", ")}]`;
119
+ };
120
+ if (childsal) {
121
+ for (var i = 0; i < childsal.length; i++) {
122
+ this.addChild(childsal[i]);
123
+ }
124
+ };
125
+ };
126
+ };
127
+
128
+ export class NbtIntArray {
129
+ constructor(childsal) {
130
+ this.childs = [];
131
+ this.addChild = function (value) {
132
+ if (value instanceof NbtNumber && value.unit == "") {
133
+ this.childs.push(value)
134
+ } else {
135
+ throw new Error("NbtIntArray only accept NbtNumber without unit")
136
+ }
137
+ };
138
+ this.isempty = function () {
139
+ return (this.childs.length == 0 ? true : false);
140
+ };
141
+ this.get = function () {
142
+ var args = Array.prototype.slice.call(arguments);
143
+ if (gettype.call(args[0]) == "[object Array]") {
144
+ args = args[0];
145
+ };
146
+ if (args.length == 1) {
147
+ args = parsePath(args[0])
148
+ }
149
+ if (args.length == 1) {
150
+ return this.childs[args[0]];
151
+ } else if (args.length > 1) {
152
+ throw new Error("NbtIntArray only accept one index")
153
+ } else {
154
+ return this;
155
+ };
156
+ };
157
+ this.set = function (index, value) {
158
+ if (index.length == 1) {
159
+ if (value instanceof NbtNumber && value.unit == "") {
160
+ this.childs[index[0]] = value;
161
+ } else {
162
+ throw new Error("NbtIntArray only accept NbtNumber without unit")
163
+ }
164
+ } else if (index.length > 1) {
165
+ throw new Error("NbtIntArray only accept one index")
166
+ };
167
+ };
168
+ this.text = function (ispretty) {
169
+ var tl = [];
170
+ for (var i = 0; i < this.childs.length; i++) {
171
+ tl.push(this.childs[i].text(ispretty));
172
+ }
173
+ return `[I; ${tl.join(", ")}]`;
174
+ };
175
+ if (childsal) {
176
+ for (var i = 0; i < childsal.length; i++) {
177
+ this.addChild(childsal[i]);
178
+ }
179
+ };
180
+ };
181
+ };
182
+
183
+ export class NbtNumber {
184
+ constructor(value, unit = "") {
185
+ unit = unit.toLowerCase();
186
+ if (unit == "b") {
187
+ if (Math.round(value) > 127) {
188
+ this.value = 127;
189
+ } else if (Math.round(value) < -128) {
190
+ this.value = -128;
191
+ } else {
192
+ this.value = Math.round(value);
193
+ }
194
+
195
+ } else if (unit == "s") {
196
+ if (Math.round(value) > 32767) {
197
+ this.value = 32767;
198
+ } else if (Math.round(value) < -32768) {
199
+ this.value = -32768;
200
+ } else {
201
+ this.value = Math.round(value);
202
+ }
203
+ }else if (unit == "l") {
204
+ if (Math.round(value) > 9223372036854775807n) {
205
+ this.value = 9223372036854775807n;
206
+ } else if (Math.round(value) < -9223372036854775808n) {
207
+ this.value = -9223372036854775808n;
208
+ } else {
209
+ this.value = Math.round(value);
210
+ }
211
+ } else if (unit == "d") {
212
+ this.value = changeToFloat(value);
213
+ } else if (unit == "f") {
214
+ this.value = changeToFloat(value);
215
+ } else if (unit == "") {
216
+ this.value = Math.round(value);
217
+ } else {
218
+ this.value = value;
219
+ };
220
+ this.unit = unit;
221
+ this.text = function (ispretty) {
222
+ if (ispretty) {
223
+ return `${highlightCode(this.value.toString(), "num")}${unit ? highlightCode(this.unit, "unit") : ""}`
224
+ } else {
225
+ return `${this.value}${unit ? this.unit : ""}`
226
+ };
227
+ };
228
+ };
229
+ };
230
+
231
+ export class NbtString {
232
+ constructor(value) {
233
+ this.value = value;
234
+ this.text = function (ispretty) {
235
+ // 转义特殊字符
236
+ let escapedValue = this.value
237
+ .replace(/\\/g, '\\\\')
238
+ .replace(/'/g, "\\'")
239
+ .replace(/\n/g, "\\n")
240
+ .replace(/\r/g, "\\r")
241
+ .replace(/\t/g, "\\t");
242
+
243
+ if (ispretty) {
244
+ return highlightCode(`'${escapedValue}'`, "str");
245
+ } else {
246
+ return `'${escapedValue}'`;
247
+ }
248
+ };
249
+ };
250
+ };
251
+
252
+ export class NbtBool {
253
+ constructor(value) {
254
+ this.value = value ? "true" : "false";
255
+ this.text = function (ispretty) {
256
+ if (ispretty) {
257
+ return highlightCode(`${this.value}`, "bool")
258
+ } else {
259
+ return `${this.value}`
260
+ };
261
+ };
262
+ };
263
+ };
264
+
265
+ export class NbtNull {
266
+ constructor() {
267
+ this.value = null;
268
+ }
269
+ text = function (ispretty) {
270
+ if (ispretty) {
271
+ return highlightCode("null", "bool"); // 使用布尔值的样式
272
+ } else {
273
+ return "null";
274
+ }
275
+ };
276
+ }
277
+
278
+ export function arrangementNbt(str) {
279
+ str = str.replace(/(: *)([0-9\.]+)([bfdis])/g, '$1new NbtNumber($2,"$3")');
280
+ str = str.replace(/(, *)([0-9\.]+)([bfdis])( *[,\]])/g, '$1new NbtNumber($2,"$3")$4');
281
+ str = str.replace(/(\[ *)([0-9\.]+)([bfdis])/g, '$1new NbtNumber($2,"$3")');
282
+
283
+ return str;
284
+ }
285
+
286
+ /**
287
+ *
288
+ * @deprecated
289
+ * @param {*} str
290
+ * @returns
291
+ */
292
+ export function decodeNbtStr(str) {
293
+ jsObj = eval("obj=" + arrangementNbt(str));
294
+ return changeObj(jsObj);
295
+ }
296
+
297
+ export function changeObj(jsObj) {
298
+ if (gettype.call(jsObj) == "[object String]") {
299
+ return new NbtString(jsObj);
300
+ } else if (gettype.call(jsObj) == "[object Boolean]") {
301
+ return new NbtBool(jsObj);
302
+ } else if (gettype.call(jsObj) == "[object Number]") {
303
+ return new NbtNumber(jsObj);
304
+ } else if (gettype.call(jsObj) == "[object Object]") {
305
+ if (jsObj instanceof NbtNumber) {
306
+ return jsObj;
307
+ } else {
308
+ var a = new NbtObject();
309
+ for (var i in jsObj) {
310
+ a.addChild(i, changeObj(jsObj[i]))
311
+ }
312
+ return a;
313
+ }
314
+ } else if (gettype.call(jsObj) == "[object Array]") {
315
+ var a = new NbtList();
316
+ for (var i in jsObj) {
317
+ a.addChild(changeObj(jsObj[i]));
318
+ }
319
+ return a;
320
+ }
321
+ }
322
+
323
+ export function parsePath(path) {
324
+ const tokens = [];
325
+ let current = '';
326
+ let inQuote = false;
327
+ let inBracket = false;
328
+ let hasQuoteInBracket = false;
329
+
330
+ for (let i = 0; i < path.length; i++) {
331
+ const char = path[i];
332
+
333
+ if (inQuote) {
334
+ if (char === '"') {
335
+ inQuote = false;
336
+ if (inBracket) {
337
+ hasQuoteInBracket = true;
338
+ }
339
+ } else {
340
+ current += char;
341
+ }
342
+ } else {
343
+ if (char === '"') {
344
+ if (current !== '') {
345
+ throw new Error('Unexpected double quote');
346
+ }
347
+ inQuote = true;
348
+ } else if (char === '[') {
349
+ if (inBracket) {
350
+ throw new Error('Nested brackets are not allowed');
351
+ }
352
+ if (current !== '') {
353
+ tokens.push(current);
354
+ current = '';
355
+ } else if (tokens.length === 0 || typeof tokens[tokens.length - 1] === 'number') {
356
+ throw new Error('Unexpected opening bracket');
357
+ }
358
+ inBracket = true;
359
+ hasQuoteInBracket = false;
360
+ } else if (char === ']') {
361
+ if (!inBracket) {
362
+ throw new Error('Unexpected closing bracket');
363
+ }
364
+ let content = current.trim();
365
+ if (content === '') {
366
+ throw new Error('Empty brackets are not allowed');
367
+ }
368
+ if (hasQuoteInBracket) {
369
+ tokens.push(content);
370
+ } else {
371
+ if (!/^\d+$/.test(content)) {
372
+ throw new Error('Brackets must contain only numbers or quoted strings');
373
+ }
374
+ tokens.push(parseInt(content, 10));
375
+ }
376
+ current = '';
377
+ inBracket = false;
378
+ } else if (char === '.') {
379
+ if (inBracket) {
380
+ throw new Error('Dot not allowed inside brackets');
381
+ }
382
+ if (current !== '') {
383
+ tokens.push(current);
384
+ current = '';
385
+ } else if (i === 0 || path[i - 1] === '.') {
386
+ throw new Error('Unexpected dot');
387
+ }
388
+ } else {
389
+ if (inBracket && !hasQuoteInBracket) {
390
+ if (char === ' ' || char === '\t') {
391
+ // 允许空格
392
+ } else if (char >= '0' && char <= '9') {
393
+ current += char;
394
+ } else {
395
+ throw new Error(`Invalid character in bracket: '${char}'`);
396
+ }
397
+ } else {
398
+ current += char;
399
+ }
400
+ }
401
+ }
402
+ }
403
+
404
+ // 结束后的状态检查
405
+ if (inQuote) throw new Error('Unclosed quote');
406
+ if (inBracket) throw new Error('Unclosed bracket');
407
+ if (current !== '') tokens.push(current);
408
+
409
+ return tokens;
410
+ }
411
+
412
+ export function parseNbtString(str) {
413
+ let index = 0;
414
+ const length = str.length;
415
+
416
+ // 确保整个字符串被解析
417
+ function ensureEnd() {
418
+ skipWhitespace();
419
+ if (index < length) {
420
+ throw new Error(`Unexpected character: '${str[index]}'. Expected end of input.`);
421
+ }
422
+ }
423
+
424
+ function parseValue() {
425
+ skipWhitespace();
426
+ if (index >= length) {
427
+ throw new Error("Unexpected end of input");
428
+ }
429
+
430
+ const char = str[index];
431
+
432
+ if (char === '{') {
433
+ const obj = parseObject();
434
+ return obj;
435
+ } else if (char === '[') {
436
+ const arr = parseArray();
437
+ return arr;
438
+ } else if (char === "'" || char === '"') {
439
+ return parseString(char);
440
+ } else if (/[0-9-]/.test(char)) {
441
+ return parseNumber();
442
+ } else if (char === 't' && index + 4 <= length && str.substr(index, 4) === "true") {
443
+ index += 4;
444
+ return new NbtBool(true);
445
+ } else if (char === 'f' && index + 5 <= length && str.substr(index, 5) === "false") {
446
+ index += 5;
447
+ return new NbtBool(false);
448
+ } else if (char === 'n' && index + 4 <= length && str.substr(index, 4) === "null") {
449
+ index += 4;
450
+ return new NbtNull();
451
+ }
452
+ throw new Error(`Unexpected character: ${char}`);
453
+ }
454
+
455
+ function parseObject() {
456
+ index++; // 跳过 '{'
457
+ const obj = new NbtObject();
458
+ let first = true;
459
+ let expectComma = false;
460
+
461
+ while (index < length) {
462
+ skipWhitespace();
463
+
464
+ // 检查是否结束
465
+ if (str[index] === '}') {
466
+ index++;
467
+ return obj;
468
+ }
469
+
470
+ // 检查逗号分隔符
471
+ if (expectComma) {
472
+ if (str[index] === ',') {
473
+ index++;
474
+ skipWhitespace();
475
+ // 允许尾随逗号: 检查逗号后是否直接是结束符
476
+ if (str[index] === '}') continue;
477
+ } else {
478
+ throw new Error(`Expected comma`);
479
+ }
480
+ }
481
+
482
+ const key = parseKey();
483
+ skipWhitespace();
484
+
485
+ // 检查键是否以数字开头(非引号包裹时)
486
+ if (!/^['"]/.test(key) && /^\d/.test(key)) {
487
+ throw new Error(`Key cannot start with a digit: ${key}`);
488
+ }
489
+
490
+ if (str[index] !== ':') {
491
+ throw new Error(`Expected colon`);
492
+ }
493
+ index++; // 跳过 ':'
494
+ skipWhitespace();
495
+
496
+ const value = parseValue();
497
+ obj.addChild(key, value);
498
+
499
+ first = false;
500
+ expectComma = true; // 下一个元素前需要逗号
501
+ skipWhitespace();
502
+ }
503
+ throw new Error("Unterminated object");
504
+ }
505
+
506
+ function parseArray() {
507
+ index++; // 跳过 '['
508
+ // 检查是否IntArray
509
+ skipWhitespace();
510
+ let arr;
511
+ if (str[index] === 'I') {
512
+ index++;
513
+ skipWhitespace();
514
+ if (str[index] !== ';') {
515
+ throw new Error(`Expected semicolon`);
516
+ }
517
+ index++;
518
+ arr = new NbtIntArray();
519
+ } else {
520
+ arr = new NbtList();
521
+ }
522
+
523
+ let first = true;
524
+ let expectComma = false;
525
+
526
+ while (index < length) {
527
+ skipWhitespace();
528
+
529
+ // 检查是否结束
530
+ if (str[index] === ']') {
531
+ index++;
532
+ return arr;
533
+ }
534
+
535
+ // 检查逗号分隔符
536
+ if (expectComma) {
537
+ if (str[index] === ',') {
538
+ index++;
539
+ skipWhitespace();
540
+ // 允许尾随逗号: 检查逗号后是否直接是结束符
541
+ if (str[index] === ']') continue;
542
+ } else {
543
+ throw new Error(`Expected comma`);
544
+ }
545
+ }
546
+
547
+ const value = parseValue();
548
+ arr.addChild(value);
549
+
550
+ first = false;
551
+ expectComma = true; // 下一个元素前需要逗号
552
+ skipWhitespace();
553
+ }
554
+ throw new Error("Unterminated array");
555
+ }
556
+
557
+ function parseString(quoteChar) {
558
+ index++; // 跳过开头的引号
559
+ let result = "";
560
+ let escaped = false;
561
+
562
+ while (index < length) {
563
+ const char = str[index++];
564
+
565
+ if (escaped) {
566
+ // 处理转义字符
567
+ switch (char) {
568
+ case 'n': result += '\n'; break;
569
+ case 'r': result += '\r'; break;
570
+ case 't': result += '\t'; break;
571
+ case 'b': result += '\b'; break;
572
+ case 'f': result += '\f'; break;
573
+ case 'v': result += '\v'; break;
574
+ case '0': result += '\0'; break;
575
+ case '\\': result += '\\'; break;
576
+ case "'": result += "'"; break;
577
+ case '"': result += '"'; break;
578
+ case 'u':
579
+ // 处理Unicode转义:\uXXXX
580
+ if (index + 4 > length) {
581
+ throw new Error("Incomplete Unicode escape sequence");
582
+ }
583
+ const hex = str.substring(index, index + 4);
584
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
585
+ throw new Error(`Invalid Unicode escape: \\u${hex}`);
586
+ }
587
+ result += String.fromCharCode(parseInt(hex, 16));
588
+ index += 4;
589
+ break;
590
+ case 'x':
591
+ // 处理十六进制转义:\xXX
592
+ if (index + 2 > length) {
593
+ throw new Error("Incomplete hexadecimal escape sequence");
594
+ }
595
+ const hexByte = str.substring(index, index + 2);
596
+ if (!/^[0-9a-fA-F]{2}$/.test(hexByte)) {
597
+ throw new Error(`Invalid hexadecimal escape: \\x${hexByte}`);
598
+ }
599
+ result += String.fromCharCode(parseInt(hexByte, 16));
600
+ index += 2;
601
+ break;
602
+ default:
603
+ // 处理未知转义序列 - 保留原样
604
+ result += '\\' + char;
605
+ }
606
+ escaped = false;
607
+ } else if (char === "\\") {
608
+ escaped = true;
609
+ } else if (char === quoteChar) {
610
+ return new NbtString(result);
611
+ } else {
612
+ result += char;
613
+ }
614
+ }
615
+ throw new Error("Unterminated string");
616
+ }
617
+
618
+ function parseNumber() {
619
+ let start = index;
620
+ // 匹配数字(包括负号、小数点和科学计数法)
621
+ if (str[index] === '-') {
622
+ index++;
623
+ }
624
+
625
+ // 整数部分
626
+ while (index < length && /[0-9]/.test(str[index])) {
627
+ index++;
628
+ }
629
+
630
+ // 小数部分
631
+ if (str[index] === '.') {
632
+ index++;
633
+ while (index < length && /[0-9]/.test(str[index])) {
634
+ index++;
635
+ }
636
+ }
637
+
638
+ // 指数部分
639
+ if (/[eE]/.test(str[index])) {
640
+ index++;
641
+ if (/[+-]/.test(str[index])) {
642
+ index++;
643
+ }
644
+ while (index < length && /[0-9]/.test(str[index])) {
645
+ index++;
646
+ }
647
+ }
648
+
649
+ const numStr = str.substring(start, index);
650
+ let unit = "";
651
+
652
+ // 检查单位后缀
653
+ if (index < length && /[bfdisl]/i.test(str[index])) {
654
+ unit = str[index++];
655
+ }
656
+
657
+ // 验证数字格式
658
+ if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(numStr)) {
659
+ throw new Error(`Invalid number format: ${numStr}`);
660
+ }
661
+
662
+ const numValue = parseFloat(numStr);
663
+ if (isNaN(numValue)) {
664
+ throw new Error(`Invalid number: ${numStr}`);
665
+ }
666
+
667
+ return new NbtNumber(numValue, unit);
668
+ }
669
+
670
+ function parseKey() {
671
+ skipWhitespace();
672
+ if (index >= length) {
673
+ throw new Error("Unexpected end of input while parsing key");
674
+ }
675
+
676
+ // 键可以是字符串(单/双引号)或标识符
677
+ if (str[index] === "'" || str[index] === '"') {
678
+ const quote = str[index];
679
+ index++;
680
+ return parseStringContent(quote);
681
+ }
682
+
683
+ // 标识符键:允许Unicode字符(包括中文)
684
+ let key = "";
685
+ while (index < length) {
686
+ const char = str[index];
687
+ // 允许Unicode字符(包括中文)、字母、数字、下划线、$
688
+ if (!/\s/.test(char) && !/[{}[\]:,]/.test(char)) {
689
+ key += char;
690
+ index++;
691
+ } else {
692
+ break;
693
+ }
694
+ }
695
+
696
+ if (!key) {
697
+ throw new Error("Empty key is not allowed");
698
+ }
699
+
700
+ return key;
701
+ }
702
+
703
+ // 辅助函数:解析字符串内容(用于键和值)
704
+ function parseStringContent(quoteChar) {
705
+ let result = "";
706
+ let escaped = false;
707
+
708
+ while (index < length) {
709
+ const char = str[index++];
710
+
711
+ if (escaped) {
712
+ // 处理转义字符
713
+ switch (char) {
714
+ case 'n': result += '\n'; break;
715
+ case 'r': result += '\r'; break;
716
+ case 't': result += '\t'; break;
717
+ case 'b': result += '\b'; break;
718
+ case 'f': result += '\f'; break;
719
+ case 'v': result += '\v'; break;
720
+ case '0': result += '\0'; break;
721
+ case '\\': result += '\\'; break;
722
+ case "'": result += "'"; break;
723
+ case '"': result += '"'; break;
724
+ case 'u':
725
+ // 处理Unicode转义:\uXXXX
726
+ if (index + 4 > length) {
727
+ throw new Error("Incomplete Unicode escape sequence");
728
+ }
729
+ const hex = str.substring(index, index + 4);
730
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
731
+ throw new Error(`Invalid Unicode escape: \\u${hex}`);
732
+ }
733
+ result += String.fromCharCode(parseInt(hex, 16));
734
+ index += 4;
735
+ break;
736
+ case 'x':
737
+ // 处理十六进制转义:\xXX
738
+ if (index + 2 > length) {
739
+ throw new Error("Incomplete hexadecimal escape sequence");
740
+ }
741
+ const hexByte = str.substring(index, index + 2);
742
+ if (!/^[0-9a-fA-F]{2}$/.test(hexByte)) {
743
+ throw new Error(`Invalid hexadecimal escape: \\x${hexByte}`);
744
+ }
745
+ result += String.fromCharCode(parseInt(hexByte, 16));
746
+ index += 2;
747
+ break;
748
+ default:
749
+ // 处理未知转义序列 - 保留原样
750
+ result += '\\' + char;
751
+ }
752
+ escaped = false;
753
+ } else if (char === "\\") {
754
+ escaped = true;
755
+ } else if (char === quoteChar) {
756
+ return result;
757
+ } else {
758
+ result += char;
759
+ }
760
+ }
761
+ throw new Error("Unterminated string");
762
+ }
763
+
764
+ function skipWhitespace() {
765
+ while (index < length && /\s/.test(str[index])) {
766
+ index++;
767
+ }
768
+ }
769
+
770
+ try {
771
+ const value = parseValue();
772
+ ensureEnd();
773
+ return value;
774
+ } catch (e) {
775
+ // 添加位置信息
776
+ // context: before>>e<<after
777
+ const context = str.substring(Math.max(0, index - 10), index) + ">>" + (str[index] || "") + "<<" + str.substring(index + 1, index + 10);
778
+ throw new Error(`${e.message} at position ${index}. Context: ...${context}...`);
779
+ }
780
+ }
781
+
@@ -0,0 +1,33 @@
1
+ import { expect } from 'chai';
2
+ import { parseNbtString, NbtObject, NbtList } from '../src/snbt.js';
3
+
4
+ describe('NBT Parser', () => {
5
+ it('should parse simple compound', () => {
6
+ const result = parseNbtString(`{ name: "Steve", age: 30 }`);
7
+ expect(result).to.be.instanceOf(NbtObject);
8
+ expect(result.get('name').value).to.equal('Steve');
9
+ expect(result.get('age').value).to.equal(30);
10
+ });
11
+
12
+ it('should handle nested compounds', () => {
13
+ const result = parseNbtString(`{ player: { pos: [100, 64, -200] } }`);
14
+ expect(result.get('player')).to.be.instanceOf(NbtObject);
15
+ expect(result.get('player.pos')).to.be.instanceOf(NbtList);
16
+ });
17
+
18
+ it('should support all number types', () => {
19
+ const result = parseNbtString(`{
20
+ byte: 127b,
21
+ short: 32767s,
22
+ int: 2147483647,
23
+ long: 2147483648L,
24
+ float: 3.14f,
25
+ double: 3.1415926535d
26
+ }`);
27
+
28
+ expect(result.get('byte').value).to.equal(127);
29
+ expect(result.get('byte').unit).to.equal('b');
30
+ });
31
+
32
+ // 添加更多测试用例...
33
+ });