UNPKG

snbt-js

Version:

JavaScript library for parsing and manipulating Minecraft SNBT data

786 lines (719 loc) 26.7 kB
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>` }; } export class NbtObject { constructor(childsal) { this.childs = {}; this.addChild = function (key, value) { this.childs[key] = value; }; this.isempty = function () { return (Object.keys(this.childs).length == 0 ? true : false); }; this.get = function () { var args = Array.prototype.slice.call(arguments); if (gettype.call(args[0]) == "[object Array]") { args = args[0]; }; if (args.length == 1) { args = parsePath(args[0]) } if (args.length == 1) { return this.childs[args[0]]; } else if (args.length > 1) { return this.childs[args[0]].get(args.slice(1)); } else { return this; }; }; this.set = function (index, value) { if (["[object String]", "[object Number]"].includes(gettype.call(index))) { this.childs[index] = value; } else if (gettype.call(index) == "[object Array]") { if (index.length == 1) { this.childs[index[0]] = value; } else if (index.length > 1) { this.get(index.slice(0, -1)).set(index.slice(-1), value); }; }; }; this.text = function (ispretty) { var tl = []; for (var i in this.childs) { tl.push(`${ispretty ? highlightCode(i, "key") : i}: ${this.childs[i].text(ispretty)}`); }; return `{${tl.join(", ")}}`; }; if (childsal) { for (var index in childsal) { this.addChild(index, childsal[index]) }; }; }; }; export class NbtList { constructor(childsal) { this.childs = []; this.addChild = function (value) { this.childs.push(value) }; this.isempty = function () { return (this.childs.length == 0 ? true : false); }; this.get = function () { var args = Array.prototype.slice.call(arguments); if (gettype.call(args[0]) == "[object Array]") { args = args[0]; }; if (args.length == 1) { args = parsePath(args[0]) } if (args.length == 1) { return this.childs[args[0]]; } else if (args.length > 1) { return this.childs[args[0]].get(args.slice(1)); } else { return this; }; }; this.set = function (index, value) { if (["[object String]", "[object Number]"].includes(gettype.call(index))) { this.childs[index] = value; } else if (gettype.call(index) == "[object Array]") { if (index.length == 1) { this.childs[index[0]] = value; } else if (index.length > 1) { this.get(index.slice(0, -1)).set(index.slice(-1), value); }; }; }; this.text = function (ispretty) { var tl = []; for (var i = 0; i < this.childs.length; i++) { tl.push(this.childs[i].text(ispretty)); } return `[${tl.join(", ")}]`; }; if (childsal) { for (var i = 0; i < childsal.length; i++) { this.addChild(childsal[i]); } }; }; }; export class NbtIntArray { constructor(childsal) { this.childs = []; this.addChild = function (value) { if (value instanceof NbtNumber && value.unit == "") { this.childs.push(value) } else { throw new Error("NbtIntArray only accept NbtNumber without unit") } }; this.isempty = function () { return (this.childs.length == 0 ? true : false); }; this.get = function () { var args = Array.prototype.slice.call(arguments); if (gettype.call(args[0]) == "[object Array]") { args = args[0]; }; if (args.length == 1) { args = parsePath(args[0]) } if (args.length == 1) { return this.childs[args[0]]; } else if (args.length > 1) { throw new Error("NbtIntArray only accept one index") } else { return this; }; }; this.set = function (index, value) { if (index.length == 1) { if (value instanceof NbtNumber && value.unit == "") { this.childs[index[0]] = value; } else { throw new Error("NbtIntArray only accept NbtNumber without unit") } } else if (index.length > 1) { throw new Error("NbtIntArray only accept one index") }; }; this.text = function (ispretty) { var tl = []; for (var i = 0; i < this.childs.length; i++) { tl.push(this.childs[i].text(ispretty)); } return `[I; ${tl.join(", ")}]`; }; if (childsal) { for (var i = 0; i < childsal.length; i++) { this.addChild(childsal[i]); } }; }; }; export class NbtNumber { constructor(value, unit = "") { 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 (Math.round(value) > 9223372036854775807n) { this.value = 9223372036854775807n; } else if (Math.round(value) < -9223372036854775808n) { this.value = -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; this.text = function (ispretty) { if (ispretty) { return `${highlightCode(this.value.toString(), "num")}${unit ? highlightCode(this.unit, "unit") : ""}` } else { return `${this.value}${unit ? this.unit : ""}` }; }; }; }; export class NbtString { constructor(value) { this.value = value; this.text = function (ispretty) { // 转义特殊字符 let 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 { constructor(value) { this.value = value ? "true" : "false"; this.text = function (ispretty) { if (ispretty) { return highlightCode(`${this.value}`, "bool") } else { return `${this.value}` }; }; }; }; export class NbtNull { constructor() { this.value = null; } text = function (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 * @param {*} str * @returns */ export function decodeNbtStr(str) { 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 { var a = new NbtObject(); for (var i in jsObj) { a.addChild(i, changeObj(jsObj[i])) } return a; } } else if (gettype.call(jsObj) == "[object Array]") { var a = new NbtList(); for (var i in jsObj) { a.addChild(changeObj(jsObj[i])); } return a; } } 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'); } let 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 first = true; 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 = parseKey(); skipWhitespace(); // 检查键是否以数字开头(非引号包裹时) if (!/^['"]/.test(key) && /^\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); first = false; expectComma = true; // 下一个元素前需要逗号 skipWhitespace(); } throw new Error("Unterminated object"); } function parseArray() { index++; // 跳过 '[' // 检查是否IntArray skipWhitespace(); let arr; if (str[index] === 'I') { index++; skipWhitespace(); if (str[index] !== ';') { throw new Error(`Expected semicolon`); } index++; arr = new NbtIntArray(); } else { arr = new NbtList(); } let first = true; 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); first = false; 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 parseStringContent(quote); } // 标识符键:允许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; } // 辅助函数:解析字符串内容(用于键和值) 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}...`); } }