UNPKG

@xmcl/mod-parser

Version:

The utilities to parse Forge/Liteloader/Fabric/Quilt mod metadata.

4 lines 121 kB
{ "version": 3, "sources": ["../forge.ts", "../../../node_modules/.pnpm/smol-toml@1.4.1/node_modules/smol-toml/dist/error.js", "../../../node_modules/.pnpm/smol-toml@1.4.1/node_modules/smol-toml/dist/util.js", "../../../node_modules/.pnpm/smol-toml@1.4.1/node_modules/smol-toml/dist/date.js", "../../../node_modules/.pnpm/smol-toml@1.4.1/node_modules/smol-toml/dist/primitive.js", "../../../node_modules/.pnpm/smol-toml@1.4.1/node_modules/smol-toml/dist/extract.js", "../../../node_modules/.pnpm/smol-toml@1.4.1/node_modules/smol-toml/dist/struct.js", "../../../node_modules/.pnpm/smol-toml@1.4.1/node_modules/smol-toml/dist/parse.js", "../forgeConfig.ts", "../liteloader.ts", "../fabric.ts", "../quilt.ts"], "sourcesContent": ["import { resolveFileSystem, FileSystem } from '@xmcl/system'\nimport { TomlDate, parse as parseToml } from 'smol-toml'\nimport { AnnotationVisitor, ClassReader, ClassVisitor, MethodVisitor, Opcodes } from '@xmcl/asm'\n\n/**\n * The @Mod data from class file\n */\nexport interface ForgeModAnnotationData {\n [key: string]: any\n value: string\n modid: string\n name: string\n version: string\n /**\n * A dependency string for this mod, which specifies which mod(s) it depends on in order to run.\n *\n * A dependency string must start with a combination of these prefixes, separated by \"-\":\n * [before, after], [required], [client, server]\n * At least one \"before\", \"after\", or \"required\" must be specified.\n * Then \":\" and the mod id.\n * Then a version range should be specified for the mod by adding \"@\" and the version range.\n * The version range format is described in the javadoc here:\n * {@link VersionRange#createFromVersionSpec(java.lang.String)}\n * Then a \";\".\n *\n * If a \"required\" mod is missing, or a mod exists with a version outside the specified range,\n * the game will not start and an error screen will tell the player which versions are required.\n *\n * Example:\n * Our example mod:\n * * depends on Forge and uses new features that were introduced in Forge version 14.21.1.2395\n * \"required:forge@[14.21.1.2395,);\"\n *\n * 1.12.2 Note: for compatibility with Forge older than 14.23.0.2501 the syntax must follow this older format:\n * \"required-after:forge@[14.21.1.2395,);\"\n * For more explanation see https://github.com/MinecraftForge/MinecraftForge/issues/4918\n *\n * * is a dedicated addon to mod1 and has to have its event handlers run after mod1's are run,\n * \"required-after:mod1;\"\n * * has optional integration with mod2 which depends on features introduced in mod2 version 4.7.0,\n * \"after:mod2@[4.7.0,);\"\n * * depends on a client-side-only rendering library called rendermod\n * \"required-client:rendermod;\"\n *\n * The full dependencies string is all of those combined:\n * \"required:forge@[14.21.1.2395,);required-after:mod1;after:mod2@[4.7.0,);required-client:rendermod;\"\n *\n * This will stop the game and display an error message if any of these is true:\n * The installed forge is too old,\n * mod1 is missing,\n * an old version of mod2 is present,\n * rendermod is missing on the client.\n */\n dependencies: string\n useMetadata: boolean\n acceptedMinecraftVersions: string\n acceptableRemoteVersions: string\n acceptableSaveVersions: string\n modLanguage: string\n modLanguageAdapter: string\n clientSideOnly: boolean\n serverSideOnly: boolean\n}\n\n/**\n * Represent the forge `mcmod.info` format.\n */\nexport interface ForgeModMcmodInfo {\n /**\n * The modid this description is linked to. If the mod is not loaded, the description is ignored.\n */\n modid: string\n /**\n * The user-friendly name of this mod.\n */\n name: string\n /**\n * A description of this mod in 1-2 paragraphs.\n */\n description: string\n /**\n * The version of the mod.\n */\n version: string\n /**\n * The Minecraft version.\n */\n mcversion: string\n /**\n * A link to the mod\u2019s homepage.\n */\n url: string\n /**\n * Defined but unused. Superseded by updateJSON.\n */\n updateUrl: string\n /**\n * The URL to a version JSON.\n */\n updateJSON: string\n /**\n * A list of authors to this mod.\n */\n authorList: string[]\n /**\n * A string that contains any acknowledgements you want to mention.\n */\n credits: string\n /**\n * The path to the mod\u2019s logo. It is resolved on top of the classpath, so you should put it in a location where the name will not conflict, maybe under your own assets folder.\n */\n logoFile: string\n /**\n * A list of images to be shown on the info page. Currently unimplemented.\n */\n screenshots: string[]\n /**\n * The modid of a parent mod, if applicable. Using this allows modules of another mod to be listed under it in the info page, like BuildCraft.\n */\n parent: string\n /**\n * If true and `Mod.useMetadata`, the below 3 lists of dependencies will be used. If not, they do nothing.\n */\n useDependencyInformation: boolean\n /**\n * A list of modids. If one is missing, the game will crash. This does not affect the ordering of mod loading! To specify ordering as well as requirement, have a coupled entry in dependencies.\n */\n requiredMods: string[]\n /**\n * A list of modids. All of the listed mods will load before this one. If one is not present, nothing happens.\n */\n dependencies: string[]\n /**\n * A list of modids. All of the listed mods will load after this one. If one is not present, nothing happens.\n */\n dependants: string[]\n}\n\n/**\n * This file defines the metadata of your mod. Its information may be viewed by users from the main screen of the game through the Mods button. A single info file can describe several mods.\n *\n * The mods.toml file is formatted as TOML, the example mods.toml file in the MDK provides comments explaining the contents of the file. It should be stored as src/main/resources/META-INF/mods.toml. A basic mods.toml, describing one mod, may look like this:\n */\nexport interface ForgeModTOMLData {\n /**\n * The modid this file is linked to\n */\n modid: string\n /**\n * The version of the mod.It should be just numbers seperated by dots, ideally conforming to Semantic Versioning\n */\n version: string\n /**\n * The user - friendly name of this mod\n */\n displayName: string\n /**\n * The URL to a version JSON\n */\n updateJSONURL: string\n /**\n * A link to the mod\u2019s homepage\n */\n displayURL: string\n /**\n * The filename of the mod\u2019s logo.It must be placed in the root resource folder, not in a subfolder\n */\n logoFile: string\n /**\n * A string that contains any acknowledgements you want to mention\n */\n credits: string\n /**\n * The authors to this mod\n */\n authors: string\n /**\n * A description of this mod\n */\n description: string\n /**\n * A list of dependencies of this mod\n */\n dependencies: { modId: string; mandatory: boolean; versionRange: string; ordering: 'NONE' | 'BEFORE' | 'AFTER'; side: 'BOTH' | 'CLIENT' | 'SERVER' }[]\n\n provides: string[]\n /**\n * The name of the mod loader type to load - for regular FML @Mod mods it should be javafml\n */\n modLoader: string\n /**\n * A version range to match for said mod loader - for regular FML @Mod it will be the forge version\n */\n loaderVersion: string\n /**\n * A URL to refer people to when problems occur with this mod\n */\n issueTrackerURL: string\n}\n\nexport interface ForgeModASMData {\n /**\n * Does class files contain cpw package\n */\n usedLegacyFMLPackage: boolean\n /**\n * Does class files contain forge package\n */\n usedForgePackage: boolean\n /**\n * Does class files contain minecraft package\n */\n usedMinecraftPackage: boolean\n /**\n * Does class files contain minecraft.client package\n */\n usedMinecraftClientPackage: boolean\n\n fmlPluginClassName?: string\n fmlPluginMcVersion?: string\n\n modAnnotations: ForgeModAnnotationData[]\n}\n\n/**\n * The metadata inferred from manifest\n */\nexport interface ManifestMetadata {\n modid: string\n name: string\n authors: string[]\n version: string\n description: string\n url: string\n}\n\nclass ModAnnotationVisitor extends AnnotationVisitor {\n constructor(readonly map: ForgeModAnnotationData) { super(Opcodes.ASM5) }\n public visit(s: string, o: any) {\n if (s === 'value') {\n this.map.modid = o\n } else {\n this.map[s] = o\n }\n }\n}\nclass McVersionAnnotationVisitor extends AnnotationVisitor {\n constructor(readonly map: (v: string) => void) { super(Opcodes.ASM5) }\n public visit(s: string, o: any) {\n if (s === 'value') {\n this.map(o)\n }\n }\n}\n\nclass DummyModConstructorVisitor extends MethodVisitor {\n private stack: any[] = []\n constructor(private parent: ModClassVisitor, api: number) {\n super(api)\n }\n\n visitLdcInsn(value: any) {\n this.stack.push(value)\n }\n\n visitFieldInsn(opcode: number, owner: string, name: string, desc: string) {\n if (opcode === Opcodes.PUTFIELD) {\n const last = this.stack.pop()\n if (last) {\n if (name === 'modId') {\n this.parent.guess.modid = last\n } else if (name === 'version') {\n this.parent.guess.version = last\n } else if (name === 'name') {\n this.parent.guess.name = last\n } else if (name === 'url') {\n this.parent.guess.url = last\n } else if (name === 'parent') {\n this.parent.guess.parent = last\n } else if (name === 'mcversion') {\n this.parent.guess.mcversion = last\n }\n }\n }\n }\n}\n\nclass ModClassVisitor extends ClassVisitor {\n public fields: Record<string, any> = {}\n public className = ''\n public isDummyModContainer = false\n public isPluginClass = false\n public mcVersionInPlugin = ''\n public pluginName = ''\n\n public constructor(readonly result: ForgeModASMData, public guess: Partial<ForgeModAnnotationData>, readonly corePlugin?: string) {\n super(Opcodes.ASM5)\n }\n\n private validateType(desc: string) {\n if (desc.indexOf('net/minecraftforge') !== -1) {\n this.result.usedForgePackage = true\n }\n if (desc.indexOf('net/minecraft') !== -1) {\n this.result.usedMinecraftPackage = true\n }\n if (desc.indexOf('cpw/mods/fml') !== -1) {\n this.result.usedLegacyFMLPackage = true\n }\n if (desc.indexOf('net/minecraft/client') !== -1) {\n this.result.usedMinecraftClientPackage = true\n }\n }\n\n visit(version: number, access: number, name: string, signature: string, superName: string, interfaces: string[]): void {\n this.className = name\n this.isPluginClass = name === this.corePlugin\n if (superName === 'net/minecraftforge/fml/common/DummyModContainer') {\n this.isDummyModContainer = true\n }\n this.validateType(superName)\n for (const intef of interfaces) {\n this.validateType(intef)\n if (intef.indexOf('net/minecraftforge/fml/relauncher/IFMLLoadingPlugin') !== -1) {\n this.result.fmlPluginClassName = name\n }\n }\n }\n\n public visitMethod(access: number, name: string, desc: string, signature: string, exceptions: string[]) {\n if (this.isDummyModContainer && name === '<init>') {\n return new DummyModConstructorVisitor(this, Opcodes.ASM5)\n }\n this.validateType(desc)\n return null\n }\n\n public visitField(access: number, name: string, desc: string, signature: string, value: any) {\n this.fields[name] = value\n return null\n }\n\n public visitAnnotation(desc: string, visible: boolean): AnnotationVisitor | null {\n if (desc === 'Lnet/minecraftforge/fml/common/Mod;' || desc === 'Lcpw/mods/fml/common/Mod;') {\n const annotationData: ForgeModAnnotationData = {\n modid: '',\n name: '',\n version: '',\n dependencies: '',\n useMetadata: true,\n clientSideOnly: false,\n serverSideOnly: false,\n acceptedMinecraftVersions: '',\n acceptableRemoteVersions: '',\n acceptableSaveVersions: '',\n modLanguage: 'java',\n modLanguageAdapter: '',\n value: '',\n }\n this.result.modAnnotations.push(annotationData)\n return new ModAnnotationVisitor(annotationData)\n } else if (desc === 'Lnet/minecraftforge/fml/relauncher/IFMLLoadingPlugin$MCVersion;') {\n return new McVersionAnnotationVisitor((v) => { this.result.fmlPluginMcVersion = v })\n }\n return null\n }\n\n visitEnd() {\n if ((this.className === 'Config' || this.className === 'net/optifine/Config' || this.className === 'notch/net/optifine/Config') && this.fields && this.fields.OF_NAME) {\n this.result.modAnnotations.push({\n modid: this.fields.OF_NAME,\n name: this.fields.OF_NAME,\n mcversion: this.fields.MC_VERSION,\n version: `${this.fields.OF_EDITION}_${this.fields.OF_RELEASE}`,\n description: 'OptiFine is a Minecraft optimization mod. It allows Minecraft to run faster and look better with full support for HD textures and many configuration options.',\n authorList: ['sp614x'],\n url: 'https://optifine.net',\n clientSideOnly: true,\n serverSideOnly: false,\n value: '',\n dependencies: '',\n useMetadata: false,\n acceptableRemoteVersions: '',\n acceptableSaveVersions: '',\n acceptedMinecraftVersions: `[${this.fields.MC_VERSION}]`,\n modLanguage: 'java',\n modLanguageAdapter: '',\n })\n }\n for (const [k, v] of Object.entries(this.fields)) {\n switch (k.toUpperCase()) {\n case 'MODID':\n case 'MOD_ID':\n this.guess.modid = this.guess.modid || v\n break\n case 'MODNAME':\n case 'MOD_NAME':\n this.guess.name = this.guess.name || v\n break\n case 'VERSION':\n case 'MOD_VERSION':\n this.guess.version = this.guess.version || v\n break\n case 'MCVERSION':\n this.guess.mcversion = this.guess.mcversion || v\n break\n }\n }\n }\n}\n\n/**\n * Read the mod info from `META-INF/MANIFEST.MF`\n * @returns The manifest directionary\n */\nexport async function readForgeModManifest(mod: ForgeModInput, manifestStore: Record<string, any> = {}): Promise<ManifestMetadata | undefined> {\n const fs = await resolveFileSystem(mod)\n if (!await fs.existsFile('META-INF/MANIFEST.MF')) { return undefined }\n const data = await fs.readFile('META-INF/MANIFEST.MF')\n const manifest: Record<string, string> = data.toString().split('\\n')\n .map((l) => l.trim())\n .filter((l) => l.length > 0)\n .map((l) => l.split(':').map((s) => s.trim()))\n .reduce((a, b) => ({ ...a, [b[0]]: b[1] }), {}) as any\n Object.assign(manifestStore, manifest)\n const metadata: ManifestMetadata = {\n modid: '',\n name: '',\n authors: [],\n version: '',\n description: '',\n url: '',\n }\n if (typeof manifest.TweakName === 'string') {\n metadata.modid = manifest.TweakName\n metadata.name = manifest.TweakName\n }\n if (typeof manifest.TweakAuthor === 'string') {\n metadata.authors = [manifest.TweakAuthor]\n }\n if (typeof manifest.TweakVersion === 'string') {\n metadata.version = manifest.TweakVersion\n }\n if (manifest.TweakMetaFile) {\n const file = manifest.TweakMetaFile\n if (await fs.existsFile(`META-INF/${file}`)) {\n const metadataContent = await fs.readFile(`META-INF/${file}`, 'utf-8').then((s) => s.replace(/^\\uFEFF/, '')).then(JSON.parse)\n if (metadataContent.id) {\n metadata.modid = metadataContent.id\n }\n if (metadataContent.name) {\n metadata.name = metadataContent.name\n }\n if (metadataContent.version) {\n metadata.version = metadataContent.version\n }\n if (metadataContent.authors) {\n metadata.authors = metadataContent.authors\n }\n if (metadataContent.description) {\n metadata.description = metadataContent.description\n }\n if (metadataContent.url) {\n metadata.url = metadataContent.url\n }\n }\n }\n return metadata\n}\n\n/**\n * Read mod metadata from new toml metadata file.\n */\nexport async function readForgeModToml(mod: ForgeModInput, manifest?: Record<string, string>, fileName = 'mods.toml') {\n const fs = await resolveFileSystem(mod)\n const existed = await fs.existsFile('META-INF/' + fileName)\n const all: ForgeModTOMLData[] = []\n if (existed) {\n const str = await fs.readFile('META-INF/' + fileName, 'utf-8')\n const root = parseToml(str)\n if (root.mods instanceof Array) {\n for (const mod of root.mods) {\n const tomlMod = mod\n if (typeof tomlMod === 'object' && !(tomlMod instanceof TomlDate) && !(tomlMod instanceof Array)) {\n const modObject: ForgeModTOMLData = {\n modid: tomlMod.modId as string ?? '',\n authors: tomlMod.authors as string ?? root.authors as string ?? '',\n // eslint-disable-next-line no-template-curly-in-string\n version: tomlMod.version === '${file.jarVersion}' && typeof manifest?.['Implementation-Version'] === 'string'\n ? manifest?.['Implementation-Version']\n : tomlMod.version as string,\n displayName: tomlMod.displayName as string ?? '',\n description: tomlMod.description as string ?? '',\n displayURL: tomlMod.displayURL as string ?? root.displayURL as string ?? '',\n updateJSONURL: tomlMod.updateJSONURL as string ?? root.updateJSONURL ?? '',\n provides: tomlMod.provides as string[] ?? [],\n dependencies: [],\n logoFile: tomlMod.logoFile as string ?? '',\n credits: tomlMod.credits as string ?? '',\n loaderVersion: root.loaderVersion as string ?? '',\n modLoader: root.modLoader as string ?? '',\n issueTrackerURL: root.issueTrackerURL as string ?? '',\n }\n all.push(modObject)\n }\n }\n }\n if (typeof root.dependencies === 'object') {\n for (const mod of all) {\n const dep = (root.dependencies as Record<string, any>)[mod.modid]\n if (dep) { mod.dependencies = dep }\n }\n }\n }\n return all\n}\n\n/**\n * Use asm to scan all the class files of the mod. This might take long time to read.\n */\nexport async function readForgeModAsm(mod: ForgeModInput, manifest: Record<string, string> = {}): Promise<ForgeModASMData> {\n const fs = await resolveFileSystem(mod)\n let corePluginClass: string | undefined\n if (manifest) {\n if (typeof manifest.FMLCorePlugin === 'string') {\n const clazz = manifest.FMLCorePlugin.replace(/\\./g, '/')\n if (await fs.existsFile(clazz) || await fs.existsFile(`/${clazz}`) || await fs.existsFile(`/${clazz}.class`) || await fs.existsFile(clazz + '.class')) {\n corePluginClass = clazz\n }\n }\n }\n const result: ForgeModASMData = {\n usedForgePackage: false,\n usedLegacyFMLPackage: false,\n usedMinecraftClientPackage: false,\n usedMinecraftPackage: false,\n modAnnotations: [],\n }\n const guessing: Partial<ForgeModAnnotationData> = {}\n await fs.walkFiles('/', async (f) => {\n if (!f.endsWith('.class')) { return }\n const data = await fs.readFile(f)\n const visitor = new ModClassVisitor(result, guessing, corePluginClass)\n\n new ClassReader(data).accept(visitor)\n })\n if (result.modAnnotations.length === 0 && guessing.modid && (result.usedForgePackage || result.usedLegacyFMLPackage)) {\n result.modAnnotations.push({\n modid: guessing.modid ?? '',\n name: guessing.name ?? '',\n version: guessing.version ?? '',\n dependencies: guessing.dependencies ?? '',\n useMetadata: guessing.useMetadata ?? false,\n clientSideOnly: guessing.clientSideOnly ?? false,\n serverSideOnly: guessing.serverSideOnly ?? false,\n acceptedMinecraftVersions: guessing.acceptedMinecraftVersions ?? '',\n acceptableRemoteVersions: guessing.acceptableRemoteVersions ?? '',\n acceptableSaveVersions: guessing.acceptableSaveVersions ?? '',\n modLanguage: guessing.modLanguage ?? 'java',\n modLanguageAdapter: guessing.modLanguageAdapter ?? '',\n value: guessing.value ?? '',\n })\n }\n return result\n}\n/**\n * Read `mcmod.info`, `cccmod.info`, and `neimod.info` json file\n * @param mod The mod path or buffer or opened file system.\n */\nexport async function readForgeModJson(mod: ForgeModInput): Promise<ForgeModMcmodInfo[]> {\n const fs = await resolveFileSystem(mod)\n const all = [] as ForgeModMcmodInfo[]\n function normalize(json: Partial<ForgeModMcmodInfo>) {\n const metadata: ForgeModMcmodInfo = {\n modid: '',\n name: '',\n description: '',\n version: '',\n mcversion: '',\n url: '',\n updateUrl: '',\n updateJSON: '',\n authorList: [],\n credits: '',\n logoFile: '',\n screenshots: [],\n parent: '',\n useDependencyInformation: false,\n requiredMods: [],\n dependencies: [],\n dependants: [],\n }\n metadata.modid = json.modid ?? metadata.modid\n metadata.name = json.name ?? metadata.name\n metadata.description = json.description ?? metadata.description\n metadata.version = json.version ?? metadata.version\n metadata.mcversion = json.mcversion ?? metadata.mcversion\n metadata.url = json.url ?? metadata.url\n metadata.updateUrl = json.updateUrl ?? metadata.updateUrl\n metadata.updateJSON = json.updateJSON ?? metadata.updateJSON\n metadata.authorList = json.authorList ?? metadata.authorList\n metadata.credits = json.credits ?? metadata.credits\n metadata.logoFile = json.logoFile ?? metadata.logoFile\n metadata.screenshots = json.screenshots ?? metadata.screenshots\n metadata.parent = json.parent ?? metadata.parent\n metadata.useDependencyInformation = json.useDependencyInformation ?? metadata.useDependencyInformation\n metadata.requiredMods = json.requiredMods ?? metadata.requiredMods\n metadata.dependencies = json.dependencies ?? metadata.dependencies\n metadata.dependants = json.dependants ?? metadata.dependants\n return metadata\n }\n function readJsonMetadata(json: any) {\n const modList: Array<Partial<ForgeModMcmodInfo>> = []\n if (json instanceof Array) {\n modList.push(...json)\n } else if (json.modList instanceof Array) {\n modList.push(...json.modList)\n } else if (json.modid) {\n modList.push(json)\n }\n all.push(...modList.map(normalize))\n }\n if (await fs.existsFile('mcmod.info')) {\n try {\n const json = JSON.parse((await fs.readFile('mcmod.info', 'utf-8')).replace(/^\\uFEFF/, ''))\n readJsonMetadata(json)\n } catch (e) { }\n } else if (await fs.existsFile('cccmod.info')) {\n try {\n const text = (await fs.readFile('cccmod.info', 'utf-8')).replace(/^\\uFEFF/, '').replace(/\\n\\n/g, '\\\\n').replace(/\\n/g, '')\n const json = JSON.parse(text)\n readJsonMetadata(json)\n } catch (e) { }\n } else if (await fs.existsFile('neimod.info')) {\n try {\n const text = (await fs.readFile('neimod.info', 'utf-8')).replace(/^\\uFEFF/, '').replace(/\\n\\n/g, '\\\\n').replace(/\\n/g, '')\n const json = JSON.parse(text)\n readJsonMetadata(json)\n } catch (e) { }\n } else {\n const files = await fs.listFiles('./')\n const infoFile = files.find((f) => f.endsWith('.info'))\n if (infoFile) {\n try {\n const text = (await fs.readFile(infoFile, 'utf-8')).replace(/^\\uFEFF/, '').replace(/\\n\\n/g, '\\\\n').replace(/\\n/g, '')\n const json = JSON.parse(text)\n readJsonMetadata(json)\n } catch (e) { }\n }\n }\n return all\n}\n\ntype ForgeModInput = Uint8Array | string | FileSystem\n\n/**\n * Represnet a full scan of a mod file data.\n */\nexport interface ForgeModMetadata extends ForgeModASMData {\n /**\n * The mcmod.info file metadata. If no mcmod.info file, it will be an empty array\n */\n mcmodInfo: ForgeModMcmodInfo[]\n /**\n * The java manifest file data. If no metadata, it will be an empty object\n */\n manifest: Record<string, any>\n /**\n * The mod info extract from manfiest. If no manifest, it will be undefined!\n */\n manifestMetadata?: ManifestMetadata\n /**\n * The toml mod metadata\n */\n modsToml: ForgeModTOMLData[]\n}\n\n/**\n * Read metadata of the input mod.\n *\n * This will scan the mcmod.info file, all class file for `@Mod` & coremod `DummyModContainer` class.\n * This will also scan the manifest file on `META-INF/MANIFEST.MF` for tweak mod.\n *\n * If the input is totally not a mod. It will throw {@link NonForgeModFileError}.\n *\n * @throws {@link NonForgeModFileError}\n * @param mod The mod path or data\n * @returns The mod metadata\n */\nexport async function readForgeMod(mod: ForgeModInput): Promise<ForgeModMetadata> {\n const fs = await resolveFileSystem(mod)\n try {\n const jsons = await readForgeModJson(fs)\n const manifest: Record<string, any> = {}\n const manifestMetadata = await readForgeModManifest(fs, manifest)\n const tomls = await readForgeModToml(fs, manifest)\n const base = await readForgeModAsm(fs, manifest).catch(() => ({\n usedLegacyFMLPackage: false,\n usedForgePackage: false,\n usedMinecraftPackage: false,\n usedMinecraftClientPackage: false,\n modAnnotations: [],\n }))\n\n if (jsons.length === 0 && (!manifestMetadata || !manifestMetadata.modid) && tomls.length === 0 && base.modAnnotations.length === 0) {\n throw new ForgeModParseFailedError(mod, base, manifest)\n }\n\n const result: ForgeModMetadata = {\n mcmodInfo: jsons,\n manifest,\n manifestMetadata: manifestMetadata?.modid ? manifestMetadata : undefined,\n modsToml: tomls,\n ...base,\n }\n return result\n } finally {\n if (mod !== fs) fs.close()\n }\n}\n\nexport class ForgeModParseFailedError extends Error {\n constructor(readonly mod: ForgeModInput, readonly asm: ForgeModASMData, readonly manifest: Record<string, any>) {\n super('Cannot find the mod metadata in the mod!')\n this.name = 'ForgeModParseFailedError'\n }\n}\n", "/*!\n * Copyright (c) Squirrel Chat et al., All rights reserved.\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are met:\n *\n * 1. Redistributions of source code must retain the above copyright notice, this\n * list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright notice,\n * this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. Neither the name of the copyright holder nor the names of its contributors\n * may be used to endorse or promote products derived from this software without\n * specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\nfunction getLineColFromPtr(string, ptr) {\n let lines = string.slice(0, ptr).split(/\\r\\n|\\n|\\r/g);\n return [lines.length, lines.pop().length + 1];\n}\nfunction makeCodeBlock(string, line, column) {\n let lines = string.split(/\\r\\n|\\n|\\r/g);\n let codeblock = '';\n let numberLen = (Math.log10(line + 1) | 0) + 1;\n for (let i = line - 1; i <= line + 1; i++) {\n let l = lines[i - 1];\n if (!l)\n continue;\n codeblock += i.toString().padEnd(numberLen, ' ');\n codeblock += ': ';\n codeblock += l;\n codeblock += '\\n';\n if (i === line) {\n codeblock += ' '.repeat(numberLen + column + 2);\n codeblock += '^\\n';\n }\n }\n return codeblock;\n}\nexport class TomlError extends Error {\n line;\n column;\n codeblock;\n constructor(message, options) {\n const [line, column] = getLineColFromPtr(options.toml, options.ptr);\n const codeblock = makeCodeBlock(options.toml, line, column);\n super(`Invalid TOML document: ${message}\\n\\n${codeblock}`, options);\n this.line = line;\n this.column = column;\n this.codeblock = codeblock;\n }\n}\n", "/*!\n * Copyright (c) Squirrel Chat et al., All rights reserved.\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are met:\n *\n * 1. Redistributions of source code must retain the above copyright notice, this\n * list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright notice,\n * this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. Neither the name of the copyright holder nor the names of its contributors\n * may be used to endorse or promote products derived from this software without\n * specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\nimport { TomlError } from './error.js';\nexport function indexOfNewline(str, start = 0, end = str.length) {\n let idx = str.indexOf('\\n', start);\n if (str[idx - 1] === '\\r')\n idx--;\n return idx <= end ? idx : -1;\n}\nexport function skipComment(str, ptr) {\n for (let i = ptr; i < str.length; i++) {\n let c = str[i];\n if (c === '\\n')\n return i;\n if (c === '\\r' && str[i + 1] === '\\n')\n return i + 1;\n if ((c < '\\x20' && c !== '\\t') || c === '\\x7f') {\n throw new TomlError('control characters are not allowed in comments', {\n toml: str,\n ptr: ptr,\n });\n }\n }\n return str.length;\n}\nexport function skipVoid(str, ptr, banNewLines, banComments) {\n let c;\n while ((c = str[ptr]) === ' ' || c === '\\t' || (!banNewLines && (c === '\\n' || c === '\\r' && str[ptr + 1] === '\\n')))\n ptr++;\n return banComments || c !== '#'\n ? ptr\n : skipVoid(str, skipComment(str, ptr), banNewLines);\n}\nexport function skipUntil(str, ptr, sep, end, banNewLines = false) {\n if (!end) {\n ptr = indexOfNewline(str, ptr);\n return ptr < 0 ? str.length : ptr;\n }\n for (let i = ptr; i < str.length; i++) {\n let c = str[i];\n if (c === '#') {\n i = indexOfNewline(str, i);\n }\n else if (c === sep) {\n return i + 1;\n }\n else if (c === end || (banNewLines && (c === '\\n' || (c === '\\r' && str[i + 1] === '\\n')))) {\n return i;\n }\n }\n throw new TomlError('cannot find end of structure', {\n toml: str,\n ptr: ptr\n });\n}\nexport function getStringEnd(str, seek) {\n let first = str[seek];\n let target = first === str[seek + 1] && str[seek + 1] === str[seek + 2]\n ? str.slice(seek, seek + 3)\n : first;\n seek += target.length - 1;\n do\n seek = str.indexOf(target, ++seek);\n while (seek > -1 && first !== \"'\" && str[seek - 1] === '\\\\' && (str[seek - 2] !== '\\\\' || str[seek - 3] === '\\\\'));\n if (seek > -1) {\n seek += target.length;\n if (target.length > 1) {\n if (str[seek] === first)\n seek++;\n if (str[seek] === first)\n seek++;\n }\n }\n return seek;\n}\n", "/*!\n * Copyright (c) Squirrel Chat et al., All rights reserved.\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are met:\n *\n * 1. Redistributions of source code must retain the above copyright notice, this\n * list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright notice,\n * this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. Neither the name of the copyright holder nor the names of its contributors\n * may be used to endorse or promote products derived from this software without\n * specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\nlet DATE_TIME_RE = /^(\\d{4}-\\d{2}-\\d{2})?[T ]?(?:(\\d{2}):\\d{2}:\\d{2}(?:\\.\\d+)?)?(Z|[-+]\\d{2}:\\d{2})?$/i;\nexport class TomlDate extends Date {\n #hasDate = false;\n #hasTime = false;\n #offset = null;\n constructor(date) {\n let hasDate = true;\n let hasTime = true;\n let offset = 'Z';\n if (typeof date === 'string') {\n let match = date.match(DATE_TIME_RE);\n if (match) {\n if (!match[1]) {\n hasDate = false;\n date = `0000-01-01T${date}`;\n }\n hasTime = !!match[2];\n // Make sure to use T instead of a space. Breaks in case of extreme values otherwise.\n hasTime && date[10] === ' ' && (date = date.replace(' ', 'T'));\n // Do not allow rollover hours.\n if (match[2] && +match[2] > 23) {\n date = '';\n }\n else {\n offset = match[3] || null;\n date = date.toUpperCase();\n if (!offset && hasTime)\n date += 'Z';\n }\n }\n else {\n date = '';\n }\n }\n super(date);\n if (!isNaN(this.getTime())) {\n this.#hasDate = hasDate;\n this.#hasTime = hasTime;\n this.#offset = offset;\n }\n }\n isDateTime() {\n return this.#hasDate && this.#hasTime;\n }\n isLocal() {\n return !this.#hasDate || !this.#hasTime || !this.#offset;\n }\n isDate() {\n return this.#hasDate && !this.#hasTime;\n }\n isTime() {\n return this.#hasTime && !this.#hasDate;\n }\n isValid() {\n return this.#hasDate || this.#hasTime;\n }\n toISOString() {\n let iso = super.toISOString();\n // Local Date\n if (this.isDate())\n return iso.slice(0, 10);\n // Local Time\n if (this.isTime())\n return iso.slice(11, 23);\n // Local DateTime\n if (this.#offset === null)\n return iso.slice(0, -1);\n // Offset DateTime\n if (this.#offset === 'Z')\n return iso;\n // This part is quite annoying: JS strips the original timezone from the ISO string representation\n // Instead of using a \"modified\" date and \"Z\", we restore the representation \"as authored\"\n let offset = (+(this.#offset.slice(1, 3)) * 60) + +(this.#offset.slice(4, 6));\n offset = this.#offset[0] === '-' ? offset : -offset;\n let offsetDate = new Date(this.getTime() - (offset * 60e3));\n return offsetDate.toISOString().slice(0, -1) + this.#offset;\n }\n static wrapAsOffsetDateTime(jsDate, offset = 'Z') {\n let date = new TomlDate(jsDate);\n date.#offset = offset;\n return date;\n }\n static wrapAsLocalDateTime(jsDate) {\n let date = new TomlDate(jsDate);\n date.#offset = null;\n return date;\n }\n static wrapAsLocalDate(jsDate) {\n let date = new TomlDate(jsDate);\n date.#hasTime = false;\n date.#offset = null;\n return date;\n }\n static wrapAsLocalTime(jsDate) {\n let date = new TomlDate(jsDate);\n date.#hasDate = false;\n date.#offset = null;\n return date;\n }\n}\n", "/*!\n * Copyright (c) Squirrel Chat et al., All rights reserved.\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are met:\n *\n * 1. Redistributions of source code must retain the above copyright notice, this\n * list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright notice,\n * this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. Neither the name of the copyright holder nor the names of its contributors\n * may be used to endorse or promote products derived from this software without\n * specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\nimport { skipVoid } from './util.js';\nimport { TomlDate } from './date.js';\nimport { TomlError } from './error.js';\nlet INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\\d(_?\\d)*))$/;\nlet FLOAT_REGEX = /^[+-]?\\d(_?\\d)*(\\.\\d(_?\\d)*)?([eE][+-]?\\d(_?\\d)*)?$/;\nlet LEADING_ZERO = /^[+-]?0[0-9_]/;\nlet ESCAPE_REGEX = /^[0-9a-f]{4,8}$/i;\nlet ESC_MAP = {\n b: '\\b',\n t: '\\t',\n n: '\\n',\n f: '\\f',\n r: '\\r',\n '\"': '\"',\n '\\\\': '\\\\',\n};\nexport function parseString(str, ptr = 0, endPtr = str.length) {\n let isLiteral = str[ptr] === '\\'';\n let isMultiline = str[ptr++] === str[ptr] && str[ptr] === str[ptr + 1];\n if (isMultiline) {\n endPtr -= 2;\n if (str[ptr += 2] === '\\r')\n ptr++;\n if (str[ptr] === '\\n')\n ptr++;\n }\n let tmp = 0;\n let isEscape;\n let parsed = '';\n let sliceStart = ptr;\n while (ptr < endPtr - 1) {\n let c = str[ptr++];\n if (c === '\\n' || (c === '\\r' && str[ptr] === '\\n')) {\n if (!isMultiline) {\n throw new TomlError('newlines are not allowed in strings', {\n toml: str,\n ptr: ptr - 1,\n });\n }\n }\n else if ((c < '\\x20' && c !== '\\t') || c === '\\x7f') {\n throw new TomlError('control characters are not allowed in strings', {\n toml: str,\n ptr: ptr - 1,\n });\n }\n if (isEscape) {\n isEscape = false;\n if (c === 'u' || c === 'U') {\n // Unicode escape\n let code = str.slice(ptr, (ptr += (c === 'u' ? 4 : 8)));\n if (!ESCAPE_REGEX.test(code)) {\n throw new TomlError('invalid unicode escape', {\n toml: str,\n ptr: tmp,\n });\n }\n try {\n parsed += String.fromCodePoint(parseInt(code, 16));\n }\n catch {\n throw new TomlError('invalid unicode escape', {\n toml: str,\n ptr: tmp,\n });\n }\n }\n else if (isMultiline && (c === '\\n' || c === ' ' || c === '\\t' || c === '\\r')) {\n // Multiline escape\n ptr = skipVoid(str, ptr - 1, true);\n if (str[ptr] !== '\\n' && str[ptr] !== '\\r') {\n throw new TomlError('invalid escape: only line-ending whitespace may be escaped', {\n toml: str,\n ptr: tmp,\n });\n }\n ptr = skipVoid(str, ptr);\n }\n else if (c in ESC_MAP) {\n // Classic escape\n parsed += ESC_MAP[c];\n }\n else {\n throw new TomlError('unrecognized escape sequence', {\n toml: str,\n ptr: tmp,\n });\n }\n sliceStart = ptr;\n }\n else if (!isLiteral && c === '\\\\') {\n tmp = ptr - 1;\n isEscape = true;\n parsed += str.slice(sliceStart, tmp);\n }\n }\n return parsed + str.slice(sliceStart, endPtr - 1);\n}\nexport function parseValue(value, toml, ptr, integersAsBigInt) {\n // Constant values\n if (value === 'true')\n return true;\n if (value === 'false')\n return false;\n if (value === '-inf')\n return -Infinity;\n if (value === 'inf' || value === '+inf')\n return Infinity;\n if (value === 'nan' || value === '+nan' || value === '-nan')\n return NaN;\n // Avoid FP representation of -0\n if (value === '-0')\n return integersAsBigInt ? 0n : 0;\n // Numbers\n let isInt = INT_REGEX.test(value);\n if (isInt || FLOAT_REGEX.test(value)) {\n if (LEADING_ZERO.test(value)) {\n throw new TomlError('leading zeroes are not allowed', {\n toml: toml,\n ptr: ptr,\n });\n }\n value = value.replace(/_/g, '');\n let numeric = +value;\n if (isNaN(numeric)) {\n throw new TomlError('invalid number', {\n toml: toml,\n ptr: ptr,\n });\n }\n if (isInt) {\n if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) {\n throw new TomlError('integer value cannot be represented losslessly', {\n toml: toml,\n ptr: ptr,\n });\n }\n if (isInt || integersAsBigInt === true)\n numeric = BigInt(value);\n }\n return numeric;\n }\n const date = new TomlDate(value);\n if (!date.isValid()) {\n throw new TomlError('invalid value', {\n toml: toml,\n ptr: ptr,\n });\n }\n return date;\n}\n", "/*!\n * Copyright (c) Squirrel Chat et al., All rights reserved.\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are met:\n *\n * 1. Redistributions of source code must retain the above copyright notice, this\n * list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright notice,\n * this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. Neither the name of the copyright holder nor the names of its contributors\n * may be used to endorse or promote products derived from this software without\n * specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\nimport { parseString, parseValue } from './primitive.js';\nimport { parseArray, parseInlineTable } from './struct.js';\nimport { indexOfNewline, skipVoid, skipUntil, skipComment, getStringEnd } from './util.js';\nimport { TomlError } from './error.js';\nfunction sliceAndTrimEndOf(str, startPtr, endPtr, allowNewLines) {\n let value = str.slice(startPtr, endPtr);\n let commentIdx = value.indexOf('#');\n if (commentIdx > -1) {\n // The call to skipComment allows to \"validate\" the comment\n // (absence of control characters)\n skipComment(str, commentIdx);\n value = value.slice(0, commentIdx);\n }\n let trimmed = value.trimEnd();\n if (!allowNewLines) {\n let newlineIdx = value.indexOf('\\n', trimmed.length);\n if (newlineIdx > -1) {\n throw new TomlError('newlines are not allowed in inline tables', {\n toml: str,\n ptr: startPtr + newlineIdx\n });\n }\n }\n return [trimmed, commentIdx];\n}\nexport function extractValue(str, ptr, end, depth, integersAsBigInt) {\n if (depth === 0) {\n throw new TomlError('document contains excessively nested structures. aborting.', {\n toml: str,\n ptr: ptr\n });\n }\n let c = str[ptr];\n if (c === '[' || c === '{') {\n let [value, endPtr] = c === '['\n ? parseArray(str, ptr, depth, integersAsBigInt)\n : parseInlineTable(str, ptr, depth, integersAsBigInt);\n let newPtr = end ? skipUntil(str, endPtr, ',', end) : endPtr;\n if (endPtr - newPtr && end === '}') {\n let nextNewLine = indexOfNewline(str, endPtr, newPtr);\n if (nextNewLine > -1) {\n throw new TomlError('newlines are not allowed in inline tables', {\n toml: str,\n ptr: nextNewLine\n });\n }\n }\n return [value, newPtr];\n }\n let endPtr;\n if (c === '\"' || c === \"'\") {\n endPtr = getStringEnd(str, ptr);\n let parsed = parseString(str, ptr, endPtr);\n if (end) {\n endPtr = skipVoid(str, endPtr, end !== ']');\n if (str[endPtr] && str[endPtr] !== ',' && str[endPtr] !== end && str[endPtr] !== '\\n' && str[endPtr] !== '\\r') {\n throw new TomlError('unexpected character encountered', {\n toml: str,\n ptr: endPtr,\n });\n }\n endPtr += (+(str[endPtr] === ','));\n }\n return [parsed, endPtr];\n }\n endPtr = skipUntil(str, ptr, ',', end);\n let slice = sliceAndTrimEndOf(str, ptr, endPtr - (+(str[endPtr - 1] === ',')), end === ']');\n if (!slice[0]) {\n throw new TomlError('in