@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,317 lines • 65.7 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ObjectKeyAvoidTermList = void 0;
const CreatorToolsHost_1 = __importStar(require("../app/CreatorToolsHost"));
const IField_1 = require("../dataform/IField");
const AppServiceProxy_1 = __importDefault(require("./AppServiceProxy"));
const Log_1 = __importDefault(require("./Log"));
const stableStringify = __importStar(require("json-stable-stringify"));
// json-stable-stringify may be exported either as a function (CommonJS) or as a default property
// (ES module interop). Create an alias that resolves to the callable function in either case.
const stableStringifyFunc = stableStringify.default ?? stableStringify;
const singleComment = Symbol("singleComment");
const multiComment = Symbol("multiComment");
const javascriptStringTranslations = new Map();
exports.ObjectKeyAvoidTermList = new Set([
"__proto__",
"prototype",
"[[Prototype]]",
"this",
"delete",
"constructor",
"hasOwnProperty",
"isPrototypeOf",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
]);
class Utilities {
static _isDebug;
static _isAppSim;
static defaultEncoding = "UTF-8";
static replacementChar = 0xfffd;
// note that if the stringToSplit is well formed, it should return untilCount+1 items items in the string[]
static splitUntil(stringToSplit, splitChar, untilCount) {
const results = [];
let lastPipe = 0;
let index = 0;
let curPipe = stringToSplit.indexOf(splitChar);
while (curPipe > 0 && index < untilCount) {
results.push(stringToSplit.substring(lastPipe, curPipe));
index++;
lastPipe = curPipe + 1;
curPipe = stringToSplit.indexOf(splitChar, lastPipe);
}
results.push(stringToSplit.substring(lastPipe, stringToSplit.length));
return results;
}
static parseJson(jsonString) {
let obj;
try {
obj = JSON.parse(jsonString);
}
catch (e) {
return undefined;
}
return obj;
}
static isScientificFloat(value) {
// Ensure it's a number (not NaN, not Infinity)
if (typeof value !== "number" || !isFinite(value)) {
return false;
}
// Convert to string in default format
const str = value.toString();
// Match scientific notation pattern: e.g., 1.23e4, -5E-3
return /^[+-]?\d+(\.\d+)?e[+-]?\d+$/i.test(str);
}
static isUsableAsObjectKey(term) {
if (term === undefined || term === null) {
return false;
}
return !exports.ObjectKeyAvoidTermList.has(term);
}
/**
* Returns true if the string looks like a Minecraft localization key
* (e.g., "pack.name", "pack.description", "skinpack.mypack.name").
* These are dotted identifiers with no spaces, used as lookup keys
* in .lang files rather than as user-visible display names.
*/
static isLikelyLocalizationKey(name) {
if (!name || name.indexOf(" ") >= 0) {
return false;
}
// Must contain at least one dot (e.g., "pack.name")
// and consist only of word-like characters and dots
return /^[\w][\w.]*\.[\w][\w.]*$/.test(name);
}
static async sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
static get isPreview() {
return true;
}
static get isAppSim() {
if (Utilities._isAppSim === undefined) {
// @ts-ignore
if (typeof window !== "undefined") {
// @ts-ignore
const query = window.location.search.toLowerCase();
if (query.indexOf("appsim=true") >= 0) {
Utilities._isAppSim = true;
}
else {
Utilities._isAppSim = false;
}
}
else {
Utilities._isAppSim = false;
}
}
return Utilities._isAppSim;
}
static get isDebug() {
if (Utilities._isDebug === undefined) {
if (AppServiceProxy_1.default.hasAppService) {
// @ts-ignore
if (typeof window !== "undefined") {
// @ts-ignore
if (window.location.href.indexOf("localhost") >= 0) {
Utilities._isDebug = true;
}
}
if (!Utilities._isDebug) {
Utilities._isDebug = false;
}
// @ts-ignore
}
else if (typeof window !== "undefined") {
// @ts-ignore
const query = window.location.search.toLowerCase();
if (query.indexOf("debug=true") >= 0) {
Utilities._isDebug = true;
}
else {
Utilities._isDebug = false;
}
}
else {
Utilities._isDebug = false;
}
}
return Utilities._isDebug;
}
static selectJsonObject(jsonO, select, ensureObjects) {
const selectors = select.split("/");
for (const selector of selectors) {
if (selector.length > 0) {
jsonO = jsonO[selector];
if (!jsonO && !ensureObjects) {
return undefined;
}
if (!jsonO && ensureObjects) {
jsonO[selector] = {};
jsonO = jsonO[selector];
}
}
}
return jsonO;
}
static removeItemInArray(objToRemove, stringArr) {
const nextArray = [];
for (const candStr of stringArr) {
if (candStr !== objToRemove) {
nextArray.push(candStr);
}
}
return nextArray;
}
static isArrayNonNegative(arr) {
for (const val of arr) {
if (val < 0) {
return false;
}
}
return true;
}
static arrayHasNegativeAndIsNumeric(arr) {
let foundNegative = false;
for (const val of arr) {
if (typeof val !== "number") {
return false;
}
else if (val < 0) {
foundNegative = true;
}
}
return foundNegative;
}
static encodeObjectWithSequentialRunLengthEncodeUsingNegative(obj) {
for (const key in obj) {
const val = obj[key];
if (val !== undefined) {
if (typeof val === "object" && !Array.isArray(val)) {
this.encodeObjectWithSequentialRunLengthEncodeUsingNegative(val);
}
else if (Array.isArray(val)) {
let isNumericArray = true;
for (const arrVal of val) {
if (typeof arrVal !== "number") {
isNumericArray = false;
break;
}
}
if (isNumericArray) {
obj[key] = this.encodeSequentialRunLengthUsingNegative(val);
}
}
}
}
}
static decodeSequentialRunLengthUsingNegative(arr) {
if (this.isArrayNonNegative(arr)) {
return arr;
}
const newArr = [];
newArr.push(arr[0]);
for (let i = 1; i < arr.length; i++) {
if (arr[i] < 0) {
let startVal = arr[i - 1];
for (let j = 0; j < Math.abs(arr[i]); j++) {
startVal++;
newArr.push(startVal);
}
}
else {
newArr.push(arr[i]);
}
}
return newArr;
}
/* Convert a numeric array from:
[1, 2, 3, 7, 9, 10, 15, 16, 17, 18]
to a "sequential run length encode", where negative numbers are used to indicate a "run"
so the above becomes
[1, -2, 7, 9, -1, 15, -3]
*/
static encodeSequentialRunLengthUsingNegative(arr) {
if (!this.isArrayNonNegative(arr)) {
return arr;
}
arr.sort();
const newArr = [];
newArr.push(arr[0]);
let streak = -1;
for (let i = 1; i < arr.length; i++) {
if (arr[i] === arr[i - 1] + 1) {
if (streak < 1) {
streak = 1;
}
else {
streak++;
}
}
else {
if (streak >= 1) {
newArr.push(-streak);
streak = -1;
}
newArr.push(arr[i]);
}
}
if (streak >= 1) {
newArr.push(-streak);
streak = -1;
}
return newArr;
}
static trimEllipsis(text, length) {
if (text.length > length) {
text = text.substring(0, length - 1) + "…";
}
return text;
}
static makeHashFileSafe(hash) {
hash = hash.replace(/\//gi, "-S");
hash = hash.replace(/\+/gi, "-P");
hash = hash.replace(/\\/gi, "-B");
hash = hash.replace(/=/gi, "-E");
hash = hash.replace(/,/gi, "-C");
return hash;
}
static getSimpleNumeric(num) {
if (num === undefined) {
return "";
}
if (num < 1000) {
return num.toString();
}
if (num < 1000) {
return Math.floor(num / 100) / 10 + "k";
}
if (num > 1000000) {
return Math.floor(num / 100000) / 10 + "m";
}
return Math.floor(num / 1000) + "k";
}
static humanifyJsName(name) {
if (typeof name === "boolean" || typeof name === "number") {
return name.toString();
}
if (name.toLowerCase().indexOf("apis used") >= 0) {
return name;
}
if (name.startsWith("apisUsed")) {
name = "APIs Used" + name.substring(8);
return name;
}
let retVal = "";
for (let i = 0; i < name.length; i++) {
if (i === 0) {
retVal += name[i].toUpperCase();
}
else {
if (name[i] >= "A" &&
name[i] <= "Z" &&
(i === name.length - 1 ||
i === 0 ||
((name[i + 1] < "A" || name[i + 1] > "Z") && (name[i - 1] < "A" || name[i - 1] > "Z")))) {
retVal += " ";
}
retVal += name[i];
}
}
retVal = retVal.replace(/_/gi, " ");
retVal = retVal.replace("Java Script", "JavaScript");
return retVal;
}
static getTitleFromEnum(categoryEnum, topicId) {
if (categoryEnum[topicId]) {
return Utilities.humanifyJsName(categoryEnum[topicId]);
}
return "General";
}
static humanifyObject(sampVal) {
if (typeof sampVal === "object") {
sampVal = JSON.stringify(sampVal, null, 2);
}
else {
sampVal = sampVal.toString();
}
sampVal = sampVal.trim();
if (sampVal.startsWith("[") && sampVal.endsWith("]")) {
sampVal = sampVal.substring(1, sampVal.length - 1);
sampVal = sampVal.replace(/"/gi, "");
}
if (sampVal.startsWith("{") && sampVal.endsWith("}")) {
sampVal = sampVal.substring(1, sampVal.length - 1);
}
return sampVal;
}
static ensureLooksLikeSentence(name) {
if (name.length > 1) {
if (name[0] >= "a" && name[0] <= "z") {
name = name[0].toUpperCase() + name.substring(1, name.length);
}
}
if (!name.endsWith(".")) {
name = name + ".";
}
return name;
}
static dehumanify(val, humanify) {
if (!humanify) {
return val;
}
return Utilities.dehumanifyMinecraftName(val);
}
static humanify(val, humanify) {
if (humanify === IField_1.FieldValueHumanify.none || val === undefined) {
return val;
}
if (humanify === IField_1.FieldValueHumanify.general) {
return Utilities.humanifyObject(val);
}
if (Array.isArray(val)) {
let simpleStr = "";
for (let i = 0; i < val.length; i++) {
const data = val[i];
if (data !== undefined) {
// if the object looks complicated, just stringify it
if (typeof data === "object") {
return JSON.stringify(val);
}
if (simpleStr.length > 0) {
simpleStr += ", ";
}
simpleStr += String(i + 1) + ": " + data.toString();
}
}
return simpleStr;
}
else if (typeof val === "object") {
let simpleStr = "";
for (const key in val) {
const data = val[key];
if (data !== undefined) {
// if the object looks complicated, just stringify it
if (typeof data === "object") {
return JSON.stringify(val);
}
if (simpleStr.length > 0) {
simpleStr += ", ";
}
simpleStr += key + ": " + data.toString();
}
}
return simpleStr;
}
if (Array.isArray(val)) {
return val;
}
return Utilities.humanifyMinecraftName(val);
}
static ensureFirstCharIsUpperCase(name) {
if (name.length > 1) {
if (name[0] >= "a" && name[0] <= "z") {
name = name[0].toUpperCase() + name.substring(1, name.length);
}
}
return name;
}
static ensureFirstCharIsLowerCase(name) {
if (name.length > 1) {
if (name[0] >= "A" && name[0] <= "Z") {
name = name[0].toLowerCase() + name.substring(1, name.length);
}
}
return name;
}
static humanifyString(val, humanify) {
if (!humanify || val === undefined) {
return val;
}
if (humanify === IField_1.FieldValueHumanify.general) {
return Utilities.humanifyObject(val).toString();
}
return Utilities.humanifyMinecraftName(val);
}
static getHumanifiedObjectNameNoSpaces(name) {
name = Utilities.getHumanifiedObjectName(name);
name = name.replace(/ /gi, "");
return name;
}
static getHumanifiedObjectName(name) {
if (typeof name !== "string") {
name = name.toString();
}
let firstColon = name.indexOf(":");
if (firstColon >= 0) {
name = name.substring(firstColon + 1);
}
name = Utilities.humanifyMinecraftName(name);
return name;
}
static replaceAllCaseInsensitive(str, find, replace) {
find = find.toLowerCase();
let strLower = str.toLowerCase();
let start = strLower.indexOf(find);
while (start >= 0) {
str = str.substring(0, start) + replace + str.substring(start + find.length);
strLower = str.toLowerCase();
start = strLower.indexOf(find, start + replace.length);
}
return str;
}
static addCommasToNumber(num) {
if (typeof num !== "number" || isNaN(num)) {
return num.toString();
}
// Handle negative numbers
const isNegative = num < 0;
const absoluteNum = Math.abs(num);
// Split into integer and decimal parts
const parts = absoluteNum.toString().split(".");
const integerPart = parts[0];
const decimalPart = parts[1];
// Add commas to integer part
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// Reconstruct the number
let result = formattedInteger;
if (decimalPart) {
result += "." + decimalPart;
}
return isNegative ? "-" + result : result;
}
static sanitizeJavascriptName(name) {
name = name.trim();
name = name.replace(/</gi, "LessThan");
name = name.replace(/>/gi, "GreaterThan");
name = name.replace(/=/gi, "Equals");
name = name.replace(/!/gi, "Not");
name = name.replace(/\+/gi, "Plus");
name = name.replace(/[,\-\.' \[\]\(\)\{\}<>\*\"\?\\\|#]/gi, "");
if (name.length > 0 && name[0] >= "0" && name[0] <= "9") {
name = "_" + name;
}
return name;
}
static wrapJavascriptNameIfNeeded(name, capitalizeFirst) {
let trimName = name.trim();
trimName = trimName.replace(/[\+]/gi, "");
if (trimName.startsWith('"') && trimName.endsWith('"')) {
return trimName;
}
let needsWrap = false;
trimName = this.sanitizeJavascriptName(trimName);
for (let i = 0; i < trimName.length; i++) {
if (trimName[i] === " " ||
trimName[i] === ":" ||
trimName[i] === "," ||
trimName[i] === "(" ||
trimName[i] === ")" ||
trimName[i] === "." ||
trimName[i] === "*" ||
trimName[i] === '"' ||
trimName[i] === "'" ||
trimName[i] === "-" ||
trimName[i] === "#" ||
trimName[i] === "[" ||
trimName[i] === "]") {
needsWrap = true;
}
}
if (needsWrap) {
trimName = '"' + trimName + '"';
}
return trimName;
}
static javascriptifyName(name, capitalizeFirst) {
let trimName = name.trim();
// Preserve single-character names if they are valid identifiers or special tokens like "#" (mcfunction comments)
if (trimName.length === 1 && /[a-zA-Z0-9_$#]/.test(trimName)) {
return trimName;
}
let retVal = "";
let capitalizeNext = capitalizeFirst === true;
for (let i = 0; i < trimName.length; i++) {
if (trimName[i] === " " ||
trimName[i] === "_" ||
trimName[i] === ":" ||
trimName[i] === "," ||
trimName[i] === "(" ||
trimName[i] === ")" ||
trimName[i] === "." ||
trimName[i] === '"' ||
trimName[i] === "'" ||
trimName[i] === "+" ||
trimName[i] === "-" ||
trimName[i] === "#" ||
trimName[i] === "[" ||
trimName[i] === "]") {
capitalizeNext = true;
}
else {
if (capitalizeNext) {
retVal += trimName[i].toUpperCase();
capitalizeNext = false;
}
else {
retVal += trimName[i];
}
}
}
retVal = this.sanitizeJavascriptName(retVal);
return retVal;
}
static humanifyMinecraftNameRemoveNamespaces(name) {
const firstPeriod = name.indexOf(".");
if (firstPeriod >= 0) {
name = name.substring(firstPeriod + 1);
}
const firstColon = name.indexOf(":");
if (firstColon >= 0) {
name = name.substring(firstColon + 1);
}
name = Utilities.humanifyMinecraftName(name);
return name;
}
static consistentStringifyTrimmed(value) {
const retVal = stableStringifyFunc(value);
if (retVal === undefined || retVal === null) {
return "";
}
return retVal;
}
static consistentStringify(value) {
const retVal = stableStringifyFunc(value, {
space: 2,
});
if (retVal === undefined || retVal === null) {
return "";
}
return retVal;
}
static humanifyMinecraftName(name, doNotReverse) {
if (name === undefined || name === null) {
return "";
}
if (typeof name === "boolean" || typeof name === "number") {
return name.toString();
}
if (name.endsWith(".behavior")) {
name = name.substring(0, name.length - 9);
}
else if (name.endsWith(".geo")) {
name = name.substring(0, name.length - 4);
}
else if (name.endsWith(".entity")) {
name = name.substring(0, name.length - 7);
}
name = name.replace("breeds_with.", "");
name = name.replace("interactions.", "");
name = name.replace("name_actions.", "");
name = name.replace("tempt.", "");
name = name.replace("triggers.", "");
name = name.replace("math.", "");
name = name.replace("query.", "");
name = name.replace(" on_", " ");
let parenStart = name.indexOf(" (");
let parenEnd = name.indexOf(")");
if (parenStart > 0 && parenEnd > parenStart) {
name = name.substring(0, parenStart) + name.substring(parenEnd + 1);
}
name = name.replace(/`/gi, "");
if (name.startsWith("on_")) {
name = name.substring(3, name.length);
}
const colon = name.indexOf(":");
if (colon >= 0 && name.substring(0, colon) === "minecraft") {
name = name.substring(colon + 1);
}
const leftBracket = name.indexOf("[");
const rightBracket = name.indexOf("]");
if (leftBracket >= 0 && rightBracket > leftBracket) {
name = name.substring(0, leftBracket) + name.substring(rightBracket + 1);
}
if (name.endsWith(".")) {
name = name.substring(0, name.length - 1);
}
name = name.replace(/[_]/gi, " ");
if (name.endsWith("_bit")) {
name = name.substring(0, name.length - 4);
}
name = name.replace(/::/gi, " ");
name = name.replace(/:/gi, " ");
name = name.replace("SharedTypes ", "");
if (name.startsWith("Struct ") || name.startsWith("struct ")) {
name = name.substring(7);
}
if ((name.startsWith("Enum ") || name.startsWith("enum ")) && name.indexOf("num_property") < 0) {
name = name.substring(5);
}
const lastPeriod = name.indexOf(".");
if (doNotReverse) {
if (lastPeriod >= 4) {
name = name.substring(0, lastPeriod) + ": " + name.substring(lastPeriod + 1);
}
}
else {
if (lastPeriod >= 4) {
name = name.substring(lastPeriod + 1) + " " + name.substring(0, lastPeriod);
}
}
/*
if (name.length > 1) {
if (name[0] >= "a" && name[0] <= "z") {
name = name[0].toUpperCase() + name.substring(1, name.length);
}
}*/
let lastCharWasSpace = false;
for (let i = 0; i < name.length; i++) {
if (name[i] === " ") {
lastCharWasSpace = true;
}
else {
if ((lastCharWasSpace || i === 0) &&
name[i] >= "a" &&
name[i] <= "z" &&
(i === name.length - 1 || (name[i + 1] >= "a" && name[i] <= "z"))) {
name = name.substring(0, i) + name[i].toUpperCase() + name.substring(i + 1);
}
lastCharWasSpace = false;
}
}
name = name.trim();
return name;
}
static lowerCaseStartOfString(name) {
if (name.length <= 0) {
return name;
}
if (name.length <= 1) {
return name.toLowerCase();
}
if (name.startsWith("APIs")) {
return "apis" + name.substring(4);
}
if (name.startsWith("PNGJPG")) {
return "pngjpg" + name.substring(6);
}
if (name.startsWith("TGA")) {
return "tga" + name.substring(3);
}
if (name.charAt(1) < "A" || name.charAt(1) > "Z") {
return name.charAt(0).toLowerCase() + name.substring(1);
}
return name;
}
static convertToJsonKey(name) {
if (!name) {
return name;
}
const cache = javascriptStringTranslations.get(name);
if (cache) {
return cache;
}
let val = Utilities.lowerCaseStartOfString(Utilities.javascriptifyName(name, false));
javascriptStringTranslations.set(name, val);
return val;
}
static dehumanifyMinecraftName(name) {
if (!name || typeof name === "boolean" || typeof name === "number") {
return name;
}
// if this is already a technical name, return;
if (name.indexOf(":") >= 0) {
return name;
}
name = name.toLowerCase();
name = name.replace(/ /gi, "_");
name = "minecraft:" + name;
return name;
}
static stringFormat(templateString, ...vals) {
var args = arguments;
return arguments[0].replace(/{(\d+)}/g, function (content, interior) {
try {
const num = parseInt(interior);
if (!isNaN(num)) {
return args[num + 1] !== undefined ? args[num + 1] : content;
}
}
catch (e) { }
return content;
});
}
static convertToHexString(byteArray) {
return Array.from(byteArray, function (byte) {
return ("0" + (byte & 0xff).toString(16)).slice(-2);
}).join("");
}
static countSignificantLines(content) {
if (content.length <= 0) {
return 0;
}
let lineCount = 1;
let curStart = 0;
let nextNewline = content.indexOf("\n");
while (nextNewline >= curStart) {
let curContent = content.substring(curStart, nextNewline).trim();
if (curContent.length > 1) {
lineCount++;
}
curStart = nextNewline + 1;
nextNewline = content.indexOf("\n", curStart);
}
return lineCount;
}
static stripLinesContaining(content, lineContains) {
let i = content.indexOf(lineContains);
while (i >= 0) {
let prevNewLine = content.lastIndexOf("\n", i);
if (prevNewLine < 0) {
prevNewLine = 0;
}
let nextNewLine = content.indexOf("\n", i);
if (nextNewLine < 0) {
nextNewLine = content.length;
}
content = content.substring(0, prevNewLine) + content.substring(nextNewLine + 1, content.length);
i = content.indexOf(lineContains, prevNewLine);
}
return content;
}
static stripWithoutWhitespace = () => "";
static stripWithWhitespace = (str, start, end) => str.slice(start, end).replace(/\S/g, " ");
static replaceJsonValue(jsonContent, attributeName, newValue) {
let nextAttribute = jsonContent.indexOf('"' + attributeName + '":');
while (nextAttribute >= 0) {
const subsequentOpenQuote = jsonContent.indexOf('"', nextAttribute + attributeName.length + 3);
if (subsequentOpenQuote > nextAttribute + attributeName.length + 2 &&
subsequentOpenQuote < nextAttribute + attributeName.length + 7) {
const subsequentEndQuote = jsonContent.indexOf('"', subsequentOpenQuote + 1);
if (subsequentEndQuote > subsequentOpenQuote) {
jsonContent =
jsonContent.substring(0, subsequentOpenQuote + 1) + newValue + jsonContent.substring(subsequentEndQuote);
}
}
nextAttribute = jsonContent.indexOf('"' + attributeName + '":', nextAttribute + attributeName.length + 2);
}
return jsonContent;
}
static makeJsonVersionAgnostic(jsonContent) {
jsonContent = Utilities.replaceJsonValue(jsonContent, "generatorVersion", "TESTSUB");
jsonContent = Utilities.replaceJsonValue(jsonContent, "uuid", "TESTSUB");
return jsonContent;
}
static isEscaped(jsonString, quotePosition) {
let index = quotePosition - 1;
let backslashCount = 0;
while (jsonString[index] === "\\") {
index -= 1;
backslashCount += 1;
}
return Boolean(backslashCount % 2);
}
static staticCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
static fixJsonContent(jsonString, { whitespace = true, trailingCommas = false } = {}) {
if (typeof jsonString !== "string") {
throw new TypeError(`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``);
}
jsonString = jsonString.trim();
const strip = whitespace ? Utilities.stripWithWhitespace : Utilities.stripWithoutWhitespace;
let isInsideString = false;
let isInsideComment = false;
let offset = 0;
let buffer = "";
let result = "";
let commaIndex = -1;
for (let index = 0; index < jsonString.length; index++) {
const currentCharacter = jsonString[index];
const nextCharacter = jsonString[index + 1];
if (!isInsideComment && currentCharacter === '"') {
// Enter or exit string
const escaped = Utilities.isEscaped(jsonString, index);
if (!escaped) {
isInsideString = !isInsideString;
}
}
if (isInsideString) {
// fix control characters inside of strings, if they exist
if (currentCharacter === "\r" || currentCharacter === "\n" || currentCharacter === "\t") {
jsonString = jsonString.substring(0, index) + " " + jsonString.substring(index + 1);
}
continue;
}
if (!isInsideComment && currentCharacter + nextCharacter === "//") {
// Enter single-line comment
buffer += jsonString.slice(offset, index);
offset = index;
isInsideComment = singleComment;
index++;
}
else if (isInsideComment === singleComment && currentCharacter + nextCharacter === "\r\n") {
// Exit single-line comment via \r\n
index++;
isInsideComment = false;
buffer += strip(jsonString, offset, index);
offset = index;
continue;
}
else if (isInsideComment === singleComment && currentCharacter === "\n") {
// Exit single-line comment via \n
isInsideComment = false;
buffer += strip(jsonString, offset, index);
offset = index;
}
else if (!isInsideComment && currentCharacter + nextCharacter === "/*") {
// Enter multiline comment
buffer += jsonString.slice(offset, index);
offset = index;
isInsideComment = multiComment;
index++;
continue;
}
else if (isInsideComment === multiComment && currentCharacter + nextCharacter === "*/") {
// Exit multiline comment
index++;
isInsideComment = false;
buffer += strip(jsonString, offset, index + 1);
offset = index + 1;
continue;
}
else if (trailingCommas && !isInsideComment) {
if (commaIndex !== -1) {
if (currentCharacter === "}" || currentCharacter === "]") {
// Strip trailing comma
buffer += jsonString.slice(offset, index);
result += strip(buffer, 0, 1) + buffer.slice(1);
buffer = "";
offset = index;
commaIndex = -1;
}
else if (currentCharacter !== " " &&
currentCharacter !== "\t" &&
currentCharacter !== "\r" &&
currentCharacter !== "\n") {
// Hit non-whitespace following a comma; comma is not trailing
buffer += jsonString.slice(offset, index);
offset = index;
commaIndex = -1;
}
}
else if (currentCharacter === ",") {
// Flush buffer prior to this point, and save new comma index
result += buffer + jsonString.slice(offset, index);
buffer = "";
offset = index;
commaIndex = index;
}
}
}
let results = result +
buffer +
(isInsideComment ? strip(jsonString.slice(offset), undefined, undefined) : jsonString.slice(offset));
results = results.replace(/,(\s*)]/g, "]"); // ["a", "b", ] => ["a", "b"]
results = results.replace(/,(\s*)}/g, "}"); // { "foo": "bar", } => { "foo": "bar"}
return results;
}
/**
* Fixes JSON content for use with comment-json parser.
* Unlike fixJsonContent(), this function:
* - PRESERVES comments (// and /* *\/)
* - Fixes trailing commas
* - Fixes control characters inside strings
*
* Use this when you want to parse JSON with comment-json while still
* handling common JSON issues like trailing commas.
*
* @param jsonString The JSON string to fix
* @returns Fixed JSON string with comments preserved
*/
static fixJsonContentForCommentJson(jsonString) {
if (typeof jsonString !== "string") {
throw new TypeError(`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``);
}
jsonString = jsonString.trim();
let isInsideString = false;
let isInsideComment = false;
// First pass: fix control characters inside strings
for (let index = 0; index < jsonString.length; index++) {
const currentCharacter = jsonString[index];
const nextCharacter = jsonString[index + 1];
if (!isInsideComment && currentCharacter === '"') {
const escaped = Utilities.isEscaped(jsonString, index);
if (!escaped) {
isInsideString = !isInsideString;
}
}
if (isInsideString) {
// fix control characters inside of strings, if they exist
if (currentCharacter === "\r" || currentCharacter === "\n" || currentCharacter === "\t") {
jsonString = jsonString.substring(0, index) + " " + jsonString.substring(index + 1);
}
continue;
}
// Track comment state so we don't break comment content
if (!isInsideComment && currentCharacter + nextCharacter === "//") {
isInsideComment = singleComment;
index++;
}
else if (isInsideComment === singleComment &&
(currentCharacter === "\n" || currentCharacter + nextCharacter === "\r\n")) {
isInsideComment = false;
}
else if (!isInsideComment && currentCharacter + nextCharacter === "/*") {
isInsideComment = multiComment;
index++;
}
else if (isInsideComment === multiComment && currentCharacter + nextCharacter === "*/") {
isInsideComment = false;
index++;
}
}
// Second pass: remove trailing commas (comment-json handles comments, but not trailing commas)
// Use simple patterns without nested quantifiers to avoid exponential backtracking
// First handle simple cases without comments between comma and bracket
jsonString = jsonString.replace(/,(\s*)]/g, "$1]");
jsonString = jsonString.replace(/,(\s*)}/g, "$1}");
// Handle cases with single-line comments between comma and bracket
// Use a non-capturing group with a single quantifier to avoid nested quantifier backtracking
// Match: comma, then any mix of whitespace/comments, then closing bracket
jsonString = jsonString.replace(/,(?=[\t\n ]*(?:\/\/[^\n]*\n[\t\n ]*)*])/g, "");
jsonString = jsonString.replace(/,(?=[\t\n ]*(?:\/\/[^\n]*\n[\t\n ]*)*})/g, "");
// Handle cases with block comments between comma and bracket
// Use lookahead with a block comment pattern that does not allow `*/` inside the body
// Pattern `(?:[^*]|\*(?!\/))*` matches any char except `*`, or `*` not followed by `/`
jsonString = jsonString.replace(/,(?=[\t\n ]*(?:\/\*(?:[^*]|\*(?!\/))*\*\/[\t\n ]*)*])/g, "");
jsonString = jsonString.replace(/,(?=[\t\n ]*(?:\/\*(?:[^*]|\*(?!\/))*\*\/[\t\n ]*)*})/g, "");
return jsonString;
}
static setIsDebug(boolVal) {
Utilities._isDebug = boolVal;
}
static getBaseUrl(url) {
if (url.length < 8) {
return url;
}
const slashIndex = url.indexOf("/", 9);
if (slashIndex < 0) {
return url;
}
return url.substring(0, slashIndex);
}
static getDateStr(date) {
let dateStr = Utilities.frontPadToLength(date.getFullYear(), 4, "0");
dateStr += Utilities.frontPadToLength(date.getMonth() + 1, 2, "0");
dateStr += Utilities.frontPadToLength(date.getDate(), 2, "0");
dateStr += Utilities.frontPadToLength(date.getHours(), 2, "0");
dateStr += Utilities.frontPadToLength(date.getMinutes(), 2, "0");
dateStr += Utilities.frontPadToLength(date.getSeconds(), 2, "0");
return dateStr;
}
static getDateFromStr(dateStr) {
if (!Utilities.isNumeric(dateStr) || dateStr.length !== 14) {
throw new Error("Improperly formatted date string: " + dateStr);
}
const year = parseInt(dateStr.substring(0, 4)), month = parseInt(dateStr.substring(4, 6)), day = parseInt(dateStr.substring(6, 8)), hours = parseInt(dateStr.substring(8, 10)), minutes = parseInt(dateStr.substring(10, 12)), seconds = parseInt(dateStr.substring(12, 14));
Log_1.default.assert(year >= 2022, "Invalid year: " + dateStr);
Log_1.default.assert(month >= 1 && month <= 12, "Invalid month: " + dateStr);
Log_1.default.assert(day >= 0 && day <= 31, "Invalid day: " + dateStr);
Log_1.default.assert(hours >= 0 && hours <= 23, "Invalid hours: " + dateStr);
Log_1.default.assert(minutes >= 0 && minutes <= 59, "Invalid minutes: " + dateStr);
Log_1.default.assert(seconds >= 0 && seconds <= 59, "Invalid seconds: " + dateStr);
return new Date(year, month - 1, day, hours, minutes, seconds);
}
static monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
static monthShortNames = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
static lengthOfDictionary(d) {
let c = 0;
for (const i in d) {
if (i) {
++c;
}
}
return c;
}
static makeSafeForJson(content) {
content = content.replace(/\\/g, "/");
content = content.replace(/"/gi, "'"); // this isn't the full way to do it, but for now...
return content;
}
static isAlphaNumeric(candidate) {
for (let i = 0; i < candidate.length; i++) {
const charCode = candidate[i];
if (!((charCode >= "0" && charCode <= "9") ||
(charCode >= "a" && charCode <= "z") ||
(charCode >= "A" && charCode <= "Z"))) {
return false;
}
}
return true;
}
static getJsonObject(contents) {
let jsonObject = undefined;
contents = Utilities.fixJsonContent(contents);
try {
jsonObject = JSON.parse(contents);
}
catch (e) {
Log_1.default.fail("Could not parse JSON: " + e.message);
}
return jsonObject;
}
static appendErrors(source, add, context) {
if (!add.isInErrorState) {
return;
}
source.isInErrorState = true;
if (add.errorMessages) {
if (source.errorMessages === undefined) {
source.errorMessages = [];
}
for (const err of add.errorMessages) {
let newContext = undefined;
if (context) {
newContext = err.context ? context + ": " + err.context : context;
}
else {
newContext = err.context;
}
source.errorMessages.push({
message: err.message,
context: newContext,
});
}
}
}
static isNumeric(candidate) {
for (let i = 0; i < candidate.length; i++) {
const charCode = candidate[i];
if ((charCode < "0" || charCode > "9") && charCode !== "." && (i > 0 || charCode !== "-")) {
return false;
}
}
return true;
}
static isNumericIsh(candidate) {
for (let i = 0; i < candidate.length; i++) {
const charCode = candidate[i];
if ((charCode < "0" || charCode > "9") &&
charCode !== "e" &&
charCode !== "+" &&
charCode !== "," &&
charCode !== "." &&
charCode !== "-") {
return false;
}
}
return true;
}
static removeQuotes(candidate) {
candidate = candidate.trim();
if (candidate.startsWith('"')) {
candidate = candidate.substring(1);
}
else if (candidate.startsWith('"')) {
candidate = candidate.substring(0, candidate.length - 1);
}
return candidate;
}
static normalizeVersionString(candidate) {
candidate = candidate.replace(/v/gi, "").trim();
candidate = candidate.trim();
if (candidate.startsWith(".")) {
candidate = "0" + candidate;
}
if (candidate.endsWith(".")) {
candidate = candidate.substring(0, candidate.length - 1);
}
return candidate;
}
static isVersionString(candidate) {
for (let i = 0; i < candidate.length; i++) {
const charCode = candidate[i];
if ((charCode < "0" || charCode > "9") && charCode !== "." && charCode !== "v") {
return false;
}
}
return true;
}
static isAlpha(candidate) {
for (let i = 0; i < candidate.length; i++) {
const charCode = candidate[i];
if (!((charCode >= "a" && charCode <= "z") || (charCode >= "A" && charCode <= "Z"))) {
return false;
}
}
return true;
}
static shallowCloneArray(source) {
const newArr = new Array(source.length);
for (let i = 0; i < source.length; i++) {
newArr[i] = source[i];
}
return newArr;
}
static uint8ArrayToBase64(bytes) {
let binary = "";
const len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
if (CreatorToolsHost_1.default.isWeb ||
CreatorToolsHost_1.default.hostType === CreatorToolsHost_1.HostType.vsCodeMainWeb ||
CreatorToolsHost_1.default.hostType === CreatorToolsHost_1.HostType.vsCodeWebService) {
return btoa(binary);
}
// See arrayBufferToBase64 below — `Buffer.from(string)` defaults to
// UTF-8 and will corrupt any byte >= 0x80. Pass the Uint8Array directly.
return Buffer.from(bytes).toString("base64");
}
static arrayBufferToBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
if (CreatorToolsHost_1.default.isWeb ||
CreatorToolsHost_1.default.hostType === CreatorToolsHost_1.HostType.vsCodeMainWeb ||
CreatorToolsHost_1.default.hostType === CreatorToolsHost_1.HostType.vsCodeWebService) {
return btoa(binary);
}
// IMPORTANT: `Buffer.from(str)` defaults to UTF-8, which corrupts any
// byte >= 0x80 (turning it into a 0xC3 0xXX two-byte sequence). We've
// built `binary` via `String.fromCharCode(byte)` so each char already
// holds a single byte value 0..255 — pass the Uint8Array directly to
// avoid the UTF-8 round-trip.
return Buffer.from(bytes).toString("base64");
}
static base64ToArrayBuffer(base64buffer) {
const start = base64buffer.indexOf(";base64,");
if (start > 0 && start < 50) {
base64buffer = base64buffer.substring(start + 8);
}
const binary = atob(base64buffer);
const arrayBuffer = new ArrayBuffer(binary.length);
const bytes = new Uint8Array(arrayBuffer);
const len = binary.length;
for (var i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return arrayBuffer;
}
static base64ToUint8Array(base64buffer) {
const start = base64buffer.indexOf(";base64,");
if (start > 0 && start < 50) {
base64buffer = base64buffer.substring(start + 8);
}
const binary = atob(base64buffer);
const arrayBuffer = new ArrayBuffer(binary.length);
const bytes = new Uint8Array(arrayBuffer);
const len = binary.length;
for (var i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
// UTF8 related string functions adapted from StrangelyTyped/StringView.
static _utf8ReadChar = function (charStruct, buf, readPos, maxBytes) {
const firstByte = buf.getUint8(readPos);
charStruct.bytesRead = 1;
charStruct.charVal = 0;
if (firstByte & 0x80) {
var numBytes = 0;
var aByte = firstByte;
while (aByte & 0x80) {
numBytes++;
aByte <<= 1;
}
if (numBytes === 1) {
charStruct.charVal = Utilities.replacementChar;
return;
}
if (numBytes > maxBytes) {
charStruct.charVal = Utilities.replacementChar;
return;
}
//2 bytes means 3 bits reserved for UTF8 byte encoding, 5 bytes remaining for codepoint, and so on
charStruct.charVal = firstByte & (0xff >> (numBytes + 1));
for (var i = 1; i < numBytes; i++) {
aByte = buf.getUint8(readPos + i);
//0xC0 should isolate the continuation flag which should be 0x80
if ((aByte & 0xc0) !== 0x80) {
// Use Log.verbose instead of console.error - this is expected when parsing binary
// data that contains non-UTF8 byte sequences (common in NBT files, LevelDB, etc.)
Log_1.default.verbose("UTF-8 read -