diff --git a/.gitignore b/.gitignore index 1bf3c29..1f12174 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules -*.lua .DS_Store event/ build/ +reference/ diff --git a/.justfile b/.justfile index 54a679b..b2199e4 100644 --- a/.justfile +++ b/.justfile @@ -1,9 +1,16 @@ set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] -build: build-autocraft sync +build: build-autocraft build-accesscontrol build-test sync build-autocraft: - pnpm build-autocraft + pnpm tstl -p ./tsconfig.autocraft.json + +build-accesscontrol: + pnpm tstl -p ./tsconfig.accesscontrol.json + cp ./src/accesscontrol/access.config.json ./build/ + +build-test: + pnpm tstl -p ./tsconfig.test.json sync: cp -r "./build/*" "C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\\" diff --git a/package.json b/package.json index 11bd692..21a847a 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,17 @@ "main": "main.ts", "scripts": { "build": "tstl", - "build-autocraft": "tstl -p ./tsconfig.autocraft.json", "sync": "cp -r \"./build/*\" \"C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\"" }, "devDependencies": { "@eslint/js": "^9.36.0", - "@sikongjueluo/advanced-peripherals-types": "file:types/advanced-peripherals", "@jackmacwindows/cc-types": "file:types/cc", "@jackmacwindows/craftos-types": "file:types/craftos", "@jackmacwindows/lua-types": "^2.13.2", "@jackmacwindows/typescript-to-lua": "^1.28.1", + "@sikongjueluo/advanced-peripherals-types": "file:types/advanced-peripherals", + "@sikongjueluo/toml2lua-types": "file:types/toml2lua", + "@sikongjueluo/dkjson-types": "file:types/dkjson", "@typescript-to-lua/language-extensions": "^1.19.0", "eslint": "^9.36.0", "typescript": "^5.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 799e3ac..27e23bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: '@sikongjueluo/advanced-peripherals-types': specifier: file:types/advanced-peripherals version: file:types/advanced-peripherals + '@sikongjueluo/dkjson-types': + specifier: file:types/dkjson + version: file:types/dkjson + '@sikongjueluo/toml2lua-types': + specifier: file:types/toml2lua + version: file:types/toml2lua '@typescript-to-lua/language-extensions': specifier: ^1.19.0 version: 1.19.0 @@ -126,6 +132,12 @@ packages: '@sikongjueluo/advanced-peripherals-types@file:types/advanced-peripherals': resolution: {directory: types/advanced-peripherals, type: directory} + '@sikongjueluo/dkjson-types@file:types/dkjson': + resolution: {directory: types/dkjson, type: directory} + + '@sikongjueluo/toml2lua-types@file:types/toml2lua': + resolution: {directory: types/toml2lua, type: directory} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -677,6 +689,10 @@ snapshots: '@sikongjueluo/advanced-peripherals-types@file:types/advanced-peripherals': {} + '@sikongjueluo/dkjson-types@file:types/dkjson': {} + + '@sikongjueluo/toml2lua-types@file:types/toml2lua': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..fc2d9bb --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - core-js-pure diff --git a/src/accesscontrol/access.config.json b/src/accesscontrol/access.config.json new file mode 100644 index 0000000..9a75614 --- /dev/null +++ b/src/accesscontrol/access.config.json @@ -0,0 +1,67 @@ +{ + "detectRange": 64, + "detectInterval": 1, + "warnInterval": 7, + "adminGroupConfig": { + "groupName": "Admin", + "groupUsers": ["Selcon"], + "isAllowed": true, + "isWarnTarget": true + }, + "defaultToastConfig": { + "title": { + "text": "Welcome", + "color": "green" + }, + "msg": { + "text": "Hello %groupName% %playerName%", + "color": "green" + }, + "prefix": "桃花源", + "brackets": "[]", + "bracketColor": "" + }, + "warnToastConfig": { + "title": { + "text": "Attention!!!", + "color": "red" + }, + "msg": { + "text": "%playerName% you are not allowed to be here", + "color": "red" + }, + "prefix": "Taohuayuan", + "brackets": "[]", + "bracketColor": "" + }, + "usersGroups": [ + { + "groupName": "user", + "groupUsers": [], + "isAllowed": true, + "isWarnTarget": true + }, + { + "groupName": "VIP", + "groupUsers": [], + "isAllowed": true, + "isWarnTarget": false + }, + { + "groupName": "enemy", + "groupUsers": [], + "isAllowed": false, + "isWarnTarget": false, + "toastConfig": { + "title": { + "text": "Warn", + "color": "red" + }, + "msg": { + "text": "Warn %playerName%", + "color": "red" + } + } + } + ] +} diff --git a/src/accesscontrol/cli.ts b/src/accesscontrol/cli.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/accesscontrol/config.ts b/src/accesscontrol/config.ts new file mode 100644 index 0000000..315690f --- /dev/null +++ b/src/accesscontrol/config.ts @@ -0,0 +1,158 @@ +import { CCLog } from "@/lib/ccLog"; +import * as dkjson from "@sikongjueluo/dkjson-types"; + +let log: CCLog | undefined; + +interface ToastConfig { + title: MinecraftTextComponent; + msg: MinecraftTextComponent; + prefix?: string; + brackets?: string; + bracketColor?: string; +} + +interface UserGroupConfig { + groupName: string; + isAllowed: boolean; + isWarnTarget: boolean; + groupUsers: string[]; + toastConfig?: ToastConfig; +} + +interface AccessConfig { + detectInterval: number; + warnInterval: number; + detectRange: number; + adminGroupConfig: UserGroupConfig; + defaultToastConfig: ToastConfig; + warnToastConfig: ToastConfig; + usersGroups: UserGroupConfig[]; +} + +const defaultConfig: AccessConfig = { + detectRange: 64, + detectInterval: 3, + warnInterval: 7, + adminGroupConfig: { + groupName: "Admin", + groupUsers: ["Selcon"], + isAllowed: true, + isWarnTarget: false, + }, + usersGroups: [ + { + groupName: "user", + groupUsers: [], + isAllowed: true, + isWarnTarget: true, + }, + { + groupName: "VIP", + groupUsers: [], + isAllowed: true, + isWarnTarget: false, + }, + { + groupName: "enemies", + groupUsers: [], + isAllowed: false, + isWarnTarget: false, + toastConfig: { + title: { + text: "Warn", + color: "red", + }, + msg: { + text: "Warn %playerName%", + color: "red", + }, + }, + }, + ], + defaultToastConfig: { + title: { + text: "Welcome", + color: "green", + }, + msg: { + text: "Hello User %playerName%", + color: "green", + }, + prefix: "Taohuayuan", + brackets: "[]", + bracketColor: "", + }, + warnToastConfig: { + title: { + text: "Attention!!!", + color: "red", + }, + msg: { + text: "%playerName% you are not allowed to be here", + color: "red", + }, + prefix: "Taohuayuan", + brackets: "[]", + bracketColor: "", + }, +}; + +function setLog(newLog: CCLog) { + log = newLog; +} + +function loadConfig(filepath: string): AccessConfig { + const [fp] = io.open(filepath, "r"); + if (fp == undefined) { + print("Failed to open config file " + filepath); + return defaultConfig; + } + + const configJson = fp.read("*a"); + if (configJson == undefined) { + print("Failed to read config file"); + return defaultConfig; + } + + const [config, pos, err] = dkjson.decode(configJson); + if (config == undefined) { + log?.warn( + `Config decode failed at ${pos}, use default instead. Error :${err}`, + ); + return defaultConfig; + } + + // Not use external lib + // const config = textutils.unserialiseJSON(configJson, { + // parse_empty_array: true, + // }); + + return config as AccessConfig; +} + +function saveConfig(config: AccessConfig, filepath: string) { + const configJson = dkjson.encode(config, { indent: true }) as string; + // Not use external lib + // const configJson = textutils.serializeJSON(config, { unicode_strings: true }); + if (configJson == undefined) { + print("Failed to save config"); + } + + const [fp, _err] = io.open(filepath, "w+"); + if (fp == undefined) { + print("Failed to open config file " + filepath); + return; + } + + fp.write(configJson); + fp.close(); +} + +export { + ToastConfig, + UserGroupConfig, + AccessConfig, + loadConfig, + saveConfig, + setLog, +}; diff --git a/src/accesscontrol/main.ts b/src/accesscontrol/main.ts new file mode 100644 index 0000000..aeeee40 --- /dev/null +++ b/src/accesscontrol/main.ts @@ -0,0 +1,355 @@ +import { CCLog, DAY } from "@/lib/ccLog"; +import { + ToastConfig, + UserGroupConfig, + loadConfig, + saveConfig, + setLog, +} from "./config"; +import * as peripheralManager from "../lib/PeripheralManager"; +import { ChatBoxEvent, pullEventAs } from "@/lib/event"; +import { quotestring } from "@sikongjueluo/dkjson-types"; + +const DEBUG = false; +const args = [...$vararg]; + +const log = new CCLog("accesscontrol.log", DAY); +setLog(log); + +const configFilepath = `${shell.dir()}/access.config.json`; +const config = loadConfig(configFilepath); +log.info("Load config successfully!"); +log.debug(textutils.serialise(config, { allow_repetitions: true })); +const groupNames = config.usersGroups.map((value) => value.groupName); +const warnTargetPlayers = config.adminGroupConfig.groupUsers.concat( + config.usersGroups + .filter((value) => value.isWarnTarget) + .map((value) => value.groupUsers ?? []) + .flat(), +); + +const playerDetector = peripheralManager.findByNameRequired("playerDetector"); +const chatBox = peripheralManager.findByNameRequired("chatBox"); + +let inRangePlayers: string[] = []; +let notAllowedPlayers: string[] = []; + +function safeParseTextComponent( + component: MinecraftTextComponent, + playerName: string, + groupName?: string, +): string { + if (component.text == undefined) { + component.text = "Wrong text, please contanct with admin"; + } else if (component.text.includes("%")) { + component.text = component.text.replace("%playerName%", playerName); + if (groupName != undefined) + component.text = component.text.replace("%groupName%", groupName); + } + return textutils.serialiseJSON(component); +} + +function sendToast( + toastConfig: ToastConfig, + player: string, + groupConfig?: UserGroupConfig, +) { + return chatBox.sendFormattedToastToPlayer( + safeParseTextComponent( + toastConfig.msg ?? config.defaultToastConfig.msg, + player, + groupConfig?.groupName, + ), + safeParseTextComponent( + toastConfig.title ?? config.defaultToastConfig.title, + player, + groupConfig?.groupName, + ), + player, + quotestring(toastConfig.prefix ?? config.defaultToastConfig.prefix!), + toastConfig.brackets ?? config.defaultToastConfig.brackets, + toastConfig.bracketColor ?? config.defaultToastConfig.bracketColor, + undefined, + true, + ); +} + +function sendWarn(player: string) { + const playerPos = playerDetector.getPlayerPos(player); + const onlinePlayers = playerDetector.getOnlinePlayers(); + + const warnMsg = `Not Allowed Player ${player} Break in Home at Position ${playerPos?.x}, ${playerPos?.y}, ${playerPos?.z}`; + log.warn(warnMsg); + sendToast(config.warnToastConfig, player); + chatBox.sendFormattedMessageToPlayer( + safeParseTextComponent(config.warnToastConfig.msg, player), + player, + "AccessControl", + "[]", + undefined, + undefined, + true, + ); + + for (const targetPlayer of warnTargetPlayers) { + if (!onlinePlayers.includes(targetPlayer)) continue; + chatBox.sendFormattedMessageToPlayer( + textutils.serialise({ + text: warnMsg, + color: "red", + } as MinecraftTextComponent), + targetPlayer, + "AccessControl", + "[]", + undefined, + undefined, + true, + ); + } +} + +function sendCommandHelp(targetPlayer: string) { + chatBox.sendMessageToPlayer( + ` + Command Usage: @AC / [args] + Command: + - add + add player to group + userGroup: ${groupNames.join(", ")} + - del + delete player in the group, except Admin + userGroup: ${groupNames.join(", ")} + - list + list all of the player with its group + - set [params] + config access control settins + options: + - warnInterval + set the interval of warn, which is not allowed + - detectInterval + set the interval of detecting players + - detectRange + set the sphere range of detect + `, + targetPlayer, + "AccessControl", + "[]", + undefined, + undefined, + true, + ); +} + +function warnLoop() { + while (true) { + for (const player of notAllowedPlayers) { + if (inRangePlayers.includes(player)) { + sendWarn(player); + } else { + notAllowedPlayers = notAllowedPlayers.filter( + (value) => value != player, + ); + } + } + + os.sleep(config.warnInterval); + } +} + +function mainLoop() { + while (true) { + const players = playerDetector.getPlayersInRange(config.detectRange); + if (DEBUG) { + const playersList = "[ " + players.join(",") + " ]"; + log.debug(`Detected ${players.length} players: ${playersList}`); + } + + for (const player of players) { + if (inRangePlayers.includes(player)) continue; + + if (config.adminGroupConfig.groupUsers.includes(player)) { + log.info(`Admin ${player} enter`); + sendToast( + config.adminGroupConfig.toastConfig ?? config.defaultToastConfig, + player, + config.adminGroupConfig, + ); + continue; + } + + let inUserGroup = false; + for (const userGroupConfig of config.usersGroups) { + if (userGroupConfig.groupUsers == undefined) continue; + if (!userGroupConfig.groupUsers.includes(player)) continue; + + if (!userGroupConfig.isAllowed) { + sendWarn(player); + notAllowedPlayers.push(player); + continue; + } + + log.info(`${userGroupConfig.groupName} ${player} enter`); + sendToast( + userGroupConfig.toastConfig ?? config.defaultToastConfig, + player, + userGroupConfig, + ); + + inUserGroup = true; + } + if (inUserGroup) continue; + + sendWarn(player); + notAllowedPlayers.push(player); + } + + inRangePlayers = players; + os.sleep(config.detectInterval); + } +} + +function configLoop() { + while (true) { + const ev = pullEventAs(ChatBoxEvent, "chat"); + + if (ev == undefined) continue; + if (!config.adminGroupConfig.groupUsers.includes(ev.username)) continue; + if (!ev.message.startsWith("@AC")) continue; + // log.info(`Received "${ev.message}" from admin ${ev.username}`); + + const params = ev.message.split(" "); + if (params.length < 2) { + sendCommandHelp(ev.username); + continue; + } + + if (params[1] == "/add" && params.length == 4) { + if (params[2] == "admin") { + config.adminGroupConfig.groupUsers.push(params[3]); + chatBox.sendMessageToPlayer( + `Add player ${params[3]} to admin`, + ev.username, + "AccessControl", + ); + } else if (groupNames.includes(params[2])) { + const groupConfig = config.usersGroups.find( + (value) => value.groupName == params[2], + )!; + + if (groupConfig.groupUsers == undefined) + groupConfig.groupUsers = [params[3]]; + else groupConfig.groupUsers.push(params[3]); + + chatBox.sendMessageToPlayer( + `Add player ${params[3]} to ${groupConfig.groupName}`, + ev.username, + "AccessControl", + ); + } else { + sendCommandHelp(ev.username); + continue; + } + } else if (params[1] == "/del" && params.length == 4) { + if (params[2] == "admin") { + chatBox.sendMessageToPlayer( + `Could't delete admin, please edit config`, + ev.username, + "AccessControl", + ); + } else if (groupNames.includes(params[2])) { + const groupConfig = config.usersGroups.find( + (value) => value.groupName == params[2], + )!; + + if (groupConfig.groupUsers == undefined) groupConfig.groupUsers = []; + else + groupConfig.groupUsers = groupConfig.groupUsers.filter( + (user) => user != params[3], + ); + + chatBox.sendMessageToPlayer( + `Delete ${groupConfig.groupName} ${params[3]}`, + ev.username, + "AccessControl", + ); + } else { + sendCommandHelp(ev.username); + continue; + } + } else if (params[1] == "/list") { + chatBox.sendMessageToPlayer( + `Admins : [ ${config.adminGroupConfig.groupUsers.join(", ")} ]`, + ev.username, + "AccessControl", + ); + for (const groupConfig of config.usersGroups) { + chatBox.sendMessageToPlayer( + `${groupConfig.groupName} : [ ${config.adminGroupConfig.groupUsers.join(", ")} ]`, + ev.username, + "AccessControl", + ); + } + } else if (params[1] == "/set" && params.length == 4) { + if (params[2] == "warnInterval") { + config.warnInterval = parseInt(params[3]); + chatBox.sendMessageToPlayer( + `Set warn interval to ${config.warnInterval}`, + ev.username, + "AccessControl", + ); + } else if (params[2] == "detectInterval") { + config.detectInterval = parseInt(params[3]); + chatBox.sendMessageToPlayer( + `Set detect interval to ${config.detectInterval}`, + ev.username, + "AccessControl", + ); + } else if (params[2] == "detectRange") { + config.detectRange = parseInt(params[3]); + chatBox.sendMessageToPlayer( + `Set detect range to ${config.detectRange}`, + ev.username, + "AccessControl", + ); + } else { + sendCommandHelp(ev.username); + continue; + } + } else { + sendCommandHelp(ev.username); + continue; + } + + saveConfig(config, configFilepath); + } +} + +function main(args: string[]) { + log.debug("Starting access control system, get args: " + args.join(", ")); + if (args.length == 1) { + if (args[0] == "start") { + parallel.waitForAll( + () => { + mainLoop(); + }, + () => { + configLoop(); + }, + () => { + warnLoop(); + }, + ); + return; + } + } + + print(`Usage: accesscontrol start`); +} + +try { + main(args); +} catch (error: unknown) { + log.error(textutils.serialise(error as object)); +} finally { + log.close(); +} diff --git a/src/autocraft/main.ts b/src/autocraft/main.ts index bc49c97..89a9707 100644 --- a/src/autocraft/main.ts +++ b/src/autocraft/main.ts @@ -58,7 +58,7 @@ function main() { // Get package NBT packagesContainer.pushItems(turtleLocalName, slot); - const packageInfo = blockReader.getBlockData().Items[1]; + const packageInfo = blockReader.getBlockData()!.Items[1]; // log.info(textutils.serialise(packageInfo)); // Get recipe @@ -96,7 +96,7 @@ function main() { restCraftCnt -= craftCnt; // Get output item - craftOutputItem ??= blockReader.getBlockData().Items[1]; + craftOutputItem ??= blockReader.getBlockData()!.Items[1]; } while (restCraftCnt > 0); // Finally output diff --git a/src/lib/PeripheralManager.ts b/src/lib/PeripheralManager.ts index 152f1e2..5affe3d 100644 --- a/src/lib/PeripheralManager.ts +++ b/src/lib/PeripheralManager.ts @@ -1,43 +1,62 @@ -type PeripheralType = "inventory" | "modem" | "wiredModem" | "blockReader"; +type PeripheralType = + | "inventory" + | "modem" + | "wiredModem" + | "blockReader" + | "chatBox" + | "playerDetector"; type BlockSide = "top" | "bottom" | "left" | "right" | "front" | "back"; // Declare the function signature for findBySide function findByName( devType: "inventory", - devName: string, + devName?: string, ): InventoryPeripheral | undefined; function findByName( devType: "modem", - devName: string, + devName?: string, ): ModemPeripheral | undefined; function findByName( devType: "wiredModem", - devName: string, + devName?: string, ): WiredModemPeripheral | undefined; function findByName( devType: "blockReader", - devName: string, + devName?: string, ): BlockReaderPeripheral | undefined; +function findByName( + devType: "chatBox", + devName?: string, +): ChatBoxPeripheral | undefined; +function findByName( + devType: "playerDetector", + devName?: string, +): PlayerDetectorPeripheral | undefined; function findByName( devType: PeripheralType, side: BlockSide, ): IPeripheral | undefined; function findByName( devType: PeripheralType, - devName: string, + devName?: string, ): IPeripheral | undefined; -// Implement the function signature for findBySide +// Implement the function signature for findByName function findByName( devType: PeripheralType, - devName: string, + devName?: string, ): IPeripheral | undefined { - const dev = peripheral.find( - devType == "wiredModem" ? "modem" : devType, - (name: string, _) => { - return name == devName; - }, - )[0]; + let dev; + if (devName == undefined) { + dev = peripheral.find(devType)[0]; + } else { + dev = peripheral.find( + devType == "wiredModem" ? "modem" : devType, + (name: string, _) => { + return name == devName; + }, + )[0]; + } // Seperate Modem and wiredModem if ( @@ -54,41 +73,54 @@ function findByName( ) return undefined; + if (dev != undefined && peripheral.getType(dev) != devType) return undefined; + return dev; } // Declare the function signature for findBySideRequired function findByNameRequired( devType: "inventory", - devName: string, + devName?: string, ): InventoryPeripheral; -function findByNameRequired(devType: "modem", devName: string): ModemPeripheral; +function findByNameRequired( + devType: "modem", + devName?: string, +): ModemPeripheral; function findByNameRequired( devType: "wiredModem", - devName: string, + devName?: string, ): WiredModemPeripheral; function findByNameRequired( devType: "blockReader", - devName: string, + devName?: string, ): BlockReaderPeripheral; +function findByNameRequired( + devType: "chatBox", + devName?: string, +): ChatBoxPeripheral; +function findByNameRequired( + devType: "playerDetector", + devName?: string, +): PlayerDetectorPeripheral; function findByNameRequired( devType: PeripheralType, side: BlockSide, ): T; function findByNameRequired( devType: PeripheralType, - devName: string, + devName?: string, ): T; // Implement the function signature for findBySideRequired function findByNameRequired( devType: PeripheralType, - side: string, + devName?: string, ): T { - const dev = findByName(devType, side); - if (!dev) { + const dev = findByName(devType, devName); + if (dev == undefined) { throw new Error( - `Required peripheral of type '${devType}' not found on side '${side}'`, + `Required peripheral of type '${devType}' not found with name '${devName}'`, ); } return dev as T; diff --git a/src/lib/ccLog.ts b/src/lib/ccLog.ts index 03c74cd..b131265 100644 --- a/src/lib/ccLog.ts +++ b/src/lib/ccLog.ts @@ -1,16 +1,35 @@ enum LogLevel { - Info = 0, - Warn = 1, - Error = 2, + Debug = 0, + Info = 1, + Warn = 2, + Error = 3, } +// Define time interval constants in seconds +export const SECOND = 1; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; +export const WEEK = 7 * DAY; + export class CCLog { private fp: LuaFile | undefined; - constructor(filename?: string) { + private filename?: string; + private interval: number; + private startTime: number; + private currentTimePeriod: string; + + constructor(filename?: string, interval: number = DAY) { term.clear(); term.setCursorPos(1, 1); + + this.interval = interval; + this.startTime = os.time(os.date("*t")); + this.currentTimePeriod = this.getTimePeriodString(this.startTime); + if (filename != undefined && filename.length != 0) { - const filepath = shell.dir() + "/" + filename; + this.filename = filename; + const filepath = this.generateFilePath(filename, this.currentTimePeriod); const [file, error] = io.open(filepath, fs.exists(filepath) ? "a" : "w+"); if (file != undefined) { this.fp = file; @@ -20,12 +39,84 @@ export class CCLog { } } + /** + * Generates a time period string based on the interval + * For DAY interval: YYYY-MM-DD + * For HOUR interval: YYYY-MM-DD-HH + * For MINUTE interval: YYYY-MM-DD-HH-MM + * For SECOND interval: YYYY-MM-DD-HH-MM-SS + */ + private getTimePeriodString(time: number): string { + // Calculate which time period this timestamp falls into + const periodStart = Math.floor(time / this.interval) * this.interval; + const periodDate = os.date("*t", periodStart); + + if (this.interval >= DAY) { + return `${periodDate.year}-${String(periodDate.month).padStart(2, "0")}-${String(periodDate.day).padStart(2, "0")}`; + } else { + return `[${periodDate.year}-${String(periodDate.month).padStart(2, "0")}-${String(periodDate.day).padStart(2, "0")}] - [${String(periodDate.hour).padStart(2, "0")}-${String(periodDate.min).padStart(2, "0")}-${String(periodDate.sec).padStart(2, "0")}]`; + } + } + + private generateFilePath(baseFilename: string, timePeriod: string): string { + // Extract file extension if present + const fileNameSubStrings = baseFilename.split("."); + let filenameWithoutExt: string; + let extension = ""; + + if (fileNameSubStrings.length > 1) { + filenameWithoutExt = fileNameSubStrings[0]; + extension = fileNameSubStrings[1]; + } else { + filenameWithoutExt = baseFilename; + } + + return `${shell.dir()}/${filenameWithoutExt}[${timePeriod}].${extension}`; + } + + private checkAndRotateLogFile() { + if (this.filename != undefined && this.filename.length != 0) { + const currentTime = os.time(os.date("*t")); + const currentTimePeriod = this.getTimePeriodString(currentTime); + + // If we're in a new time period, rotate the log file + if (currentTimePeriod !== this.currentTimePeriod) { + // Close current file if open + if (this.fp) { + this.fp.close(); + this.fp = undefined; + } + + // Update the current time period + this.currentTimePeriod = currentTimePeriod; + + // Open new log file for the new time period + const filepath = this.generateFilePath( + this.filename, + this.currentTimePeriod, + ); + const [file, error] = io.open( + filepath, + fs.exists(filepath) ? "a" : "w+", + ); + if (file != undefined) { + this.fp = file; + } else { + throw Error(error); + } + } + } + } + private getFormatMsg(msg: string, level: LogLevel): string { const date = os.date("*t"); - return `[ ${date.year}/${date.month}/${date.day} -- ${date.hour}:${date.min}:${date.sec} ${LogLevel[level]} ] : ${msg}\r\n`; + return `[ ${date.year}/${String(date.month).padStart(2, "0")}/${String(date.day).padStart(2, "0")} ${String(date.hour).padStart(2, "0")}:${String(date.min).padStart(2, "0")}:${String(date.sec).padStart(2, "0")} ${LogLevel[level]} ] : ${msg}`; } public writeLine(msg: string, color?: Color) { + // Check if we need to rotate the log file + this.checkAndRotateLogFile(); + let originalColor: Color = 0; if (color != undefined) { originalColor = term.getTextColor(); @@ -33,17 +124,18 @@ export class CCLog { } // Log - term.write(msg); + print(msg); if (this.fp != undefined) { - this.fp.write(msg); + this.fp.write(msg + "\r\n"); } if (color != undefined) { term.setTextColor(originalColor); } + } - // Next line - term.setCursorPos(1, term.getCursorPos()[1] + 1); + public debug(msg: string) { + this.writeLine(this.getFormatMsg(msg, LogLevel.Debug), colors.gray); } public info(msg: string) { diff --git a/src/lib/ccTime.ts b/src/lib/ccTime.ts new file mode 100644 index 0000000..33b0ff3 --- /dev/null +++ b/src/lib/ccTime.ts @@ -0,0 +1,25 @@ +class ccDate { + private _timestamp: number; + + constructor() { + this._timestamp = os.time(os.date("*t")); + } + + public static toDateTable(timestamp: number): LuaDate { + return os.date("*t", timestamp) as LuaDate; + } + + public toDateTable(): LuaDate { + return os.date("*t", this._timestamp) as LuaDate; + } + + public static toTimestamp(date: LuaDate): number { + return os.time(date); + } + + public toTimestamp(): number { + return this._timestamp; + } +} + +export { ccDate }; diff --git a/src/lib/event.ts b/src/lib/event.ts index 4bc170b..631871f 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -2,312 +2,461 @@ // delete them from eventInitializers as well. export interface IEvent { - get_name(): string; - get_args(): any[]; + get_name(): string; + get_args(): unknown[]; } export class CharEvent implements IEvent { - public character: string = ""; - public get_name() {return "char";} - public get_args() {return [this.character];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "char") return null; - let ev = new CharEvent(); - ev.character = (args[1] as string); - return ev; - } + public character = ""; + public get_name() { + return "char"; + } + public get_args() { + return [this.character]; + } + public static init(args: unknown[]): IEvent | undefined { + if (!(typeof args[0] === "string") || args[0] != "char") return undefined; + const ev = new CharEvent(); + ev.character = args[1] as string; + return ev; + } } export class KeyEvent implements IEvent { - public key: Key = 0; - public isHeld: boolean = false; - public isUp: boolean = false; - public get_name() {return this.isUp ? "key_up" : "key";} - public get_args() {return [this.key, (this.isUp ? null : this.isHeld)];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || ((args[0] as string) != "key" && (args[0] as string) != "key_up")) return null; - let ev = new KeyEvent(); - ev.key = (args[1] as number); - ev.isUp = (args[0] as string) == "key_up"; - ev.isHeld = ev.isUp ? false : (args[2] as boolean); - return ev; - } + public key: Key = 0; + public isHeld = false; + public isUp = false; + public get_name() { + return this.isUp ? "key_up" : "key"; + } + public get_args() { + return [this.key, this.isUp ? undefined : this.isHeld]; + } + public static init(args: unknown[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + (args[0] != "key" && args[0] != "key_up") + ) + return undefined; + const ev = new KeyEvent(); + ev.key = args[1] as number; + ev.isUp = (args[0] as string) == "key_up"; + ev.isHeld = ev.isUp ? false : (args[2] as boolean); + return ev; + } } export class PasteEvent implements IEvent { - public text: string = ""; - public get_name() {return "paste";} - public get_args() {return [(this.text as any)];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "paste") return null; - let ev = new PasteEvent(); - ev.text = (args[1] as string); - return ev; - } + public text = ""; + public get_name() { + return "paste"; + } + public get_args() { + return [this.text]; + } + public static init(args: unknown[]): IEvent | undefined { + if (!(typeof args[0] === "string") || args[0] != "paste") return undefined; + const ev = new PasteEvent(); + ev.text = args[1] as string; + return ev; + } } export class TimerEvent implements IEvent { - public id: number = 0; - public isAlarm: boolean = false; - public get_name() {return this.isAlarm ? "alarm" : "timer";} - public get_args() {return [this.id];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || ((args[0] as string) != "timer" && (args[0] as string) != "alarm")) return null; - let ev = new TimerEvent(); - ev.id = (args[1] as number); - ev.isAlarm = (args[0] as string) == "alarm"; - return ev; - } + public id = 0; + public isAlarm = false; + public get_name() { + return this.isAlarm ? "alarm" : "timer"; + } + public get_args() { + return [this.id]; + } + public static init(args: unknown[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + (args[0] != "timer" && args[0] != "alarm") + ) + return undefined; + const ev = new TimerEvent(); + ev.id = args[1] as number; + ev.isAlarm = args[0] == "alarm"; + return ev; + } } export class TaskCompleteEvent implements IEvent { - public id: number = 0; - public success: boolean = false; - public error: string | null = null; - public params: any[] = []; - public get_name() {return "task_complete";} - public get_args() { - if (this.success) return [this.id, this.success].concat(this.params); - else return [this.id, this.success, this.error]; - } - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "task_complete") return null; - let ev = new TaskCompleteEvent(); - ev.id = (args[1] as number); - ev.success = (args[2] as boolean); - if (ev.success) { - ev.error = null; - ev.params = args.slice(3); - } else { - ev.error = (args[3] as string); - ev.params = []; - } - return ev; + public id = 0; + public success = false; + public error: string | undefined = undefined; + public params: any[] = []; + public get_name() { + return "task_complete"; + } + public get_args() { + if (this.success) return [this.id, this.success].concat(this.params); + else return [this.id, this.success, this.error]; + } + public static init(args: unknown[]): IEvent | undefined { + if (!(typeof args[0] === "string") || args[0] != "task_complete") + return undefined; + const ev = new TaskCompleteEvent(); + ev.id = args[1] as number; + ev.success = args[2] as boolean; + if (ev.success) { + ev.error = undefined; + ev.params = args.slice(3); + } else { + ev.error = args[3] as string; + ev.params = []; } + return ev; + } } export class RedstoneEvent implements IEvent { - public get_name() {return "redstone";} - public get_args() {return [];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "redstone") return null; - let ev = new RedstoneEvent(); - return ev; - } + public get_name() { + return "redstone"; + } + public get_args() { + return []; + } + public static init(args: any[]): IEvent | undefined { + if (!(typeof args[0] === "string") || (args[0] as string) != "redstone") + return undefined; + let ev = new RedstoneEvent(); + return ev; + } } export class TerminateEvent implements IEvent { - public get_name() {return "terminate";} - public get_args() {return [];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "terminate") return null; - let ev = new TerminateEvent(); - return ev; - } + public get_name() { + return "terminate"; + } + public get_args() { + return []; + } + public static init(args: any[]): IEvent | undefined { + if (!(typeof args[0] === "string") || (args[0] as string) != "terminate") + return undefined; + let ev = new TerminateEvent(); + return ev; + } } export class DiskEvent implements IEvent { - public side: string = ""; - public eject: boolean = false; - public get_name() {return this.eject ? "disk_eject" : "disk";} - public get_args() {return [this.side];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || ((args[0] as string) != "disk" && (args[0] as string) != "disk_eject")) return null; - let ev = new DiskEvent(); - ev.side = (args[1] as string); - ev.eject = (args[0] as string) == "disk_eject"; - return ev; - } + public side: string = ""; + public eject: boolean = false; + public get_name() { + return this.eject ? "disk_eject" : "disk"; + } + public get_args() { + return [this.side]; + } + public static init(args: any[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + ((args[0] as string) != "disk" && (args[0] as string) != "disk_eject") + ) + return undefined; + let ev = new DiskEvent(); + ev.side = args[1] as string; + ev.eject = (args[0] as string) == "disk_eject"; + return ev; + } } export class PeripheralEvent implements IEvent { - public side: string = ""; - public detach: boolean = false; - public get_name() {return this.detach ? "peripheral_detach" : "peripheral";} - public get_args() {return [this.side];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || ((args[0] as string) != "peripheral" && (args[0] as string) != "peripheral_detach")) return null; - let ev = new PeripheralEvent(); - ev.side = (args[1] as string); - ev.detach = (args[0] as string) == "peripheral_detach"; - return ev; - } + public side: string = ""; + public detach: boolean = false; + public get_name() { + return this.detach ? "peripheral_detach" : "peripheral"; + } + public get_args() { + return [this.side]; + } + public static init(args: any[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + ((args[0] as string) != "peripheral" && + (args[0] as string) != "peripheral_detach") + ) + return undefined; + let ev = new PeripheralEvent(); + ev.side = args[1] as string; + ev.detach = (args[0] as string) == "peripheral_detach"; + return ev; + } } export class RednetMessageEvent implements IEvent { - public sender: number = 0; - public message: any; - public protocol: string | null = null; - public get_name() {return "rednet_message";} - public get_args() {return [this.sender, this.message, this.protocol];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "rednet_message") return null; - let ev = new RednetMessageEvent(); - ev.sender = (args[1] as number); - ev.message = args[2]; - ev.protocol = (args[3] as string); - return ev; - } + public sender: number = 0; + public message: any; + public protocol: string | undefined = undefined; + public get_name() { + return "rednet_message"; + } + public get_args() { + return [this.sender, this.message, this.protocol]; + } + public static init(args: any[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + (args[0] as string) != "rednet_message" + ) + return undefined; + let ev = new RednetMessageEvent(); + ev.sender = args[1] as number; + ev.message = args[2]; + ev.protocol = args[3] as string; + return ev; + } } export class ModemMessageEvent implements IEvent { - public side: string = ""; - public channel: number = 0; - public replyChannel: number = 0; - public message: any; - public distance: number = 0; - public get_name() {return "modem_message";} - public get_args() {return [this.side, this.channel, this.replyChannel, this.message, this.distance];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "modem_message") return null; - let ev = new ModemMessageEvent(); - ev.side = (args[1] as string); - ev.channel = (args[2] as number); - ev.replyChannel = (args[3] as number); - ev.message = args[4]; - ev.distance = (args[5] as number); - return ev; - } + public side: string = ""; + public channel: number = 0; + public replyChannel: number = 0; + public message: any; + public distance: number = 0; + public get_name() { + return "modem_message"; + } + public get_args() { + return [ + this.side, + this.channel, + this.replyChannel, + this.message, + this.distance, + ]; + } + public static init(args: any[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + (args[0] as string) != "modem_message" + ) + return undefined; + let ev = new ModemMessageEvent(); + ev.side = args[1] as string; + ev.channel = args[2] as number; + ev.replyChannel = args[3] as number; + ev.message = args[4]; + ev.distance = args[5] as number; + return ev; + } } export class HTTPEvent implements IEvent { - public url: string = ""; - public handle: HTTPResponse | null = null; - public error: string | null = null; - public get_name() {return this.error == null ? "http_success" : "http_failure";} - public get_args() {return [this.url, (this.error == null ? this.handle : this.error), (this.error != null ? this.handle : null)];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || ((args[0] as string) != "http_success" && (args[0] as string) != "http_failure")) return null; - let ev = new HTTPEvent(); - ev.url = (args[1] as string); - if ((args[0] as string) == "http_success") { - ev.error = null; - ev.handle = (args[2] as HTTPResponse); - } else { - ev.error = (args[2] as string); - if (ev.error == null) ev.error = ""; - ev.handle = (args[3] as HTTPResponse); - } - return ev; + public url: string = ""; + public handle: HTTPResponse | undefined = undefined; + public error: string | undefined = undefined; + public get_name() { + return this.error == undefined ? "http_success" : "http_failure"; + } + public get_args() { + return [ + this.url, + this.error == undefined ? this.handle : this.error, + this.error != undefined ? this.handle : undefined, + ]; + } + public static init(args: any[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + ((args[0] as string) != "http_success" && + (args[0] as string) != "http_failure") + ) + return undefined; + let ev = new HTTPEvent(); + ev.url = args[1] as string; + if ((args[0] as string) == "http_success") { + ev.error = undefined; + ev.handle = args[2] as HTTPResponse; + } else { + ev.error = args[2] as string; + if (ev.error == undefined) ev.error = ""; + ev.handle = args[3] as HTTPResponse; } + return ev; + } } export class WebSocketEvent implements IEvent { - public handle: WebSocket | null = null; - public error: string | null = null; - public get_name() {return this.error == null ? "websocket_success" : "websocket_failure";} - public get_args() {return [this.handle == null ? this.error : this.handle];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || ((args[0] as string) != "websocket_success" && (args[0] as string) != "websocket_failure")) return null; - let ev = new WebSocketEvent(); - if ((args[0] as string) == "websocket_success") { - ev.handle = (args[1] as WebSocket); - ev.error = null; - } else { - ev.error = (args[1] as string); - ev.handle = null; - } - return ev; + public handle: WebSocket | undefined = undefined; + public error: string | undefined = undefined; + public get_name() { + return this.error == undefined ? "websocket_success" : "websocket_failure"; + } + public get_args() { + return [this.handle == undefined ? this.error : this.handle]; + } + public static init(args: any[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + ((args[0] as string) != "websocket_success" && + (args[0] as string) != "websocket_failure") + ) + return undefined; + let ev = new WebSocketEvent(); + if ((args[0] as string) == "websocket_success") { + ev.handle = args[1] as WebSocket; + ev.error = undefined; + } else { + ev.error = args[1] as string; + ev.handle = undefined; } + return ev; + } } export enum MouseEventType { - Click, - Up, - Scroll, - Drag, - Touch, - Move, + Click, + Up, + Scroll, + Drag, + Touch, + Move, } export class MouseEvent implements IEvent { - public button: number = 0; - public x: number = 0; - public y: number = 0; - public side: string | null = null; - public type: MouseEventType = MouseEventType.Click; - public get_name() { - return { - [MouseEventType.Click]: "mouse_click", - [MouseEventType.Up]: "mouse_up", - [MouseEventType.Scroll]: "mouse_scroll", - [MouseEventType.Drag]: "mouse_drag", - [MouseEventType.Touch]: "monitor_touch", - [MouseEventType.Move]: "mouse_move" - }[this.type]; - } - public get_args() {return [(this.type == MouseEventType.Touch ? this.side : this.button), this.x, this.y];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string")) return null; - let ev = new MouseEvent(); - const type = args[0] as string; - if (type == "mouse_click") {ev.type = MouseEventType.Click; ev.button = (args[1] as number); ev.side = null;} - else if (type == "mouse_up") {ev.type = MouseEventType.Up; ev.button = (args[1] as number); ev.side = null;} - else if (type == "mouse_scroll") {ev.type = MouseEventType.Scroll; ev.button = (args[1] as number); ev.side = null;} - else if (type == "mouse_drag") {ev.type = MouseEventType.Drag; ev.button = (args[1] as number); ev.side = null;} - else if (type == "monitor_touch") {ev.type = MouseEventType.Touch; ev.button = 0; ev.side = (args[1] as string);} - else if (type == "mouse_move") {ev.type = MouseEventType.Move; ev.button = (args[1] as number); ev.side = null;} - else return null; - ev.x = (args[2] as number); - ev.y = (args[3] as number); - return ev; - } + public button: number = 0; + public x: number = 0; + public y: number = 0; + public side: string | undefined = undefined; + public type: MouseEventType = MouseEventType.Click; + public get_name() { + return { + [MouseEventType.Click]: "mouse_click", + [MouseEventType.Up]: "mouse_up", + [MouseEventType.Scroll]: "mouse_scroll", + [MouseEventType.Drag]: "mouse_drag", + [MouseEventType.Touch]: "monitor_touch", + [MouseEventType.Move]: "mouse_move", + }[this.type]; + } + public get_args() { + return [ + this.type == MouseEventType.Touch ? this.side : this.button, + this.x, + this.y, + ]; + } + public static init(args: any[]): IEvent | undefined { + if (!(typeof args[0] === "string")) return undefined; + let ev = new MouseEvent(); + const type = args[0] as string; + if (type == "mouse_click") { + ev.type = MouseEventType.Click; + ev.button = args[1] as number; + ev.side = undefined; + } else if (type == "mouse_up") { + ev.type = MouseEventType.Up; + ev.button = args[1] as number; + ev.side = undefined; + } else if (type == "mouse_scroll") { + ev.type = MouseEventType.Scroll; + ev.button = args[1] as number; + ev.side = undefined; + } else if (type == "mouse_drag") { + ev.type = MouseEventType.Drag; + ev.button = args[1] as number; + ev.side = undefined; + } else if (type == "monitor_touch") { + ev.type = MouseEventType.Touch; + ev.button = 0; + ev.side = args[1] as string; + } else if (type == "mouse_move") { + ev.type = MouseEventType.Move; + ev.button = args[1] as number; + ev.side = undefined; + } else return undefined; + ev.x = args[2] as number; + ev.y = args[3] as number; + return ev; + } } export class ResizeEvent implements IEvent { - public side: string | null = null; - public get_name() {return this.side == null ? "term_resize" : "monitor_resize";} - public get_args() {return [this.side];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || ((args[0] as string) != "term_resize" && (args[0] as string) != "monitor_resize")) return null; - let ev = new ResizeEvent(); - if ((args[0] as string) == "monitor_resize") ev.side = (args[1] as string); - else ev.side = null; - return ev; - } + public side: string | undefined = undefined; + public get_name() { + return this.side == undefined ? "term_resize" : "monitor_resize"; + } + public get_args() { + return [this.side]; + } + public static init(args: any[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + ((args[0] as string) != "term_resize" && + (args[0] as string) != "monitor_resize") + ) + return undefined; + let ev = new ResizeEvent(); + if ((args[0] as string) == "monitor_resize") ev.side = args[1] as string; + else ev.side = undefined; + return ev; + } } export class TurtleInventoryEvent implements IEvent { - public get_name() {return "turtle_inventory";} - public get_args() {return [];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "turtle_inventory") return null; - let ev = new TurtleInventoryEvent(); - return ev; - } + public get_name() { + return "turtle_inventory"; + } + public get_args() { + return []; + } + public static init(args: any[]): IEvent | undefined { + if ( + !(typeof args[0] === "string") || + (args[0] as string) != "turtle_inventory" + ) + return undefined; + let ev = new TurtleInventoryEvent(); + return ev; + } } class SpeakerAudioEmptyEvent implements IEvent { - public side: string = ""; - public get_name() {return "speaker_audio_empty";} - public get_args() {return [this.side];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "speaker_audio_empty") return null; - let ev: SpeakerAudioEmptyEvent; - ev.side = args[1] as string; - return ev; - } + public side = ""; + public get_name() { + return "speaker_audio_empty"; + } + public get_args() { + return [this.side]; + } + public static init(args: unknown[]): IEvent | undefined { + if (!(typeof args[0] === "string") || args[0] != "speaker_audio_empty") + return undefined; + const ev = new SpeakerAudioEmptyEvent(); + ev.side = args[1] as string; + return ev; + } } class ComputerCommandEvent implements IEvent { - public args: string[] = []; - public get_name() {return "computer_command";} - public get_args() {return this.args;} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "computer_command") return null; - let ev: ComputerCommandEvent; - ev.args = args.slice(1); - return ev; - } + public args: string[] = []; + public get_name() { + return "computer_command"; + } + public get_args() { + return this.args; + } + public static init(args: unknown[]): IEvent | undefined { + if (!(typeof args[0] === "string") || args[0] != "computer_command") + return undefined; + const ev = new ComputerCommandEvent(); + ev.args = args.slice(1) as string[]; + return ev; + } } /* class Event implements IEvent { - + public get_name() {return "";} public get_args() {return [(: any)];} - public static init(args: any[]): IEvent | null { - if (!(typeof args[0] === "string") || (args[0] as string) != "") return null; + public static init(args: any[]): IEvent | undefined { + if (!(typeof args[0] === "string") || (args[0] as string) != "") return undefined; let ev: Event; return ev; @@ -315,60 +464,107 @@ class Event implements IEvent { } */ -export class GenericEvent implements IEvent { - public args: any[] = []; - public get_name() {return (this.args[0] as string);} - public get_args() {return this.args.slice(1);} - public static init(args: any[]): IEvent | null { - let ev = new GenericEvent(); - ev.args = args; - return ev; - } +export class ChatBoxEvent implements IEvent { + public username: string = ""; + public message: string = ""; + public uuid: string = ""; + public isHidden: boolean = false; + public messageUtf8: string = ""; + + public get_name() { + return "chat"; + } + public get_args() { + return [ + this.username, + this.message, + this.uuid, + this.isHidden, + this.messageUtf8, + ]; + } + public static init(args: any[]): IEvent | undefined { + if (!(typeof args[0] === "string") || (args[0] as string) != "chat") + return undefined; + let ev = new ChatBoxEvent(); + ev.username = args[1] as string; + ev.message = args[2] as string; + ev.uuid = args[3] as string; + ev.isHidden = args[4] as boolean; + ev.messageUtf8 = args[5] as string; + return ev; + } } -let eventInitializers: ((args: any[]) => IEvent | null)[] = [ - CharEvent.init, - KeyEvent.init, - PasteEvent.init, - TimerEvent.init, - TaskCompleteEvent.init, - RedstoneEvent.init, - TerminateEvent.init, - DiskEvent.init, - PeripheralEvent.init, - RednetMessageEvent.init, - ModemMessageEvent.init, - HTTPEvent.init, - WebSocketEvent.init, - MouseEvent.init, - ResizeEvent.init, - TurtleInventoryEvent.init, - SpeakerAudioEmptyEvent.init, - ComputerCommandEvent.init, - GenericEvent.init +export class GenericEvent implements IEvent { + public args: any[] = []; + public get_name() { + return this.args[0] as string; + } + public get_args() { + return this.args.slice(1); + } + public static init(args: any[]): IEvent | undefined { + let ev = new GenericEvent(); + ev.args = args; + return ev; + } +} + +let eventInitializers: ((args: unknown[]) => IEvent | undefined)[] = [ + (args) => CharEvent.init(args), + (args) => KeyEvent.init(args), + (args) => PasteEvent.init(args), + (args) => TimerEvent.init(args), + (args) => TaskCompleteEvent.init(args), + (args) => RedstoneEvent.init(args), + (args) => TerminateEvent.init(args), + (args) => DiskEvent.init(args), + (args) => PeripheralEvent.init(args), + (args) => RednetMessageEvent.init(args), + (args) => ModemMessageEvent.init(args), + (args) => HTTPEvent.init(args), + (args) => WebSocketEvent.init(args), + (args) => MouseEvent.init(args), + (args) => ResizeEvent.init(args), + (args) => TurtleInventoryEvent.init(args), + (args) => SpeakerAudioEmptyEvent.init(args), + (args) => ComputerCommandEvent.init(args), + (args) => ChatBoxEvent.init(args), + (args) => GenericEvent.init(args), ]; type Constructor = new (...args: any[]) => T; -export function pullEventRaw(filter: string | null = null): IEvent | null { - let args = table.pack(...coroutine.yield(filter)); - for (let init of eventInitializers) { - let ev = init(args); - if (ev != null) return ev; - } - return GenericEvent.init(args); +export function pullEventRaw( + filter: string | undefined = undefined, +): IEvent | undefined { + let args = table.pack(...coroutine.yield(filter)); + for (let init of eventInitializers) { + let ev = init(args); + if (ev != undefined) return ev; + } + return GenericEvent.init(args); } -export function pullEvent(filter: string | null = null): IEvent | null { - let ev = pullEventRaw(filter); - if (ev instanceof TerminateEvent) throw "Terminated"; - return ev; +export function pullEvent( + filter: string | undefined = undefined, +): IEvent | undefined { + let ev = pullEventRaw(filter); + if (ev instanceof TerminateEvent) throw "Terminated"; + return ev; } -export function pullEventRawAs(type: Constructor, filter: string | null = null): T | null { - let ev = pullEventRaw(filter); - if ((ev instanceof type)) return ev as T; - else return null; +export function pullEventRawAs( + type: Constructor, + filter: string | undefined = undefined, +): T | undefined { + let ev = pullEventRaw(filter); + if (ev instanceof type) return ev as T; + else return undefined; } -export function pullEventAs(type: Constructor, filter: string | null = null): T | null { - let ev = pullEvent(filter); - if ((ev instanceof type)) return ev as T; - else return null; +export function pullEventAs( + type: Constructor, + filter: string | undefined = undefined, +): T | undefined { + let ev = pullEvent(filter); + if (ev instanceof type) return ev as T; + else return undefined; } diff --git a/src/test/main.ts b/src/test/main.ts new file mode 100644 index 0000000..76aa5a1 --- /dev/null +++ b/src/test/main.ts @@ -0,0 +1,3 @@ +import { testTimeBasedRotation } from "./testCcLog"; + +testTimeBasedRotation(); diff --git a/src/test/testCcLog.ts b/src/test/testCcLog.ts new file mode 100644 index 0000000..fa91418 --- /dev/null +++ b/src/test/testCcLog.ts @@ -0,0 +1,35 @@ +import { CCLog, MINUTE, HOUR, SECOND } from "@/lib/ccLog"; + +// Test the new time-based rotation functionality +function testTimeBasedRotation() { + print("Testing time-based log rotation functionality..."); + + // Test with default interval (1 day) + const logger1 = new CCLog("test_log_default.txt"); + logger1.info("This is a test message with default interval (1 day)"); + + // Test with custom interval (1 hour) + const logger2 = new CCLog("test_log_hourly.txt", HOUR); + logger2.info("This is a test message with 1-hour interval"); + + // Test with custom interval (30 minutes) + const logger3 = new CCLog("test_log_30min.txt", 30 * MINUTE); + logger3.info("This is a test message with 30-minute interval"); + + // Test with custom interval (5 seconds) + const logger4 = new CCLog("test_log_5sec.txt", 5 * SECOND); + logger4.info("This is a test message with 5-second interval"); + for (let i = 0; i < 10; i++) { + logger4.info(`This is a test message with 5-second interval ${i}`); + sleep(1); + } + + logger1.close(); + logger2.close(); + logger3.close(); + logger4.close(); + + print("Test completed successfully!"); +} + +export { testTimeBasedRotation }; diff --git a/tsconfig.accesscontrol.json b/tsconfig.accesscontrol.json new file mode 100644 index 0000000..8b69f63 --- /dev/null +++ b/tsconfig.accesscontrol.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/MCJack123/TypeScriptToLua/master/tsconfig-schema.json", + "extends": "./tsconfig.json", + "tstl": { + "luaBundle": "build/accesscontrol.lua", + "luaBundleEntry": "src/accesscontrol/main.ts" + }, + "include": ["src/accesscontrol/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 1a8cdc7..48a9f49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,8 @@ "strict": true, "types": [ "@sikongjueluo/advanced-peripherals-types", + "@sikongjueluo/toml2lua-types", + "@sikongjueluo/dkjson-types", "@jackmacwindows/lua-types/cc", "@jackmacwindows/craftos-types", "@jackmacwindows/cc-types", @@ -19,7 +21,9 @@ "@jackmacwindows/lua-types/cc": ["./types/cc"], "@sikongjueluo/advanced-peripherals-types": [ "./types/advanced-peripherals" - ] + ], + "@sikongjueluo/toml2lua-types": ["./types/toml2lua"], + "@sikongjueluo/dkjson-types": ["./types/dkjson"] } }, "tstl": { diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..d0212ea --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/MCJack123/TypeScriptToLua/master/tsconfig-schema.json", + "extends": "./tsconfig.json", + "tstl": { + "luaBundle": "build/test.lua", + "luaBundleEntry": "src/test/main.ts" + }, + "include": ["src/test/*.ts"] +} diff --git a/types/advanced-peripherals/block-reader.d.ts b/types/advanced-peripherals/block-reader.d.ts new file mode 100644 index 0000000..836c7a1 --- /dev/null +++ b/types/advanced-peripherals/block-reader.d.ts @@ -0,0 +1,38 @@ +/// + +/** + * Represents the Block Reader peripheral from Advanced Peripherals. + * Used to read data about blocks in front of it. + * + * @see https://docs.advanced-peripherals.de/0.7/peripherals/block_reader/ + */ +/** @noSelf **/ +declare interface BlockReaderPeripheral extends IPeripheral { + /** + * Returns the registry name of the block (ex. minecraft:dirt). + * + * @returns The registry name of the block + */ + getBlockName(): string; + + /** + * Returns the block data of the block if block is a tile entity. + * + * @returns The block data table if the block is a tile entity, otherwise nil + */ + getBlockData(): BlockDetailData | undefined; + + /** + * Returns the properties of a block and its state. + * + * @returns The block states table if available, otherwise nil + */ + getBlockStates(): Record | undefined; + + /** + * Returns true whether the block is a tile entity or not. + * + * @returns Boolean indicating if the block is a tile entity, or nil if unable to determine + */ + isTileEntity(): boolean | undefined; +} diff --git a/types/advanced-peripherals/chat-box.d.ts b/types/advanced-peripherals/chat-box.d.ts new file mode 100644 index 0000000..e98c7c3 --- /dev/null +++ b/types/advanced-peripherals/chat-box.d.ts @@ -0,0 +1,150 @@ +/// + +/** + * Represents the Chat Box peripheral from Advanced Peripherals. + * Used to interact with Minecraft's chat system. + * + * @see https://minecraft.wiki/w/Text_component_format + * @see https://docs.advanced-peripherals.de/latest/peripherals/chat_box/ + */ +/** @noSelf **/ +declare interface ChatBoxPeripheral extends IPeripheral { + /** + * Broadcasts a message to the global chat or if range is specified it is sent to all players in the range. + * The prefix will change the text that appears inside the brackets at the start of a message. Defaults to "AP". + * To change the brackets used around the prefix you must specify a string like so: "[]", "()", "<>", ... + * bracketColor specifies the color to use for the brackets, this must be in the MOTD code format. + * If utf8Support is true: message, prefix, brackets, and bracketColor are all expected to be UTF8 encoded, using the utf8 standard library, unicode escape sequences, or similar. + * + * @param message The message to send + * @param prefix The prefix to display in brackets at the start of the message (defaults to "AP") + * @param brackets The bracket style around the prefix (e.g., "[]", "()", "<>") + * @param bracketColor The color for the brackets in MOTD code format + * @param range The range in blocks to send the message to players (if not global) + * @param utf8Support Whether to use UTF8 encoding for the message + * @returns true if the message is successfully sent, or nil and an error message if it fails + */ + sendMessage( + message: string, + prefix?: string, + brackets?: string, + bracketColor?: string, + range?: number, + utf8Support?: boolean, + ): LuaMultiReturn<[boolean, string | undefined]>; + + /** + * Similar to sendMessage() this sends a message to one specific player. Specify the player to send the message to with the username parameter. + * + * @param message The message to send + * @param username The username of the player to send the message to + * @param prefix The prefix to display in brackets at the start of the message (defaults to "AP") + * @param brackets The bracket style around the prefix (e.g., "[]", "()", "<>") + * @param bracketColor The color for the brackets in MOTD code format + * @param range The range in blocks to send the message to players (if not global) + * @param utf8Support Whether to use UTF8 encoding for the message + * @returns true if the message is successfully sent, or nil and an error message if it fails + */ + sendMessageToPlayer( + message: string, + username: string, + prefix?: string, + brackets?: string, + bracketColor?: string, + range?: number, + utf8Support?: boolean, + ): LuaMultiReturn<[boolean, string | undefined]>; + + /** + * Sends a toast to the specified player. The design of the toast is the classic notification design. + * It's planned to add a custom rendered design in the future. + * + * @param message The message for the toast + * @param title The title of the toast + * @param username The username of the player to send the toast to + * @param prefix The prefix to display in brackets at the start of the message (defaults to "AP") + * @param brackets The bracket style around the prefix (e.g., "[]", "()", "<>") + * @param bracketColor The color for the brackets in MOTD code format + * @param range The range in blocks to send the message to players (if not global) + * @param utf8Support Whether to use UTF8 encoding for the message + * @returns true if the toast is successfully sent, or nil and an error message if it fails + */ + sendToastToPlayer( + message: string, + title: string, + username: string, + prefix?: string, + brackets?: string, + bracketColor?: string, + range?: number, + utf8Support?: boolean, + ): LuaMultiReturn<[boolean, string | undefined]>; + + /** + * This function is fundamentally the same as sendMessage() except it takes a Minecraft text component as the first parameter. + * Find out more information on how the text component format works on the minecraft wiki. You can generate the json at minecraft.tools. + * + * @param json The Minecraft text component to send (as JSON string) + * @param prefix The prefix to display in brackets at the start of the message (defaults to "AP") + * @param brackets The bracket style around the prefix (e.g., "[]", "()", "<>") + * @param bracketColor The color for the brackets in MOTD code format + * @param range The range in blocks to send the message to players (if not global) + * @param utf8Support Whether to use UTF8 encoding for the message + * @returns true if the message is successfully sent, or nil and an error message if it fails + */ + sendFormattedMessage( + json: string, + prefix?: string, + brackets?: string, + bracketColor?: string, + range?: number, + utf8Support?: boolean, + ): LuaMultiReturn<[boolean, string | undefined]>; + + /** + * Similar to sendFormattedMessage() this sends a formatted message to one specific player. Specify the player to send the message to with the username parameter. + * + * @param json The Minecraft text component to send (as JSON string) + * @param username The username of the player to send the message to + * @param prefix The prefix to display in brackets at the start of the message (defaults to "AP") + * @param brackets The bracket style around the prefix (e.g., "[]", "()", "<>") + * @param bracketColor The color for the brackets in MOTD code format + * @param range The range in blocks to send the message to players (if not global) + * @param utf8Support Whether to use UTF8 encoding for the message + * @returns true if the message is successfully sent, or nil and an error message if it fails + */ + sendFormattedMessageToPlayer( + json: string, + username: string, + prefix?: string, + brackets?: string, + bracketColor?: string, + range?: number, + utf8Support?: boolean, + ): LuaMultiReturn<[boolean, string | undefined]>; + + /** + * This function is fundamentally the same as sendToast() except it takes a Minecraft text component as the first and second parameter. + * Find out more information on how the text component format works on the minecraft wiki. You can generate the json at minecraft.tools. + * + * @param messageJson The Minecraft text component for the message (as JSON string) + * @param titleJson The Minecraft text component for the title (as JSON string) + * @param username The username of the player to send the toast to + * @param prefix The prefix to display in brackets at the start of the message (defaults to "AP") + * @param brackets The bracket style around the prefix (e.g., "[]", "()", "<>") + * @param bracketColor The color for the brackets in MOTD code format + * @param range The range in blocks to send the message to players (if not global) + * @param utf8Support Whether to use UTF8 encoding for the message + * @returns true if the toast is successfully sent, or nil and an error message if it fails + */ + sendFormattedToastToPlayer( + messageJson: string, + titleJson: string, + username: string, + prefix?: string, + brackets?: string, + bracketColor?: string, + range?: number, + utf8Support?: boolean, + ): LuaMultiReturn<[boolean, string | undefined]>; +} diff --git a/types/advanced-peripherals/index.d.ts b/types/advanced-peripherals/index.d.ts index 8e35763..eaf87c6 100644 --- a/types/advanced-peripherals/index.d.ts +++ b/types/advanced-peripherals/index.d.ts @@ -1,14 +1,3 @@ -declare interface BlockItemDetailData { - id: string; - tag: object; - Count: number; - Slot: number; -} - -declare interface BlockDetailData { - Items: Record; -} - -declare class BlockReaderPeripheral { - getBlockData(): BlockDetailData; -} +/// +/// +/// \ No newline at end of file diff --git a/types/advanced-peripherals/package.json b/types/advanced-peripherals/package.json index d885fcb..55dd0af 100644 --- a/types/advanced-peripherals/package.json +++ b/types/advanced-peripherals/package.json @@ -4,7 +4,7 @@ "description": "TypeScript type definitions for base Advanced Peripherals APIs.", "types": "./index.d.ts", "files": [ - "./index.d.ts" + "./*.d.ts" ], "author": "SikongJueluo", "license": "MIT" diff --git a/types/advanced-peripherals/player-detector.d.ts b/types/advanced-peripherals/player-detector.d.ts new file mode 100644 index 0000000..f8be771 --- /dev/null +++ b/types/advanced-peripherals/player-detector.d.ts @@ -0,0 +1,276 @@ +/** + * Represents the Player Detector peripheral from Advanced Peripherals. + * Used to detect and track players in the world. + * + * @see https://docs.advanced-peripherals.de/0.7/peripherals/player_detector/ + */ +/** @noSelf **/ +declare interface PlayerDetectorPeripheral extends IPeripheral { + /** + * Returns information about the player with the specified username. + * + * @param username The player's username to look up + * @returns A table containing player information, or nil if the player is not found + */ + getPlayerPos(username: string): PlayerInfo | undefined; + + /** + * Returns information about the player with the specified username. + * Alternative name for getPlayerPos. + * + * @param username The player's username to look up + * @returns A table containing player information, or nil if the player is not found + */ + getPlayer(username: string): PlayerInfo | undefined; + + /** + * Returns a list of all online players. + * + * @returns Table containing all online players as an array of usernames + */ + getOnlinePlayers(): string[]; + + /** + * Returns a list of players within the given range of the peripheral. + * + * @param range The range to search for players + * @returns Array containing usernames of players within range + */ + getPlayersInRange(range: number): string[]; + + /** + * Returns a list of players within the 2 positions posOne and posTwo. + * + * @param posOne Position with x, y, z coordinates + * @param posTwo Position with x, y, z coordinates + * @returns Array containing usernames of players within the specified coordinates + */ + getPlayersInCoords(posOne: Coordinate, posTwo: Coordinate): string[]; + + /** + * Returns a list of players within a cuboid centered at the peripheral. + * + * @param w Width of the cuboid (x-axis) + * @param h Height of the cuboid (y-axis) + * @param d Depth of the cuboid (z-axis) + * @returns Array containing usernames of players within the specified cuboid + */ + getPlayersInCubic(w: number, h: number, d: number): string[]; + + /** + * Returns true if the player whose username matches the provided username is within the given range of the peripheral. + * + * @param range The range to check + * @param username The player's username to check + * @returns Boolean indicating if the player is in range + */ + isPlayerInRange(range: number, username: string): boolean; + + /** + * Returns true if the player is within the 2 positions. + * + * @param posOne Position with x, y, z coordinates + * @param posTwo Position with x, y, z coordinates + * @param username The player's username to check + * @returns Boolean indicating if the player is in the specified coordinates + */ + isPlayerInCoords( + posOne: Coordinate, + posTwo: Coordinate, + username: string, + ): boolean; + + /** + * Returns true if the player is within the cuboid centered at the peripheral. + * + * @param w Width of the cuboid (x-axis) + * @param h Height of the cuboid (y-axis) + * @param d Depth of the cuboid (z-axis) + * @param username The player's username to check + * @returns Boolean indicating if the player is in the specified cuboid + */ + isPlayerInCubic(w: number, h: number, d: number, username: string): boolean; + + /** + * Returns true if there is any player in the given range. + * + * @param range The range to check + * @returns Boolean indicating if any player is in range + */ + isPlayersInRange(range: number): boolean; + + /** + * Returns true if any player is within the 2 positions. + * + * @param posOne Position with x, y, z coordinates + * @param posTwo Position with x, y, z coordinates + * @returns Boolean indicating if any player is in the specified coordinates + */ + isPlayersInCoords(posOne: Coordinate, posTwo: Coordinate): boolean; + + /** + * Returns true if any player is within the cuboid centered at the peripheral. + * + * @param w Width of the cuboid (x-axis) + * @param h Height of the cuboid (y-axis) + * @param d Depth of the cuboid (z-axis) + * @returns Boolean indicating if any player is in the specified cuboid + */ + isPlayersInCubic(w: number, h: number, d: number): boolean; +} + +/** + * Represents a coordinate in 3D space. + */ +declare interface Coordinate { + /** + * The x coordinate. + */ + x: number; + /** + * The y coordinate. + */ + y: number; + /** + * The z coordinate. + */ + z: number; +} + +/** + * Contains detailed information about a player. + */ +declare interface PlayerInfo { + /** + * The dimension the player is in. + */ + dimension: string; + /** + * The height of the player's eyes. + */ + eyeHeight: number; + /** + * The pitch of the player's head. + */ + pitch: number; + /** + * The health of the player. + */ + health: number; + /** + * The max health of the player. + */ + maxHealth: number; + /** + * The air supply of the player. + */ + airSupply: number; + /** + * The respawn position of the player. + */ + respawnPosition: number; + /** + * The respawn dimension of the player. + */ + respawnDimension: number; + /** + * The respawn angle of the player in degrees. + */ + respawnAngle: number; + /** + * The yaw of the player's head. + */ + yaw: number; + /** + * The x coordinate. + */ + x: number; + /** + * The y coordinate. + */ + y: number; + /** + * The z coordinate. + */ + z: number; +} + +/** + * Player click event type for the Player Detector peripheral. + * Fires when a player clicks on the block. + */ +declare interface PlayerClickEvent { + /** + * The name of the event. + */ + event: "playerClick"; + /** + * The username of the player who clicked the block. + */ + username: string; + /** + * The name of the peripheral like playerDetector_4. + */ + devicename: string; +} + +/** + * Player join event type for the Player Detector peripheral. + * Fires when a player joins the world/a server. + */ +declare interface PlayerJoinEvent { + /** + * The name of the event. + */ + event: "playerJoin"; + /** + * The username of the player who joined. + */ + username: string; + /** + * The resource id of the dimension the player is in. + */ + dimension: string; +} + +/** + * Player leave event type for the Player Detector peripheral. + * Fires when a player leaves the world/a server. + */ +declare interface PlayerLeaveEvent { + /** + * The name of the event. + */ + event: "playerLeave"; + /** + * The username of the player who left. + */ + username: string; + /** + * The resource id of the dimension the player was in. + */ + dimension: string; +} + +/** + * Player changed dimension event type for the Player Detector peripheral. + * Fires when a player changes dimensions. + */ +declare interface PlayerChangedDimensionEvent { + /** + * The name of the event. + */ + event: "playerChangedDimension"; + /** + * The username of the player who changed dimensions. + */ + username: string; + /** + * The resource id of the dimension the player was in. + */ + fromDim: string; + /** + * The resource id of the dimension the player is in. + */ + toDim: string; +} diff --git a/types/advanced-peripherals/shared.d.ts b/types/advanced-peripherals/shared.d.ts new file mode 100644 index 0000000..7f25df9 --- /dev/null +++ b/types/advanced-peripherals/shared.d.ts @@ -0,0 +1,111 @@ +declare interface BlockItemDetailData { + id: string; + tag: object; + Count: number; + Slot: number; +} + +declare interface BlockDetailData { + Items: Record; +} + +/** + * Minecraft Text Component format + * @see https://minecraft.wiki/w/Text_component_format + */ +declare type MinecraftColor = + | "black" + | "dark_blue" + | "dark_green" + | "dark_aqua" + | "dark_red" + | "dark_purple" + | "gold" + | "gray" + | "dark_gray" + | "blue" + | "green" + | "aqua" + | "red" + | "light_purple" + | "yellow" + | "white" + | "reset"; // RGB color in #RRGGBB format + +declare type MinecraftFont = + | "minecraft:default" + | "minecraft:uniform" + | "minecraft:alt"; + +declare type ClickEventAction = + | "open_url" + | "open_file" + | "run_command" + | "suggest_command" + | "change_page" + | "copy_to_clipboard"; + +declare type HoverEventAction = "show_text" | "show_item" | "show_entity"; + +declare interface ClickEvent { + action: ClickEventAction; + value: string | number; +} + +declare interface HoverEvent { + action: HoverEventAction; + contents?: unknown; + value?: unknown; +} + +declare interface BaseTextComponent { + type?: "text" | "translatable" | "score" | "selector" | "keybind" | "nbt"; + text?: string; + translate?: string; + with?: (MinecraftTextComponent | string)[]; + score?: { + name: string; + objective: string; + value?: string; + }; + selector?: string; + keybind?: string; + nbt?: string; + interpret?: boolean; + separator?: MinecraftTextComponent; + block?: string; + entity?: string; + storage?: string; + + // Formatting + color?: MinecraftColor; + font?: MinecraftFont; + bold?: boolean; + italic?: boolean; + underlined?: boolean; + strikethrough?: boolean; + obfuscated?: boolean; + insertion?: string; + clickEvent?: ClickEvent; + hoverEvent?: HoverEvent; + shadow_color?: number; + + // Nested components + extra?: MinecraftTextComponent[]; +} + +declare interface TextTextComponent extends BaseTextComponent { + type?: "text"; + text: string; +} + +declare interface TranslatableTextComponent extends BaseTextComponent { + type: "translatable"; + translate: string; + with?: (MinecraftTextComponent | string)[]; +} + +declare type MinecraftTextComponent = + | TextTextComponent + | TranslatableTextComponent + | BaseTextComponent; diff --git a/types/craftos/index.d.ts b/types/craftos/index.d.ts index 80bd1c8..95fb026 100644 --- a/types/craftos/index.d.ts +++ b/types/craftos/index.d.ts @@ -477,7 +477,7 @@ declare namespace parallel { } /** @noSelf */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface IPeripheral {} +declare interface IPeripheral {} /** @noSelf */ declare class CommandPeripheral implements IPeripheral { diff --git a/types/dkjson/index.d.ts b/types/dkjson/index.d.ts new file mode 100644 index 0000000..dbd5111 --- /dev/null +++ b/types/dkjson/index.d.ts @@ -0,0 +1,130 @@ +/** @noResolution */ + +/** + * Represents a JSON null value that is distinct from Lua's nil. + */ +interface JsonNull { + readonly __json_null: unique symbol; +} + +/** + * State object for JSON encoding options + */ +interface EncodeState { + /** When set, the output will contain newlines and indentations */ + indent?: boolean; + /** Specifies ordering of keys in encoded output */ + keyorder?: string[]; + /** Initial indentation level when indent is set (2 spaces per level, default is 0) */ + level?: number; + /** Array to store result strings for concatenation at once */ + buffer?: string[]; + /** Index of last element in buffer (when set) */ + bufferlen?: number; + /** Used to detect reference cycles (temporary, created when absent) */ + tables?: unknown[]; + /** Called when encoder cannot encode a value */ + exception?: ( + reason: string, + value: unknown, + state: EncodeState, + defaultmessage: string, + ) => unknown; +} + +/** + * Options for JSON decoding with custom metatables + */ +interface DecodeOptions { + objectmeta?: unknown; + arraymeta?: unknown; +} + +/** + * The dkjson module version string + */ +export const version: string; + +/** + * Special value representing JSON null (distinct from Lua's nil) + */ +export const jsonNull: JsonNull; + +/** + * Encode a Lua value to a JSON string. + * @param object The value to encode (can be a table, string, number, boolean, nil, json.null or any object with a __tojson function in its metatable) + * @param state Optional table with configuration options for encoding + * @returns A string containing the JSON representation, or true if state.buffer is set and encoding was successful + */ +export function encode( + object: object | string | number | boolean | undefined, + state?: EncodeState, +): string | boolean; + +/** + * Decode a JSON string starting at a given position. + * @param str The JSON string to decode + * @param pos Starting position (default is 1) + * @param nullval Value to return for null values (default is nil, can be set to json.null) + * @param objectmeta Custom metatable for decoded objects (optional) + * @param arraymeta Custom metatable for decoded arrays (optional) + * @returns The decoded object (or the custom null value) and position of next character not part of the object, or undefined, position, and error message in case of errors + */ +export function decode( + str: string, + pos?: number, + nullval?: unknown, + objectmeta?: unknown, + arraymeta?: unknown, +): LuaMultiReturn<[object | undefined, number | undefined, string | undefined]>; +/** + * Quote a UTF-8 string and escape critical characters using JSON escape sequences. + * Only necessary when building custom __tojson functions. + * @param str The string to quote and escape + * @returns The quoted and escaped string + */ +export function quotestring(str: string): string; + +/** + * When state.indent is set, adds a newline to state.buffer and spaces according to state.level. + * @param state The encoding state containing indent and level information + */ +export function addnewline(state: EncodeState): void; + +/** + * Function that can be used as the exception option in encode. Instead of raising an error, + * this function encodes the error message as a string for debugging malformed input. + * @param reason The reason for the exception + * @param value The value that caused the exception + * @param state The encoding state + * @param defaultmessage The default error message + * @returns The encoded error message + */ +export function encodeexception( + reason: string, + value: unknown, + state: EncodeState, + defaultmessage: string, +): string; + +/** + * Require the LPeg module and return a copy of the module table where the decode function + * is replaced by a version that uses LPeg for better performance. + * @returns A copy of the module with LPeg-optimized decode function + */ +export function use_lpeg(): { + version: string; + null: JsonNull; + encode: typeof encode; + decode: typeof decode; + quotestring: typeof quotestring; + addnewline: typeof addnewline; + encodeexception: typeof encodeexception; + use_lpeg: typeof use_lpeg; + using_lpeg: boolean; +}; + +/** + * Variable set to true in the module table copy that uses LPeg support. + */ +export const using_lpeg: boolean; diff --git a/types/dkjson/index.lua b/types/dkjson/index.lua new file mode 100644 index 0000000..1ed896b --- /dev/null +++ b/types/dkjson/index.lua @@ -0,0 +1,762 @@ +-- Module options: +local always_use_lpeg = false +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ + +David Kolf's JSON module for Lua 5.1 - 5.4 + +Version 2.8 + + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + + +Copyright (C) 2010-2024 David Heiko Kolf + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable = + pairs, type, tostring, tonumber, getmetatable, setmetatable +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.8" } + +local jsonlpeg = {} + +if register_global_module_table then + if always_use_lpeg then + _G[global_module_name] = jsonlpeg + else + _G[global_module_name] = json + end +end + +local _ENV = nil -- blocking globals in Lua 5.2 and later + +pcall(function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable({}, { + __tojson = function() return "null" end +}) + +local function isarray(tbl) + local max, n, arraylen = 0, 0, 0 + for k, v in pairs(tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", + ["\\"] = "\\\\", + ["\b"] = "\\b", + ["\f"] = "\\f", + ["\n"] = "\\n", + ["\r"] = "\\r", + ["\t"] = "\\t" +} + +local function escapeutf8(uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte(uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor(value / 0x400), 0xDC00 + (value % 0x400) + return strformat("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub(str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind(str, pattern) then + return gsub(str, pattern, repl) + else + return str + end +end + +local function quotestring(value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub(value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind(value, "[\194\216\220\225\226\239]") then + value = fsub(value, "\194[\128-\159\173]", escapeutf8) + value = fsub(value, "\216[\128-\132]", escapeutf8) + value = fsub(value, "\220\143", escapeutf8) + value = fsub(value, "\225\158[\180\181]", escapeutf8) + value = fsub(value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub(value, "\226\129[\160-\175]", escapeutf8) + value = fsub(value, "\239\187\191", escapeutf8) + value = fsub(value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind(str, o, 1, true) + if i then + return strsub(str, 1, i - 1) .. n .. strsub(str, j + 1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint() + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str(num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num(str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2(level, buffer, buflen) + buffer[buflen + 1] = "\n" + buffer[buflen + 2] = strrep(" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline(state) + if state.indent then + state.bufferlen = addnewline2(state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair(key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type(key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2(level, buffer, buflen) + end + -- When Lua is compiled with LUA_NOCVTN2S this will fail when + -- numbers are mixed into the keys of the table. JSON keys are always + -- strings, so this would be an implicit conversion too and the failure + -- is intentional. + buffer[buflen + 1] = quotestring(key) + buffer[buflen + 2] = ":" + return encode2(value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type(res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler(reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function(value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type(value) + local valmeta = getmetatable(value) + valmeta = type(valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson(value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str(value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring(value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray(value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2(value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v ~= nil then + used[k] = true + buflen, msg = addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + for k, v in pairs(value) do + if not used[k] then + buflen, msg = addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k, v in pairs(value) do + buflen, msg = addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2(level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode(value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2(value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error(msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat(buffer) + end +end + +local function loc(str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind(str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return strformat("line %d, column %d", line, where - linepos) +end + +local function unterminated(str, what, where) + return nil, strlen(str) + 1, "unterminated " .. what .. " at " .. loc(str, where) +end + +local function scanwhite(str, pos) + while true do + pos = strfind(str, "%S", pos) + if not pos then return nil end + local sub2 = strsub(str, pos, pos + 1) + if sub2 == "\239\187" and strsub(str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind(str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind(str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", + ["\\"] = "\\", + ["/"] = "/", + ["b"] = "\b", + ["f"] = "\f", + ["n"] = "\n", + ["r"] = "\r", + ["t"] = "\t" +} + +local function unichar(value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar(value) + elseif value <= 0x07ff then + return strchar(0xc0 + floor(value / 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar(0xe0 + floor(value / 0x1000), + 0x80 + (floor(value / 0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar(0xf0 + floor(value / 0x40000), + 0x80 + (floor(value / 0x1000) % 0x40), + 0x80 + (floor(value / 0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring(str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind(str, "[\"\\]", lastpos) + if not nextpos then + return unterminated(str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub(str, lastpos, nextpos - 1) + end + if strsub(str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub(str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber(strsub(str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub(str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber(strsub(str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar(value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat(buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable(what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable(tbl, objectmeta) + else + setmetatable(tbl, arraymeta) + end + while true do + pos = scanwhite(str, pos) + if not pos then return unterminated(str, what, startpos) end + local char = strsub(str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue(str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite(str, pos) + if not pos then return unterminated(str, what, startpos) end + char = strsub(str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc(str, pos) .. ")" + end + pos = scanwhite(str, pos + 1) + if not pos then return unterminated(str, what, startpos) end + local val2 + val2, pos, err = scanvalue(str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite(str, pos) + if not pos then return unterminated(str, what, startpos) end + char = strsub(str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function(str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite(str, pos) + if not pos then + return nil, strlen(str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub(str, pos, pos) + if char == "{" then + return scantable('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring(str, pos) + else + local pstart, pend = strfind(str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num(strsub(str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind(str, "^%a%w*", pos) + if pstart then + local name = strsub(str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc(str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return { __jsontype = 'object' }, { __jsontype = 'array' } + end +end + +function json.decode(str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue(str, pos, nullval, objectmeta, arraymeta) +end + +-- function json.use_lpeg () +-- local g = require ("lpeg") + +-- if type(g.version) == 'function' and g.version() == "0.11" then +-- error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" +-- end + +-- local pegmatch = g.match +-- local P, S, R = g.P, g.S, g.R + +-- local function ErrorCall (str, pos, msg, state) +-- if not state.msg then +-- state.msg = msg .. " at " .. loc (str, pos) +-- state.pos = pos +-- end +-- return false +-- end + +-- local function Err (msg) +-- return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) +-- end + +-- local function ErrorUnterminatedCall (str, pos, what, state) +-- return ErrorCall (str, pos - 1, "unterminated " .. what, state) +-- end + +-- local SingleLineComment = P"//" * (1 - S"\n\r")^0 +-- local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" +-- local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 + +-- local function ErrUnterminated (what) +-- return g.Cmt (g.Cc (what) * g.Carg (2), ErrorUnterminatedCall) +-- end + +-- local PlainChar = 1 - S"\"\\\n\r" +-- local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars +-- local HexDigit = R("09", "af", "AF") +-- local function UTF16Surrogate (match, pos, high, low) +-- high, low = tonumber (high, 16), tonumber (low, 16) +-- if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then +-- return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) +-- else +-- return false +-- end +-- end +-- local function UTF16BMP (hex) +-- return unichar (tonumber (hex, 16)) +-- end +-- local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) +-- local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP +-- local Char = UnicodeEscape + EscapeSequence + PlainChar +-- local String = P"\"" * (g.Cs (Char ^ 0) * P"\"" + ErrUnterminated "string") +-- local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) +-- local Fractal = P"." * R"09"^0 +-- local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 +-- local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num +-- local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) +-- local SimpleValue = Number + String + Constant +-- local ArrayContent, ObjectContent + +-- -- The functions parsearray and parseobject parse only a single value/pair +-- -- at a time and store them directly to avoid hitting the LPeg limits. +-- local function parsearray (str, pos, nullval, state) +-- local obj, cont +-- local start = pos +-- local npos +-- local t, nt = {}, 0 +-- repeat +-- obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) +-- if cont == 'end' then +-- return ErrorUnterminatedCall (str, start, "array", state) +-- end +-- pos = npos +-- if cont == 'cont' or cont == 'last' then +-- nt = nt + 1 +-- t[nt] = obj +-- end +-- until cont ~= 'cont' +-- return pos, setmetatable (t, state.arraymeta) +-- end + +-- local function parseobject (str, pos, nullval, state) +-- local obj, key, cont +-- local start = pos +-- local npos +-- local t = {} +-- repeat +-- key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) +-- if cont == 'end' then +-- return ErrorUnterminatedCall (str, start, "object", state) +-- end +-- pos = npos +-- if cont == 'cont' or cont == 'last' then +-- t[key] = obj +-- end +-- until cont ~= 'cont' +-- return pos, setmetatable (t, state.objectmeta) +-- end + +-- local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) +-- local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) +-- local Value = Space * (Array + Object + SimpleValue) +-- local ExpectedValue = Value + Space * Err "value expected" +-- local ExpectedKey = String + Err "key expected" +-- local End = P(-1) * g.Cc'end' +-- local ErrInvalid = Err "invalid JSON" +-- ArrayContent = (Value * Space * (P"," * g.Cc'cont' + P"]" * g.Cc'last'+ End + ErrInvalid) + g.Cc(nil) * (P"]" * g.Cc'empty' + End + ErrInvalid)) * g.Cp() +-- local Pair = g.Cg (Space * ExpectedKey * Space * (P":" + Err "colon expected") * ExpectedValue) +-- ObjectContent = (g.Cc(nil) * g.Cc(nil) * P"}" * g.Cc'empty' + End + (Pair * Space * (P"," * g.Cc'cont' + P"}" * g.Cc'last' + End + ErrInvalid) + ErrInvalid)) * g.Cp() +-- local DecodeValue = ExpectedValue * g.Cp () + +-- jsonlpeg.version = json.version +-- jsonlpeg.encode = json.encode +-- jsonlpeg.null = json.null +-- jsonlpeg.quotestring = json.quotestring +-- jsonlpeg.addnewline = json.addnewline +-- jsonlpeg.encodeexception = json.encodeexception +-- jsonlpeg.using_lpeg = true + +-- function jsonlpeg.decode (str, pos, nullval, ...) +-- local state = {} +-- state.objectmeta, state.arraymeta = optionalmetatables(...) +-- local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) +-- if state.msg then +-- return nil, state.pos, state.msg +-- else +-- return obj, retpos +-- end +-- end + +-- -- cache result of this function: +-- json.use_lpeg = function () return jsonlpeg end +-- jsonlpeg.use_lpeg = json.use_lpeg + +-- return jsonlpeg +-- end + +-- if always_use_lpeg then +-- return json.use_lpeg() +-- end + +return json diff --git a/types/dkjson/package.json b/types/dkjson/package.json new file mode 100644 index 0000000..1a5b0ae --- /dev/null +++ b/types/dkjson/package.json @@ -0,0 +1,12 @@ +{ + "name": "@sikongjueluo/dkjson-types", + "version": "1.0.0", + "description": "TypeScript type definitions for library dkjson APIs.", + "types": "./index.d.ts", + "files": [ + "./*.d.ts", + "./index.lua" + ], + "author": "SikongJueluo", + "license": "MIT" +} diff --git a/types/toml2lua/index.d.ts b/types/toml2lua/index.d.ts new file mode 100644 index 0000000..bb9e683 --- /dev/null +++ b/types/toml2lua/index.d.ts @@ -0,0 +1,84 @@ +/** @noResolution */ + +/** + * Represents a date-time value in TOML. + * The object can have various combinations of year, month, day, hour, min, sec, and zone properties. + */ +interface TomlDate { + year?: number; + month?: number; + day?: number; + hour?: number; + min?: number; + sec?: number; + zone?: number; // timezone offset +} + +/** + * Options for TOML parsing + */ +interface ParseOptions { + /** Whether to follow the TOML spec strictly (default: true) */ + strict?: boolean; +} + +/** The current supported TOML version */ +export const version: string; + +/** Whether the parser should follow the TOML spec strictly */ +export const strict: boolean; + +/** + * Creates a date object with proper validation and string representation + * @param tab Date components to validate and create a date object from + * @returns Validated date object or nil with error message + */ +export function datefy(tab: TomlDate): TomlDate | [undefined, string]; + +/** + * Checks if a table is a date object + * @param tab The table to check + * @returns True if the table is a date object, false otherwise + */ +export function isdate(tab: unknown): boolean; + +/** + * Creates a multi-step parser for streaming TOML data + * @param options Parsing options + * @returns A coroutine-based parser function that can be called with data chunks + * and then called without arguments to get the result + */ +export function multistep_parser(options?: ParseOptions): { + (data: string): void; // Provide data chunk + (): [any, string] | any; // Get final result (call without arguments) +}; + +/** + * Parses TOML data into a Lua table + * @param data The TOML string to parse + * @param options Parsing options + * @returns The parsed data as a Lua table, or [null, error_message] on failure + */ +export function parse( + data: string, + options?: ParseOptions, +): [undefined, string] | any; + +/** + * Parse TOML and return values in toml-test intermediate format + * Useful for debugging or when you need explicit type information + * @param data The TOML string to parse + * @param options Parsing options + * @returns The parsed data in toml-test format, or [null, error_message] on failure + */ +export function parseToTestFormat( + data: string, + options?: ParseOptions, +): [undefined, string] | any; + +/** + * Encodes a Lua table to TOML format + * @param tbl The Lua table to encode + * @returns The TOML string representation of the table + */ +export function encode(tbl: any): string; diff --git a/types/toml2lua/index.lua b/types/toml2lua/index.lua new file mode 100644 index 0000000..f66ad2d --- /dev/null +++ b/types/toml2lua/index.lua @@ -0,0 +1,1767 @@ +-- The MIT License (MIT) + +-- Copyright (c) 2017 Jonathan Stoler +-- Copyright (c) 2025 Oleg Pustovit +-- Copyright (c) 2020-2025 Contributors (https://github.com/nexo-tech/toml2lua) + +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the “Software”), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: + +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. + +-- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +-- THE SOFTWARE. + +local TOML = { + -- denotes the current supported TOML version + version = "1.0.0", + + -- sets whether the parser should follow the TOML spec strictly + -- currently, no errors are thrown for the following rules if strictness is turned off: + -- tables having mixed keys + -- redefining a table + -- redefining a key within a table + strict = true, +} + +-- Value type creators for toml-test compatible intermediate format +local function createTomlTestValue(tomlType, value) + return { type = tomlType, value = tostring(value) } +end + +-- Compatibility creators that maintain current behavior for now +local function createStringValue(str) + return { value = str, type = "string" } +end + +local function createIntegerValue(num) + return { value = num, type = "integer" } +end + +local function createFloatValue(num) + return { value = num, type = "float" } +end + +local function createBooleanValue(bool) + return { value = bool, type = "boolean" } +end + +local function createDateValue(dateObj) + return { value = dateObj, type = "date" } +end + +local function createArrayValue(arr) + return { value = arr, type = "array" } +end + +-- Helper function to determine the correct TOML type for dates +local function getDateTomlType(dateObj) + if dateObj.year and dateObj.hour and dateObj.zone ~= nil then + return "datetime" + elseif dateObj.year and dateObj.hour and dateObj.zone == nil then + return "datetime-local" + elseif dateObj.year and not dateObj.hour then + return "date-local" + elseif not dateObj.year and dateObj.hour then + return "time-local" + else + return "datetime" -- fallback + end +end + +-- Convert from internal format to toml-test format +local function toTomlTestFormat(internalValue) + if internalValue.type == "string" then + return createTomlTestValue("string", internalValue.value) + elseif internalValue.type == "integer" then + return createTomlTestValue("integer", internalValue.value) + elseif internalValue.type == "float" then + -- Handle special float values + if internalValue.value == math.huge then + return createTomlTestValue("float", "inf") + elseif internalValue.value == -math.huge then + return createTomlTestValue("float", "-inf") + elseif internalValue.value ~= internalValue.value then -- NaN check + return createTomlTestValue("float", "nan") + else + return createTomlTestValue("float", internalValue.value) + end + elseif internalValue.type == "boolean" then + return createTomlTestValue("bool", internalValue.value) + elseif internalValue.type == "date" then + local dateType = getDateTomlType(internalValue.value) + return createTomlTestValue(dateType, internalValue.value) + elseif internalValue.type == "array" then + -- Arrays are handled differently - they remain as Lua tables + return internalValue + else + return internalValue -- fallback + end +end + +-- Convert from toml-test format back to Lua native types (for final output) +local function fromTomlTestFormat(tomlTestValue) + if tomlTestValue.type == "string" then + return tomlTestValue.value + elseif tomlTestValue.type == "integer" then + return tonumber(tomlTestValue.value) + elseif tomlTestValue.type == "float" then + local val = tomlTestValue.value + if val == "inf" then + return math.huge + elseif val == "-inf" then + return -math.huge + elseif val == "nan" then + return 0 / 0 + else + return tonumber(val) + end + elseif tomlTestValue.type == "bool" then + return tomlTestValue.value == "true" + elseif tomlTestValue.type:match("^date") or tomlTestValue.type:match("^time") then + return tomlTestValue.value -- date objects remain as-is + else + return tomlTestValue.value -- fallback + end +end + +local date_metatable = { + __tostring = function(t) + local rep = '' + if t.year then + rep = rep .. string.format("%04d-%02d-%02d", t.year, t.month, t.day) + end + if t.hour then + if t.year then + rep = rep .. ' ' + end + rep = rep .. string.format("%02d:%02d:", t.hour, t.min) + local sec, frac = math.modf(t.sec) + rep = rep .. string.format("%02d", sec) + if frac > 0 then + rep = rep .. tostring(frac):gsub("0(.-)0*$", "%1") + end + end + if t.zone then + if t.zone >= 0 then + rep = rep .. '+' .. string.format("%02d:00", t.zone) + elseif t.zone < 0 then + rep = rep .. '-' .. string.format("%02d:00", -t.zone) + end + end + return rep + end, +} + +local setmetatable, getmetatable = setmetatable, getmetatable + +TOML.datefy = function(tab) + -- Validate date/time components + if tab.year and (tab.year < 0 or tab.year > 9999) then + return nil, "Invalid year" + end + if tab.month and (tab.month < 1 or tab.month > 12) then + return nil, "Invalid month" + end + if tab.day and (tab.day < 1 or tab.day > 31) then + return nil, "Invalid day" + end + if tab.hour and (tab.hour < 0 or tab.hour > 23) then + return nil, "Invalid hour" + end + if tab.min and (tab.min < 0 or tab.min > 59) then + return nil, "Invalid minute" + end + if tab.sec and (tab.sec < 0 or tab.sec > 60) then -- Allow leap seconds + return nil, "Invalid second" + end + if tab.zone and (tab.zone < -23 or tab.zone > 23) then + return nil, "Invalid timezone" + end + + -- Additional validation for day based on month/year + if tab.year and tab.month and tab.day then + local days_in_month = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } + + -- Check for leap year + if tab.month == 2 and ((tab.year % 4 == 0 and tab.year % 100 ~= 0) or (tab.year % 400 == 0)) then + days_in_month[2] = 29 + end + + if tab.day > days_in_month[tab.month] then + return nil, "Invalid day for month" + end + end + + return setmetatable(tab, date_metatable) +end + +TOML.isdate = function(tab) + return getmetatable(tab) == date_metatable +end + +-- converts TOML data into a lua table +TOML.multistep_parser = function(options) + options = options or {} + local strict = (options.strict ~= nil and options.strict or TOML.strict) + local toml = '' + + -- the output table + local out = {} + local ERR = {} + + -- the current table to write to + local obj = out + + -- stores text data + local buffer = "" + + -- the current location within the string to parse + local cursor = 1 + + -- remember that the last chunk was already read + local stream_ended = false + + local nl_count = 1 + + local function result_or_error() + if #ERR > 0 then return nil, ERR[1] end + return out + end + + -- produce a parsing error message + -- the error contains the line number of the current position + local function err(message, strictOnly) + if not strictOnly or (strictOnly and strict) then + local line = 1 + local c = 0 + local msg = "At TOML line " .. nl_count .. ': ' .. message .. "." + if not ERR[msg] then + ERR[1 + #ERR] = msg + ERR[msg] = true + end + end + end + + -- read n characters (at least) or chunk terminator (nil) + local function getNewData(n) + while not stream_ended do + if cursor + (n or 0) < #toml then break end + local new_data = coroutine.yield(result_or_error()) + if new_data == nil then + stream_ended = true + break + end + toml = toml:sub(cursor) + cursor = 1 + toml = toml .. new_data + end + end + + -- TODO : use 1-based indexing ? + -- returns the next n characters from the current position + local function getData(a, b) + getNewData(b) + a = a or 0 + b = b or (toml:len() - cursor) + return toml:sub(cursor + a, cursor + b) + end + + -- returns the next n characters from the current position + local function char(n) + n = n or 0 + return getData(n, n) + end + + -- count how many new lines are in the next n chars + local function count_source_line(n) + local count = 0 + for _ in getData(0, n - 1):gmatch('\n') do + count = count + 1 + end + return count + end + + -- function to check if current position is at a newline (LF or CRLF) + local function isNewline() + if char() == "\10" then -- LF + return true + elseif char() == "\13" and char(1) == "\10" then -- CRLF + return true + end + return false + end + + -- moves the current position forward n (default: 1) characters + local function step(n) + n = n or 1 + nl_count = nl_count + count_source_line(n) + cursor = cursor + n + end + + -- prevent infinite loops by checking whether the cursor is + -- at the end of the document or not + local function bounds() + if cursor <= toml:len() then return true end + getNewData(1) + return cursor <= toml:len() + end + + -- Check if we are at end of the data + local function dataEnd() + return cursor >= toml:len() + end + + -- Match official TOML definition of whitespace + local function matchWs(n) + n = n or 0 + return getData(n, n):match("[\009\032]") + end + + -- Match the official TOML definition of newline + local function matchnl(n) + n = n or 0 + local c = getData(n, n) + if c == '\10' then return '\10' end + return getData(n, n + 1):match("^\13\10") + end + + -- move forward until the next non-whitespace character + local function skipWhitespace() + while (matchWs()) do + step() + end + end + + -- remove the (Lua) whitespace at the beginning and end of a string + local function trim(str) + return str:gsub("^%s*(.-)%s*$", "%1") + end + + -- divide a string into a table around a delimiter + local function split(str, delim) + if str == "" then return {} end + local result = {} + local append = delim + if delim:match("%%") then + append = delim:gsub("%%", "") + end + for match in (str .. append):gmatch("(.-)" .. delim) do + table.insert(result, match) + end + return result + end + + local function parseString() + local quoteType = char() -- should be single or double quote + + -- this is a multiline string if the next 2 characters match + local multiline = (char(1) == char(2) and char(1) == char()) + + -- buffer to hold the string + local str = "" + + -- skip the quotes + step(multiline and 3 or 1) + + local foundClosingQuote = false + + while (bounds()) do + if multiline and matchnl() and str == "" then + -- skip line break line at the beginning of multiline string + if char() == "\13" and char(1) == "\10" then + step(2) -- skip CRLF + else + step() -- skip LF + end + end + + -- keep going until we encounter the quote character again + if char() == quoteType then + if multiline then + if char(1) == char(2) and char(1) == quoteType then + step(3) + foundClosingQuote = true + break + end + else + step() + foundClosingQuote = true + break + end + end + + if matchnl() and not multiline then + err("Single-line string cannot contain line break") + end + + -- if we're in a double-quoted string, watch for escape characters! + if quoteType == '"' and char() == "\\" then + if multiline and matchnl(1) then + -- skip until first non-whitespace character + step(1) -- go past the line break + while (bounds()) do + if not matchWs() and not matchnl() then + break + end + if isNewline() then + if char() == "\13" and char(1) == "\10" then + step(2) -- skip CRLF + else + step() -- skip LF + end + else + step() + end + end + else + -- all available escape characters + local escape = { + b = "\b", + t = "\t", + n = "\n", + f = "\f", + r = "\r", + ['"'] = '"', + ["\\"] = "\\", + } + -- utf function from http://stackoverflow.com/a/26071044 + -- converts \uXXX into actual unicode + local function utf(char) + local bytemarkers = { { 0x7ff, 192 }, { 0xffff, 224 }, { 0x1fffff, 240 } } + if char < 128 then return string.char(char) end + local charbytes = {} + for bytes, vals in pairs(bytemarkers) do + if char <= vals[1] then + for b = bytes + 1, 2, -1 do + local mod = char % 64 + char = (char - mod) / 64 + charbytes[b] = string.char(128 + mod) + end + charbytes[1] = string.char(vals[2] + char) + break + end + end + return table.concat(charbytes) + end + + if escape[char(1)] then + -- normal escape + str = str .. escape[char(1)] + step(2) -- go past backslash and the character + elseif char(1) == "u" then + -- utf-16 + step() + local uni = char(1) .. char(2) .. char(3) .. char(4) + step(5) + local uniNum = tonumber(uni, 16) + if not uniNum then + err("Unicode escape is not a Unicode scalar") + elseif (uniNum >= 0 and uniNum <= 0xd7ff) and not (uniNum >= 0xe000 and uniNum <= 0x10ffff) then + str = str .. utf(uniNum) + else + err("Unicode escape is not a Unicode scalar") + end + elseif char(1) == "U" then + -- utf-32 + step() + local uni = char(1) .. char(2) .. char(3) .. char(4) .. char(5) .. char(6) .. char(7) .. char(8) + step(9) + local uniNum = tonumber(uni, 16) + if (uniNum >= 0 and uniNum <= 0xd7ff) and not (uniNum >= 0xe000 and uniNum <= 0x10ffff) then + str = str .. utf(uniNum) + else + err("Unicode escape is not a Unicode scalar") + end + else + err("Invalid escape") + step() + end + end + else + -- if we're not in a double-quoted string, just append it to our buffer raw and keep going + str = str .. char() + step() + end + end + + -- If we get here without finding the closing quote, it's an error + if not foundClosingQuote then + err("Unterminated string") + end + + return createStringValue(str) + end + + -- Unified date/time component matchers + local function matchDate() + local year, month, day, n = + getData(0, 10):match('^(%d%d%d%d)%-([0-1][0-9])%-([0-3][0-9])()') + + if not year then return nil end + step(n - 1) + + return tonumber(year), tonumber(month), tonumber(day) + end + + local function matchTime() + local hour, minute, second, n = + getData(0, 19):match('^([0-2][0-9])%:([0-6][0-9])%:(%d+%.?%d*)()') + + if not hour then return nil end + step(n - 1) + + return tonumber(hour), tonumber(minute), tonumber(second) + end + + local function matchTimezone() + local eastwest, offset, zero, n = + getData(0, 6):match('^([%+%-])([0-9][0-9])%:([0-9][0-9])()') + + if not eastwest then return nil end + step(n - 1) + + return tonumber(eastwest .. offset) + end + + -- Helper function to create and validate date objects + local function createValidatedDateValue(components) + local value, e = TOML.datefy(components) + if not value then + err(e) + return nil + end + return createDateValue(value) + end + + local function parseDate() + local year, month, day = matchDate() + if not year then + err("Invalid date") + return nil + end + + local hour, minute, second = nil, nil, nil + local zone = nil + + -- Check for date-time separator + if char():match('[T ]') then + step(1) + hour, minute, second = matchTime() + if not hour then + err("Invalid date") + return nil + end + + -- Check for timezone + if char():match('Z') then + step(1) + zone = 0 + else + zone = matchTimezone() + end + end + + local components = { + year = year, + month = month, + day = day, + hour = hour, + min = minute, + sec = second, + zone = zone, + } + + return createValidatedDateValue(components) + end + + local function parseTime() + local hour, minute, second = matchTime() + if not hour then + err("Invalid time") + return nil + end + + local components = { + hour = hour, + min = minute, + sec = second, + } + + return createValidatedDateValue(components) + end + + -- Helper functions for number parsing + local function isNumberTerminator() + return matchWs() or char() == "#" or matchnl() or char() == "," or char() == "]" or char() == "}" + end + + local function validateUnderscore(currentChar, nextChar, numberStr, prevUnderscore) + if currentChar == "_" then + if prevUnderscore then + err("Double underscore in number") + return false + end + if numberStr == "" then + err("Underscore cannot be at beginning of number") + return false + end + if numberStr:sub(#numberStr) == "." then + err("Underscore after decimal point") + return false + end + if nextChar == "." then + err("Underscore before decimal point") + return false + end + return true + end + return false + end + + local function parseSpecialBaseNumber() + local prefixes = { ["0x"] = 16, ["0o"] = 8, ["0b"] = 2 } + local prefix = char() .. char(1) + local base = prefixes[prefix] + if not base then return nil end + + step(2) + local digits = ({ [2] = "[01]", [8] = "[0-7]", [16] = "%x" })[base] + local num = "" + local prevUnderscore = false + + while bounds() do + if char():match(digits) then + num = num .. char() + prevUnderscore = false + elseif isNumberTerminator() then + break + elseif char() == "_" then + if prevUnderscore then + err("Double underscore in number") + return false -- Return false to indicate error + end + if num == "" then + err("Underscore cannot be at beginning of number") + return false + end + prevUnderscore = true + else + err("Invalid number") + return false + end + step() + end + + if prevUnderscore then + err("Invalid underscore at end of number") + return false + end + if num == "" then + err("Invalid number") + return false + end + + return createIntegerValue(tonumber(num, base)) + end + + local function parseNumber() + -- Try parsing special base numbers first + local specialResult = parseSpecialBaseNumber() + if specialResult == false then + return nil -- Error in special base parsing + elseif specialResult then + return specialResult -- Valid special base number + end + -- specialResult is nil, so not a special base number - continue with decimal parsing + + -- Parse decimal numbers + local num = "" + local exp = nil + local dotfound = false + local prev_underscore = false + + while bounds() do + if char():match("[%+%-%.eE_0-9]") then + if char():match '%.' then dotfound = true end + + -- Handle underscore validation + if validateUnderscore(char(), char(1), num, prev_underscore) then + prev_underscore = true + else + -- Handle exponent + if not exp then + if char():lower() == "e" then + exp = "" + elseif char() ~= "_" then + num = num .. char() + end + elseif char():match("[%+%-_0-9]") then + if char() ~= "_" then + exp = exp .. char() + end + else + err("Invalid exponent") + end + + prev_underscore = false + end + elseif isNumberTerminator() then + break + else + err("Invalid number") + end + step() + end + + if prev_underscore then + err("Invalid underscore at end of number") + return nil + end + + -- Validate number format + if num:match('^[%+%-]?0[0-9]') then + err('Leading zero found in number') + end + if dotfound and num:match('%.$') then + err('No trailing zero found in float') + end + + -- Apply exponent + local exp_val = exp and tonumber(exp) or 0 + local multiplier = 1 + if exp_val > 0 then + multiplier = math.floor(10 ^ exp_val) + elseif exp_val < 0 then + multiplier = 10 ^ exp_val + end + local finalNum = tonumber(num) * multiplier + + -- Return appropriate type + if exp_val < 0 or dotfound then + return createFloatValue(finalNum) + end + return createIntegerValue(finalNum) + end + + local parseArray, getValue + + function parseArray() + step() -- skip [ + skipWhitespace() + + local arrayType + local array = {} + + while (bounds()) do + if char() == "]" then + break + elseif matchnl() then + -- skip + step() + skipWhitespace() + elseif char() == "#" then + while (bounds() and not matchnl()) do + step() + end + else + -- get the next object in the array + local v = getValue() + if not v then break end + + -- v1.0.0 allows mixed types in arrays + if arrayType == nil then + arrayType = v.type + elseif arrayType ~= v.type then + -- Mixed types are allowed in v1.0.0, so just update arrayType to indicate mixed + arrayType = "mixed" + end + + array = array or {} + table.insert(array, v.value) + + if char() == "," then + step() + end + skipWhitespace() + end + end + + -- Check if we found the closing bracket + if not bounds() or char() ~= "]" then + err("Missing closing bracket in array") + end + step() + + return createArrayValue(array) + end + + local function parseInlineTable() + step() -- skip opening brace + + local buffer = "" + local quoted = false + local tbl = {} + + while bounds() do + if char() == "}" then + break + elseif char() == "'" or char() == '"' then + buffer = parseString().value + quoted = true + skipWhitespace() + elseif char() == "=" then + if not quoted then + buffer = trim(buffer) + end + + step() -- skip = + skipWhitespace() + + if matchnl() then + err("Newline in inline table") + end + + local v = getValue().value + tbl[buffer] = v + + skipWhitespace() + + if char() == "," then + step() + skipWhitespace() + if matchnl() then + err("Newline in inline table") + end + elseif matchnl() then + err("Newline in inline table") + end + + quoted = false + buffer = "" + else + if quoted then + if not matchWs() then + err("Unexpected character after the key") + end + else + if matchnl() then + err("Newline in inline table") + end + buffer = buffer .. char() + end + step() + end + end + + -- Check if we found the closing brace + if not bounds() or char() ~= "}" then + err("Missing closing brace in inline table") + end + step() -- skip closing brace + + return createArrayValue(tbl) + end + + local function parseBoolean() + local v + if getData(0, 3) == "true" then + step(4) + v = createBooleanValue(true) + elseif getData(0, 4) == "false" then + step(5) + v = createBooleanValue(false) + elseif getData(0, 2) == "inf" then + step(3) + v = createFloatValue(math.huge) + elseif getData(0, 3) == "+inf" then + step(4) + v = createFloatValue(math.huge) + elseif getData(0, 3) == "-inf" then + step(4) + v = createFloatValue(-math.huge) + elseif getData(0, 2) == "nan" then + step(3) + v = createFloatValue(0 / 0) + elseif getData(0, 3) == "+nan" then + step(4) + v = createFloatValue(0 / 0) + elseif getData(0, 3) == "-nan" then + step(4) + v = createFloatValue(0 / 0) + else + err("Invalid primitive") + end + + skipWhitespace() + if char() == "#" then + while (bounds() and not matchnl()) do + step() + end + end + + return v + end + + -- Value type detection helpers + local function isStringStart() + return char() == '"' or char() == "'" + end + + local function isDateStart() + return getData(0, 5):match("^%d%d%d%d%-%d") + end + + local function isTimeStart() + return getData(0, 3):match("^%d%d%:%d") + end + + local function isSpecialFloat() + local data2 = getData(0, 2) + local data3 = getData(0, 3) + return data2 == "inf" or data3 == "+inf" or data3 == "-inf" or + data2 == "nan" or data3 == "+nan" or data3 == "-nan" + end + + local function isNumberStart() + return char():match("[%+%-0-9]") + end + + local function isArrayStart() + return char() == "[" + end + + local function isInlineTableStart() + return char() == "{" + end + + -- figure out the type and get the next value in the document + function getValue() + if isStringStart() then + return parseString() + elseif isDateStart() then + return parseDate() + elseif isTimeStart() then + return parseTime() + elseif isSpecialFloat() then + return parseBoolean() -- Special float values handled in parseBoolean + elseif isNumberStart() then + return parseNumber() + elseif isArrayStart() then + return parseArray() + elseif isInlineTableStart() then + return parseInlineTable() + else + return parseBoolean() + end + end + + local function parse() + -- track whether the current key was quoted or not + local quotedKey = false + + local function check_key() + if buffer == "" then + err("Empty key") + end + if buffer:match("[%s%c%%%(%)%*%+%.%?%[%]!\"#$&',/:;<=>@`\\^{|}~]") and not quotedKey then + err('Invalid character in key') + end + end + + -- avoid double table definition + local defined_table = setmetatable({}, { __mode = 'kv' }) + + -- keep track of container type i.e. table vs array + local container_type = setmetatable({}, { __mode = 'kv' }) + + local function processKey(isLast, tableArray, quotedKey) + if isLast and obj[buffer] and not tableArray and #obj[buffer] > 0 then + err("Cannot redefine table", true) + end + + -- set obj to the appropriate table so we can start + -- filling it with values! + if tableArray then + -- push onto cache + local current = obj[buffer] + + -- crete as needed + identify table vs array + local isArray = false + if current then + isArray = (container_type[current] == 'array') + else + current = {} + obj[buffer] = current + if isLast then + isArray = true + container_type[current] = 'array' + else + isArray = false + container_type[current] = 'hash' + end + end + + if isLast and not isArray then + err('The selected key contains a table, not an array', true) + end + + -- update current object + if not isLast then obj = current end + if isArray then + if isLast then table.insert(current, {}) end + obj = current[#current] + end + else + local newObj = obj[buffer] or {} + obj[buffer] = newObj + if #newObj > 0 then + if type(newObj) ~= 'table' then + err('Duplicate field') + else + -- an array is already in progress for this key, so modify its + -- last element, instead of the array itself + obj = newObj[#newObj] + end + else + obj = newObj + end + end + if isLast then + if defined_table[obj] then + err('Duplicated table definition') + end + defined_table[obj] = true + end + end + + -- track dotted key parsing state + local dottedKeyParts = {} + local inDottedKey = false + + -- parse the document! + while (bounds()) do + -- skip comments and whitespace + -- Only treat # as comment if we're not in the middle of parsing a key + if char() == "#" and (trim(buffer) == "" or quotedKey) then + while (bounds() and not matchnl()) do + step() + end + end + + if matchnl() then + if trim(buffer) ~= '' then + err('Invalid key') + end + buffer = "" + dottedKeyParts = {} + inDottedKey = false + step() + elseif char() == "=" then + step() + skipWhitespace() + + -- Add current buffer to dotted key parts if we're in a dotted key + if inDottedKey then + if not quotedKey then + buffer = trim(buffer) + end + if buffer ~= "" then + table.insert(dottedKeyParts, buffer) + end + end + + -- Handle dotted keys vs regular keys + if inDottedKey and #dottedKeyParts > 1 then + -- This is a dotted key - create nested structure + local currentObj = obj + local conflictDetected = false + + for i = 1, #dottedKeyParts - 1 do + local key = dottedKeyParts[i] + local numericKey = key + if key:match("^[0-9]+$") then + numericKey = tonumber(key) + end + if numericKey and not currentObj[numericKey] then + currentObj[numericKey] = {} + elseif type(currentObj[numericKey]) ~= "table" then + err('Cannot create table: key "' .. key .. '" already has a non-table value') + conflictDetected = true + break + end + currentObj = currentObj[numericKey] + end + + if not conflictDetected then + local finalKey = dottedKeyParts[#dottedKeyParts] + local finalNumericKey = finalKey + if finalKey:match("^[0-9]+$") then + finalNumericKey = tonumber(finalKey) + end + + local v = getValue() + if v then + if currentObj[finalNumericKey] ~= nil then + err('Cannot redefine key "' .. finalKey .. '"', true) + end + if finalNumericKey then + currentObj[finalNumericKey] = v.value + end + end + else + -- Still need to consume the value even if there was a conflict + getValue() + end + elseif not quotedKey and buffer:find("%.") then + -- Handle simple dotted keys (backward compatibility) + local keys = {} + for key in buffer:gmatch("[^%.]+") do + table.insert(keys, key) + end + + -- Validate each key segment + for _, key in ipairs(keys) do + local tempBuffer = key + if tempBuffer:match("[%s%c%%%(%)%*%+%.%?%[%]!\"#$&',/:;<=>@`\\^{|}~]") then + err('Invalid character in key') + end + end + + -- Navigate/create the nested structure + local currentObj = obj + local conflictDetected = false + for i = 1, #keys - 1 do + local key = keys[i] + local numericKey = key + if key:match("^[0-9]+$") then + numericKey = tonumber(key) + end + if numericKey and not currentObj[numericKey] then + currentObj[numericKey] = {} + elseif type(currentObj[numericKey]) ~= "table" then + err('Cannot create table: key "' .. key .. '" already has a non-table value') + conflictDetected = true + break + end + currentObj = currentObj[numericKey] + end + + -- Set the final key only if no conflict was detected + if not conflictDetected then + local finalKey = keys[#keys] + local finalNumericKey = finalKey + if finalKey:match("^[0-9]+$") then + finalNumericKey = tonumber(finalKey) + end + + local v = getValue() + if v and finalNumericKey then + if currentObj[finalNumericKey] ~= nil then + err('Cannot redefine key "' .. finalKey .. '"', true) + end + currentObj[finalNumericKey] = v.value + end + else + -- Still need to consume the value even if there was a conflict + getValue() + end + else + -- Regular key handling + if not quotedKey then + buffer = trim(buffer) + check_key() + end + + local keyForAccess = buffer + if buffer:match("^[0-9]+$") and not quotedKey then + local numericBuffer = tonumber(buffer) + if numericBuffer then + keyForAccess = numericBuffer + end + end + + if buffer == "" and not quotedKey then + err("Empty key name") + end + local v = getValue() + if v then + -- if the key already exists in the current object, throw an error + if obj[keyForAccess] ~= nil then + err('Cannot redefine key "' .. buffer .. '"', true) + end + obj[keyForAccess] = v.value + end + end + + -- clear the buffer and reset dotted key state + buffer = "" + dottedKeyParts = {} + inDottedKey = false + quotedKey = false + + -- skip whitespace and comments + skipWhitespace() + if char() == "#" then + while (bounds() and not matchnl()) do + step() + end + end + + -- if there is anything left on this line after parsing a key and its value, + -- throw an error + if not dataEnd() and not matchnl() then + err("Invalid primitive") + end + elseif char() == "[" then + if trim(buffer) ~= '' then + err("Invalid key") + end + + buffer = "" + step() + local tableArray = false + + -- if there are two brackets in a row, it's a table array! + if char() == "[" then + tableArray = true + step() + end + + obj = out + + while (bounds()) do + if char() == "]" then + break + elseif char() == '"' or char() == "'" then + buffer = parseString().value + quotedKey = true + elseif char() == "." then + step() -- skip period + if not quotedKey then + buffer = trim(buffer) + end + if not quotedKey then check_key() end + processKey(false, tableArray, quotedKey) + buffer = "" + elseif char() == "[" then + err('Invalid character in key') + step() + else + buffer = buffer .. char() + step() + end + end + if tableArray then + if char(1) ~= "]" then + err("Mismatching brackets") + else + step() -- skip inside bracket + end + end + step() -- skip outside bracket + if not quotedKey then + buffer = trim(buffer) + end + if not quotedKey then check_key() end + processKey(true, tableArray, quotedKey) + buffer = "" + buffer = "" + quotedKey = false + skipWhitespace() + if bounds() and (not char():match('#') and not matchnl()) then + err("Something found on the same line of a table definition") + end + elseif char() == "." then + -- Handle dot in dotted key + if buffer == "" then + err("Empty key segment before dot") + end + + -- Add current buffer content to dotted key parts + if not quotedKey then + buffer = trim(buffer) + end + if buffer == "" then + err("Empty key segment") + end + table.insert(dottedKeyParts, buffer) + inDottedKey = true + buffer = "" + quotedKey = false + step() + elseif (char() == '"' or char() == "'") then + -- quoted key + buffer = parseString().value + quotedKey = true + else + if not quotedKey then + buffer = buffer .. (matchnl() and "" or char()) + end + step() + end + end + + -- Check for incomplete line at end of file + if trim(buffer) ~= '' then + err('Invalid key') + end + + return result_or_error() + end + + local coparse = coroutine.wrap(parse) + coparse() + return coparse +end + +TOML.parse = function(data, options) + local cp = TOML.multistep_parser(options) + cp(data) + return cp() +end + +-- Parse TOML and return values in toml-test intermediate format +-- This can be useful for debugging or when you need explicit type information +TOML.parseToTestFormat = function(data, options) + options = options or {} + local originalParser = TOML.multistep_parser(options) + + -- Create a modified parser that returns toml-test format + local function convertToTestFormat(result) + if type(result) ~= "table" then + return result + end + + local converted = {} + for key, value in pairs(result) do + if type(value) == "table" and value.type and value.value ~= nil then + -- This looks like an intermediate format value, convert it + converted[key] = toTomlTestFormat(value) + elseif type(value) == "table" then + -- Recursively convert nested tables + converted[key] = convertToTestFormat(value) + else + -- Native Lua value, wrap it appropriately + local valueType = type(value) + if valueType == "string" then + converted[key] = createTomlTestValue("string", value) + elseif valueType == "number" then + if value == math.floor(value) then + converted[key] = createTomlTestValue("integer", value) + else + converted[key] = createTomlTestValue("float", value) + end + elseif valueType == "boolean" then + converted[key] = createTomlTestValue("bool", value) + else + converted[key] = value + end + end + end + return converted + end + + originalParser(data) + local result = originalParser() + if result then + return convertToTestFormat(result) + end + return result +end + +TOML.encode = function(tbl) + local toml = "" + + local cache = {} + + -- Helper function to encode keys properly according to TOML v1.0.0 spec + local function encodeKey(key) + local keyStr = tostring(key) + + -- Empty keys must be quoted + if keyStr == "" then + return '""' + end + + -- Check if the key needs quoting (contains special characters) + -- Bare keys may only contain ASCII letters, ASCII digits, underscores, and dashes (A-Za-z0-9_-) + if keyStr:match("^[A-Za-z0-9_%-]+$") then + return keyStr + else + -- Key needs to be quoted, escape quotes and backslashes + local escapedKey = keyStr:gsub("\\", "\\\\"):gsub('"', '\\"') + return '"' .. escapedKey .. '"' + end + end + + -- Helper function to encode dotted table names + local function encodeDottedName(keyList) + local encodedKeys = {} + for i, key in ipairs(keyList) do + table.insert(encodedKeys, encodeKey(key)) + end + return table.concat(encodedKeys, ".") + end + + -- Helper function to get sorted keys for consistent output order + local function getSortedKeys(t) + local keys = {} + for k in pairs(t) do + table.insert(keys, k) + end + -- Sort keys, handling mixed types gracefully + table.sort(keys, function(a, b) + local ta, tb = type(a), type(b) + if ta == tb then + return tostring(a) > tostring(b) -- Reverse alphabetical order + else + return ta < tb -- type names sorted alphabetically + end + end) + return keys + end + + local function parse(tbl) + local keys = getSortedKeys(tbl) + + -- First pass: handle all non-table values + for _, k in ipairs(keys) do + local v = tbl[k] + if type(v) == "boolean" then + toml = toml .. encodeKey(k) .. " = " .. tostring(v) .. "\n" + elseif type(v) == "number" then + -- Handle special float values for v1.0.0 compatibility + if v == math.huge then + toml = toml .. encodeKey(k) .. " = inf\n" + elseif v == -math.huge then + toml = toml .. encodeKey(k) .. " = -inf\n" + elseif v ~= v then -- NaN check (NaN != NaN) + toml = toml .. encodeKey(k) .. " = nan\n" + else + toml = toml .. encodeKey(k) .. " = " .. tostring(v) .. "\n" + end + elseif type(v) == "string" then + local quote = '"' + v = v:gsub("\\", "\\\\") + + -- if the string has any line breaks, make it multiline + if v:match("^\n(.*)$") then + quote = quote:rep(3) + v = "\\n" .. v + elseif v:match("\n") then + quote = quote:rep(3) + end + + v = v:gsub("\b", "\\b") + v = v:gsub("\t", "\\t") + v = v:gsub("\f", "\\f") + v = v:gsub("\r", "\\r") + v = v:gsub('"', '\\"') + toml = toml .. encodeKey(k) .. " = " .. quote .. v .. quote .. "\n" + elseif type(v) == "table" and getmetatable(v) == date_metatable then + toml = toml .. encodeKey(k) .. " = " .. tostring(v) .. "\n" + end + end + + -- Second pass: handle simple array values (arrays of non-tables) + for _, k in ipairs(keys) do + local v = tbl[k] + if type(v) == "table" and getmetatable(v) ~= date_metatable then + -- Check if this is an array (all numeric keys) + local isArray = true + local isArrayOfHashTables = true + for kk, vv in pairs(v) do + if type(kk) ~= "number" then + isArray = false + break + end + if type(vv) ~= "table" then + isArrayOfHashTables = false + else + -- Check if the inner table is a hash table (has non-numeric keys) + local isHashTable = false + local hasKeys = false + for kkk, vvv in pairs(vv) do + hasKeys = true + if type(kkk) ~= "number" then + isHashTable = true + break + end + end + -- Empty tables are considered hash tables for array of tables syntax + if hasKeys and not isHashTable then + isArrayOfHashTables = false + end + end + end + + if isArray and not isArrayOfHashTables then + -- Check if this is an array of arrays (all elements are arrays) + local isArrayOfArrays = true + for kk, vv in pairs(v) do + if type(vv) ~= "table" then + isArrayOfArrays = false + break + end + -- Check if the inner table is also an array + for kkk, vvv in pairs(vv) do + if type(kkk) ~= "number" then + isArrayOfArrays = false + break + end + end + if not isArrayOfArrays then break end + end + + if isArrayOfArrays then + -- This is an array of arrays, encode as nested arrays + toml = toml .. encodeKey(k) .. " = [" + local first_outer = true + for kk, vv in pairs(v) do + if not first_outer then + toml = toml .. ", " + end + toml = toml .. "[" + local first_inner = true + for kkk, vvv in pairs(vv) do + if not first_inner then + toml = toml .. ", " + end + if type(vvv) == "number" then + -- Check if any number in any array is a float + local hasFloat = false + for _, arr in pairs(v) do + for _, val in pairs(arr) do + if type(val) == "number" and val ~= math.floor(val) then + hasFloat = true + break + end + end + if hasFloat then break end + end + if hasFloat then + toml = toml .. string.format("%.1f", vvv) + else + toml = toml .. tostring(vvv) + end + else + toml = toml .. tostring(vvv) + end + first_inner = false + end + toml = toml .. "]" + first_outer = false + end + toml = toml .. "]\n" + else + -- This is a simple array, use multi-line format + toml = toml .. encodeKey(k) .. " = [\n" + for kk, vv in pairs(v) do + if type(vv) == "string" then + local escaped_string = vv + escaped_string = escaped_string:gsub("\\", "\\\\") + escaped_string = escaped_string:gsub("\b", "\\b") + escaped_string = escaped_string:gsub("\t", "\\t") + escaped_string = escaped_string:gsub("\f", "\\f") + escaped_string = escaped_string:gsub("\r", "\\r") + escaped_string = escaped_string:gsub("\n", "\\n") + escaped_string = escaped_string:gsub('"', '\\"') + toml = toml .. '"' .. escaped_string .. '",\n' + else + toml = toml .. tostring(vv) .. ",\n" + end + end + toml = toml .. "]\n" + end + end + end + end + + -- Third pass: handle hash table values and arrays of tables + for _, k in ipairs(keys) do + local v = tbl[k] + if type(v) == "table" and getmetatable(v) ~= date_metatable then + -- Check if this is an array (all numeric keys) + local isArray = true + local isArrayOfHashTables = true + for kk, vv in pairs(v) do + if type(kk) ~= "number" then + isArray = false + break + end + if type(vv) ~= "table" then + isArrayOfHashTables = false + else + -- Check if the inner table is a hash table (has non-numeric keys) + local isHashTable = false + local hasKeys = false + for kkk, vvv in pairs(vv) do + hasKeys = true + if type(kkk) ~= "number" then + isHashTable = true + break + end + end + -- Empty tables are considered hash tables for array of tables syntax + if hasKeys and not isHashTable then + isArrayOfHashTables = false + end + end + end + + if isArray and isArrayOfHashTables then + -- This is an array of hash tables, use [[table]] syntax + for kk, vv in pairs(v) do + toml = toml .. "[[" .. encodeKey(k) .. "]]\n" + if type(vv) == "table" then + parse(vv) + end + end + elseif not isArray then + local array, arrayTable = true, true + local first = {} + local tableCopy = {} + for kk, vv in pairs(v) do + if type(kk) ~= "number" then array = false end + if type(vv) ~= "table" then + first[kk] = vv + arrayTable = false + else + tableCopy[kk] = vv + end + end + + if array then + if arrayTable then + -- Check if inner tables are arrays (all numeric keys) or hash tables + local innerTablesAreArrays = true + for kk, vv in pairs(tableCopy) do + for k3, v3 in pairs(vv) do + if type(k3) ~= "number" then + innerTablesAreArrays = false + break + end + end + if not innerTablesAreArrays then + break + end + end + + if innerTablesAreArrays then + -- This is an array of arrays, encode as nested array + toml = toml .. encodeKey(k) .. " = [" + + -- Check if any element in any array is a float to determine formatting + local hasFloat = false + for kk, vv in pairs(tableCopy) do + for k3, v3 in pairs(vv) do + if type(v3) == "number" and v3 ~= math.floor(v3) then + hasFloat = true + break + end + end + if hasFloat then break end + end + + local first_element = true + for kk, vv in pairs(tableCopy) do + if not first_element then + toml = toml .. ", " + end + toml = toml .. "[" + local first_inner = true + + for k3, v3 in pairs(vv) do + if not first_inner then + toml = toml .. ", " + end + if type(v3) == "string" then + toml = toml .. '"' .. v3 .. '"' + elseif type(v3) == "number" then + if hasFloat then + -- Format all numbers as floats for consistency + toml = toml .. string.format("%.1f", v3) + else + toml = toml .. tostring(v3) + end + else + toml = toml .. tostring(v3) + end + first_inner = false + end + toml = toml .. "]" + first_element = false + end + toml = toml .. "]\n" + else + -- double bracket syntax go! + table.insert(cache, k) + for kk, vv in pairs(tableCopy) do + toml = toml .. "[[" .. encodeDottedName(cache) .. "]]\n" + local tableCopyInner = {} + local firstInner = {} + local sortedKeys = getSortedKeys(vv) + for _, k3 in ipairs(sortedKeys) do + local v3 = vv[k3] + if type(v3) ~= "table" then + firstInner[k3] = v3 + else + tableCopyInner[k3] = v3 + end + end + parse(firstInner) + parse(tableCopyInner) + end + table.remove(cache) + end + else + -- plain ol boring array + toml = toml .. encodeKey(k) .. " = [\n" + local quote = '"' + for kk, vv in pairs(first) do + if type(vv) == "string" then + local escaped_string = vv + escaped_string = escaped_string:gsub("\\", "\\\\") + escaped_string = escaped_string:gsub("\b", "\\b") + escaped_string = escaped_string:gsub("\t", "\\t") + escaped_string = escaped_string:gsub("\f", "\\f") + escaped_string = escaped_string:gsub("\r", "\\r") + escaped_string = escaped_string:gsub("\n", "\\n") + escaped_string = escaped_string:gsub('"', '\\"') + toml = toml .. quote .. escaped_string .. quote .. ",\n" + else + toml = toml .. tostring(vv) .. ",\n" + end + end + toml = toml .. "]\n" + end + else + -- just a key/value table, folks + table.insert(cache, k) + toml = toml .. "[" .. encodeDottedName(cache) .. "]\n" + parse(first) + parse(tableCopy) + table.remove(cache) + end + end + end + end + end + + parse(tbl) + + return toml:sub(1, -2) +end + +return TOML diff --git a/types/toml2lua/package.json b/types/toml2lua/package.json new file mode 100644 index 0000000..0d993f7 --- /dev/null +++ b/types/toml2lua/package.json @@ -0,0 +1,12 @@ +{ + "name": "@sikongjueluo/toml2lua-types", + "version": "1.0.0", + "description": "TypeScript type definitions for library toml2lua APIs.", + "types": "./index.d.ts", + "files": [ + "./*.d.ts", + "./index.lua" + ], + "author": "SikongJueluo", + "license": "MIT" +}