UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,317 lines 65.7 kB
"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 -