snbt-js
Version:
TypeScript library for parsing and manipulating Minecraft SNBT data
939 lines • 28.8 kB
JavaScript
const gettype = Object.prototype.toString;
export function changeToFloat(val) {
if ((val / 1).toString().includes(".")) {
return (val / 1);
}
else {
return (val / 1).toFixed(1);
}
;
}
;
export function highlightCode(text, type) {
if (type == "key") {
return `<span style="color:aqua">${text}</span>`;
}
else if (type == "str") {
return `<span style="color:rgb(84,351,84)">${text}</span>`;
}
else if (type == "num") {
return `<span style="color:orange">${text}</span>`;
}
else if (type == "bool") {
return `<span style="color:yellow">${text}</span>`;
}
else if (type == "unit") {
return `<span style="color:red">${text}</span>`;
}
return text;
}
export class NbtValue {
}
function isSafeKey(key) {
if (key === "" || /^\d/.test(key)) {
return false;
}
return !/[\s{}[\]:,]/.test(key);
}
function quoteKey(key) {
const escaped = key
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t");
return `"${escaped}"`;
}
export class NbtObject extends NbtValue {
constructor(childsal) {
super();
this.childs = {};
if (childsal) {
for (const index in childsal) {
this.addChild(index, childsal[index]);
}
}
}
addChild(key, value) {
this.childs[key] = value;
}
;
isempty() {
return (Object.keys(this.childs).length == 0 ? true : false);
}
;
get(...args) {
let pathArgs;
if (Array.isArray(args[0])) {
pathArgs = args[0];
}
else {
pathArgs = args;
}
if (pathArgs.length == 1) {
pathArgs = parsePath(pathArgs[0]);
}
if (pathArgs.length == 1) {
return this.childs[pathArgs[0]];
}
else if (pathArgs.length > 1) {
const child = this.childs[pathArgs[0]];
if (child === undefined) {
return undefined;
}
return child.get(pathArgs.slice(1));
}
else {
return this;
}
;
}
;
set(index, value) {
if (typeof index === "string" || typeof index === "number") {
this.childs[index] = value;
}
else if (Array.isArray(index)) {
if (index.length == 1) {
this.childs[index[0]] = value;
}
else if (index.length > 1) {
const parent = this.get(index.slice(0, -1));
if (parent !== undefined) {
parent.set(index.slice(-1), value);
}
}
;
}
;
}
;
text(ispretty) {
const tl = [];
for (const i in this.childs) {
const key = isSafeKey(i) ? i : quoteKey(i);
tl.push(`${ispretty ? highlightCode(key, "key") : key}: ${this.childs[i].text(ispretty)}`);
}
;
return `{${tl.join(", ")}}`;
}
;
}
;
export class NbtList extends NbtValue {
constructor(childsal) {
super();
this.childs = [];
if (childsal) {
for (const value of childsal) {
this.addChild(value);
}
}
}
addChild(value) {
this.childs.push(value);
}
;
isempty() {
return (this.childs.length == 0 ? true : false);
}
;
get(...args) {
let pathArgs;
if (Array.isArray(args[0])) {
pathArgs = args[0];
}
else {
pathArgs = args;
}
if (pathArgs.length == 1) {
pathArgs = parsePath(pathArgs[0]);
}
if (pathArgs.length == 1) {
return this.childs[pathArgs[0]];
}
else if (pathArgs.length > 1) {
const child = this.childs[pathArgs[0]];
if (child === undefined) {
return undefined;
}
return child.get(pathArgs.slice(1));
}
else {
return this;
}
;
}
;
set(index, value) {
if (typeof index === "string" || typeof index === "number") {
this.childs[index] = value;
}
else if (Array.isArray(index)) {
if (index.length == 1) {
this.childs[index[0]] = value;
}
else if (index.length > 1) {
const parent = this.get(index.slice(0, -1));
if (parent !== undefined) {
parent.set(index.slice(-1), value);
}
}
;
}
;
}
;
text(ispretty) {
const tl = [];
for (let i = 0; i < this.childs.length; i++) {
tl.push(this.childs[i].text(ispretty));
}
return `[${tl.join(", ")}]`;
}
;
}
;
class NbtNumberArray extends NbtValue {
constructor(unit, label, childsal) {
super();
this.childs = [];
this.unit = unit;
this.label = label;
if (childsal) {
for (const value of childsal) {
this.addChild(value);
}
}
}
validateValue(value) {
if (!(value instanceof NbtNumber && value.unit === this.unit)) {
const unitDesc = this.unit === "" ? "without unit" : `with unit "${this.unit}"`;
throw new Error(`${this.constructor.name} only accept NbtNumber ${unitDesc}`);
}
}
addChild(value) {
this.validateValue(value);
this.childs.push(value);
}
;
isempty() {
return (this.childs.length == 0 ? true : false);
}
;
get(...args) {
let pathArgs;
if (Array.isArray(args[0])) {
pathArgs = args[0];
}
else {
pathArgs = args;
}
if (pathArgs.length == 1) {
pathArgs = parsePath(pathArgs[0]);
}
if (pathArgs.length == 1) {
return this.childs[pathArgs[0]];
}
else if (pathArgs.length > 1) {
throw new Error(`${this.constructor.name} only accept one index`);
}
else {
return this;
}
;
}
;
set(index, value) {
const indices = Array.isArray(index) ? index : [index];
if (indices.length == 1) {
this.validateValue(value);
this.childs[indices[0]] = value;
}
else if (indices.length > 1) {
throw new Error(`${this.constructor.name} only accept one index`);
}
;
}
;
text(ispretty) {
const tl = [];
for (let i = 0; i < this.childs.length; i++) {
tl.push(this.childs[i].text(ispretty));
}
return `[${this.label}; ${tl.join(", ")}]`;
}
;
}
;
export class NbtIntArray extends NbtNumberArray {
constructor(childsal) {
super("", "I", childsal);
}
}
;
export class NbtLongArray extends NbtNumberArray {
constructor(childsal) {
super("l", "L", childsal);
}
}
;
export class NbtByteArray extends NbtNumberArray {
constructor(childsal) {
super("b", "B", childsal);
}
}
;
export class NbtNumber extends NbtValue {
constructor(value, unit = "") {
super();
unit = unit.toLowerCase();
if (unit == "b") {
if (Math.round(value) > 127) {
this.value = 127;
}
else if (Math.round(value) < -128) {
this.value = -128;
}
else {
this.value = Math.round(value);
}
}
else if (unit == "s") {
if (Math.round(value) > 32767) {
this.value = 32767;
}
else if (Math.round(value) < -32768) {
this.value = -32768;
}
else {
this.value = Math.round(value);
}
}
else if (unit == "l") {
if (BigInt(Math.round(value)) > 9223372036854775807n) {
this.value = Number(9223372036854775807n);
}
else if (BigInt(Math.round(value)) < -9223372036854775808n) {
this.value = Number(-9223372036854775808n);
}
else {
this.value = Math.round(value);
}
}
else if (unit == "d") {
this.value = changeToFloat(value);
}
else if (unit == "f") {
this.value = changeToFloat(value);
}
else if (unit == "") {
this.value = Math.round(value);
}
else {
this.value = value;
}
;
this.unit = unit;
}
text(ispretty) {
if (ispretty) {
return `${highlightCode(this.value.toString(), "num")}${this.unit ? highlightCode(this.unit, "unit") : ""}`;
}
else {
return `${this.value}${this.unit ? this.unit : ""}`;
}
;
}
;
}
;
export class NbtString extends NbtValue {
constructor(value) {
super();
this.value = value;
}
text(ispretty) {
// 转义特殊字符
const escapedValue = this.value
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t");
if (ispretty) {
return highlightCode(`'${escapedValue}'`, "str");
}
else {
return `'${escapedValue}'`;
}
}
;
}
;
export class NbtBool extends NbtValue {
constructor(value) {
super();
this.value = !!value;
}
text(ispretty) {
if (ispretty) {
return highlightCode(`${this.value}`, "bool");
}
else {
return `${this.value}`;
}
;
}
;
}
;
export class NbtNull extends NbtValue {
constructor() {
super();
this.value = null;
}
text(ispretty) {
if (ispretty) {
return highlightCode("null", "bool"); // 使用布尔值的样式
}
else {
return "null";
}
}
;
}
export function arrangementNbt(str) {
str = str.replace(/(: *)([0-9\.]+)([bfdis])/g, '$1new NbtNumber($2,"$3")');
str = str.replace(/(, *)([0-9\.]+)([bfdis])( *[,\]])/g, '$1new NbtNumber($2,"$3")$4');
str = str.replace(/(\[ *)([0-9\.]+)([bfdis])/g, '$1new NbtNumber($2,"$3")');
return str;
}
/**
*
* @deprecated Use `parseNbtString()` instead
* @param str
* @returns
*/
export function decodeNbtStr(str) {
const jsObj = eval("obj=" + arrangementNbt(str));
return changeObj(jsObj);
}
export function changeObj(jsObj) {
if (gettype.call(jsObj) == "[object String]") {
return new NbtString(jsObj);
}
else if (gettype.call(jsObj) == "[object Boolean]") {
return new NbtBool(jsObj);
}
else if (gettype.call(jsObj) == "[object Number]") {
return new NbtNumber(jsObj);
}
else if (gettype.call(jsObj) == "[object Object]") {
if (jsObj instanceof NbtNumber) {
return jsObj;
}
else {
const a = new NbtObject();
for (const i in jsObj) {
a.addChild(i, changeObj(jsObj[i]));
}
return a;
}
}
else if (gettype.call(jsObj) == "[object Array]") {
const a = new NbtList();
for (const i in jsObj) {
a.addChild(changeObj(jsObj[i]));
}
return a;
}
throw new Error(`Unsupported type: ${gettype.call(jsObj)}`);
}
export function parsePath(path) {
if (typeof path === 'number') {
return [path];
}
const tokens = [];
let current = '';
let inQuote = false;
let inBracket = false;
let hasQuoteInBracket = false;
for (let i = 0; i < path.length; i++) {
const char = path[i];
if (inQuote) {
if (char === '"') {
inQuote = false;
if (inBracket) {
hasQuoteInBracket = true;
}
}
else {
current += char;
}
}
else {
if (char === '"') {
if (current !== '') {
throw new Error('Unexpected double quote');
}
inQuote = true;
}
else if (char === '[') {
if (inBracket) {
throw new Error('Nested brackets are not allowed');
}
if (current !== '') {
tokens.push(current);
current = '';
}
else if (tokens.length === 0 || typeof tokens[tokens.length - 1] === 'number') {
throw new Error('Unexpected opening bracket');
}
inBracket = true;
hasQuoteInBracket = false;
}
else if (char === ']') {
if (!inBracket) {
throw new Error('Unexpected closing bracket');
}
const content = current.trim();
if (content === '') {
throw new Error('Empty brackets are not allowed');
}
if (hasQuoteInBracket) {
tokens.push(content);
}
else {
if (!/^\d+$/.test(content)) {
throw new Error('Brackets must contain only numbers or quoted strings');
}
tokens.push(parseInt(content, 10));
}
current = '';
inBracket = false;
}
else if (char === '.') {
if (inBracket) {
throw new Error('Dot not allowed inside brackets');
}
if (current !== '') {
tokens.push(current);
current = '';
}
else if (i === 0 || path[i - 1] === '.') {
throw new Error('Unexpected dot');
}
}
else {
if (inBracket && !hasQuoteInBracket) {
if (char === ' ' || char === '\t') {
// 允许空格
}
else if (char >= '0' && char <= '9') {
current += char;
}
else {
throw new Error(`Invalid character in bracket: '${char}'`);
}
}
else {
current += char;
}
}
}
}
// 结束后的状态检查
if (inQuote)
throw new Error('Unclosed quote');
if (inBracket)
throw new Error('Unclosed bracket');
if (current !== '')
tokens.push(current);
return tokens;
}
export function parseNbtString(str) {
let index = 0;
const length = str.length;
// 确保整个字符串被解析
function ensureEnd() {
skipWhitespace();
if (index < length) {
throw new Error(`Unexpected character: '${str[index]}'. Expected end of input.`);
}
}
function parseValue() {
skipWhitespace();
if (index >= length) {
throw new Error("Unexpected end of input");
}
const char = str[index];
if (char === '{') {
const obj = parseObject();
return obj;
}
else if (char === '[') {
const arr = parseArray();
return arr;
}
else if (char === "'" || char === '"') {
return parseString(char);
}
else if (/[0-9-]/.test(char)) {
return parseNumber();
}
else if (char === 't' && index + 4 <= length && str.substr(index, 4) === "true") {
index += 4;
return new NbtBool(true);
}
else if (char === 'f' && index + 5 <= length && str.substr(index, 5) === "false") {
index += 5;
return new NbtBool(false);
}
else if (char === 'n' && index + 4 <= length && str.substr(index, 4) === "null") {
index += 4;
return new NbtNull();
}
throw new Error(`Unexpected character: ${char}`);
}
function parseObject() {
index++; // 跳过 '{'
const obj = new NbtObject();
let expectComma = false;
while (index < length) {
skipWhitespace();
// 检查是否结束
if (str[index] === '}') {
index++;
return obj;
}
// 检查逗号分隔符
if (expectComma) {
if (str[index] === ',') {
index++;
skipWhitespace();
// 允许尾随逗号: 检查逗号后是否直接是结束符
if (str[index] === '}')
continue;
}
else {
throw new Error(`Expected comma`);
}
}
const { key, quoted } = parseKey();
skipWhitespace();
// 检查键是否以数字开头(非引号包裹时)
if (!quoted && /^\d/.test(key)) {
throw new Error(`Key cannot start with a digit: ${key}`);
}
if (str[index] !== ':') {
throw new Error(`Expected colon`);
}
index++; // 跳过 ':'
skipWhitespace();
const value = parseValue();
obj.addChild(key, value);
expectComma = true; // 下一个元素前需要逗号
skipWhitespace();
}
throw new Error("Unterminated object");
}
function parseArray() {
index++; // 跳过 '['
// 检查是否为类型化数组(B; / I; / L;)
skipWhitespace();
const typedArrayCtors = { B: NbtByteArray, I: NbtIntArray, L: NbtLongArray };
let arr;
const TypedCtor = typedArrayCtors[str[index]];
if (TypedCtor) {
index++;
skipWhitespace();
if (str[index] !== ';') {
throw new Error(`Expected semicolon`);
}
index++;
arr = new TypedCtor();
}
else {
arr = new NbtList();
}
let expectComma = false;
while (index < length) {
skipWhitespace();
// 检查是否结束
if (str[index] === ']') {
index++;
return arr;
}
// 检查逗号分隔符
if (expectComma) {
if (str[index] === ',') {
index++;
skipWhitespace();
// 允许尾随逗号: 检查逗号后是否直接是结束符
if (str[index] === ']')
continue;
}
else {
throw new Error(`Expected comma`);
}
}
const value = parseValue();
arr.addChild(value);
expectComma = true; // 下一个元素前需要逗号
skipWhitespace();
}
throw new Error("Unterminated array");
}
function parseString(quoteChar) {
index++; // 跳过开头的引号
let result = "";
let escaped = false;
while (index < length) {
const char = str[index++];
if (escaped) {
// 处理转义字符
switch (char) {
case 'n':
result += '\n';
break;
case 'r':
result += '\r';
break;
case 't':
result += '\t';
break;
case 'b':
result += '\b';
break;
case 'f':
result += '\f';
break;
case 'v':
result += '\v';
break;
case '0':
result += '\0';
break;
case '\\':
result += '\\';
break;
case "'":
result += "'";
break;
case '"':
result += '"';
break;
case 'u':
// 处理Unicode转义:\uXXXX
if (index + 4 > length) {
throw new Error("Incomplete Unicode escape sequence");
}
const hex = str.substring(index, index + 4);
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
throw new Error(`Invalid Unicode escape: \\u${hex}`);
}
result += String.fromCharCode(parseInt(hex, 16));
index += 4;
break;
case 'x':
// 处理十六进制转义:\xXX
if (index + 2 > length) {
throw new Error("Incomplete hexadecimal escape sequence");
}
const hexByte = str.substring(index, index + 2);
if (!/^[0-9a-fA-F]{2}$/.test(hexByte)) {
throw new Error(`Invalid hexadecimal escape: \\x${hexByte}`);
}
result += String.fromCharCode(parseInt(hexByte, 16));
index += 2;
break;
default:
// 处理未知转义序列 - 保留原样
result += '\\' + char;
}
escaped = false;
}
else if (char === "\\") {
escaped = true;
}
else if (char === quoteChar) {
return new NbtString(result);
}
else {
result += char;
}
}
throw new Error("Unterminated string");
}
function parseNumber() {
let start = index;
// 匹配数字(包括负号、小数点和科学计数法)
if (str[index] === '-') {
index++;
}
// 整数部分
while (index < length && /[0-9]/.test(str[index])) {
index++;
}
// 小数部分
if (str[index] === '.') {
index++;
while (index < length && /[0-9]/.test(str[index])) {
index++;
}
}
// 指数部分
if (/[eE]/.test(str[index])) {
index++;
if (/[+-]/.test(str[index])) {
index++;
}
while (index < length && /[0-9]/.test(str[index])) {
index++;
}
}
const numStr = str.substring(start, index);
let unit = "";
// 检查单位后缀
if (index < length && /[bfdisl]/i.test(str[index])) {
unit = str[index++];
}
// 验证数字格式
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(numStr)) {
throw new Error(`Invalid number format: ${numStr}`);
}
const numValue = parseFloat(numStr);
if (isNaN(numValue)) {
throw new Error(`Invalid number: ${numStr}`);
}
return new NbtNumber(numValue, unit);
}
function parseKey() {
skipWhitespace();
if (index >= length) {
throw new Error("Unexpected end of input while parsing key");
}
// 键可以是字符串(单/双引号)或标识符
if (str[index] === "'" || str[index] === '"') {
const quote = str[index];
index++;
return { key: parseStringContent(quote), quoted: true };
}
// 标识符键:允许Unicode字符(包括中文)
let key = "";
while (index < length) {
const char = str[index];
// 允许Unicode字符(包括中文)、字母、数字、下划线、$
if (!/\s/.test(char) && !/[{}[\]:,]/.test(char)) {
key += char;
index++;
}
else {
break;
}
}
if (!key) {
throw new Error("Empty key is not allowed");
}
return { key, quoted: false };
}
// 辅助函数:解析字符串内容(用于键和值)
function parseStringContent(quoteChar) {
let result = "";
let escaped = false;
while (index < length) {
const char = str[index++];
if (escaped) {
// 处理转义字符
switch (char) {
case 'n':
result += '\n';
break;
case 'r':
result += '\r';
break;
case 't':
result += '\t';
break;
case 'b':
result += '\b';
break;
case 'f':
result += '\f';
break;
case 'v':
result += '\v';
break;
case '0':
result += '\0';
break;
case '\\':
result += '\\';
break;
case "'":
result += "'";
break;
case '"':
result += '"';
break;
case 'u':
// 处理Unicode转义:\uXXXX
if (index + 4 > length) {
throw new Error("Incomplete Unicode escape sequence");
}
const hex = str.substring(index, index + 4);
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
throw new Error(`Invalid Unicode escape: \\u${hex}`);
}
result += String.fromCharCode(parseInt(hex, 16));
index += 4;
break;
case 'x':
// 处理十六进制转义:\xXX
if (index + 2 > length) {
throw new Error("Incomplete hexadecimal escape sequence");
}
const hexByte = str.substring(index, index + 2);
if (!/^[0-9a-fA-F]{2}$/.test(hexByte)) {
throw new Error(`Invalid hexadecimal escape: \\x${hexByte}`);
}
result += String.fromCharCode(parseInt(hexByte, 16));
index += 2;
break;
default:
// 处理未知转义序列 - 保留原样
result += '\\' + char;
}
escaped = false;
}
else if (char === "\\") {
escaped = true;
}
else if (char === quoteChar) {
return result;
}
else {
result += char;
}
}
throw new Error("Unterminated string");
}
function skipWhitespace() {
while (index < length && /\s/.test(str[index])) {
index++;
}
}
try {
const value = parseValue();
ensureEnd();
return value;
}
catch (e) {
// 添加位置信息
// context: before>>e<<after
const context = str.substring(Math.max(0, index - 10), index) + ">>" + (str[index] || "") + "<<" + str.substring(index + 1, index + 10);
throw new Error(`${e.message} at position ${index}. Context: ...${context}...`);
}
}
//# sourceMappingURL=snbt.js.map