UNPKG

fomod

Version:

A library for creating, parsing, editing, and validating XML-based Fomod installers, widely popularized in the Bethesda modding scene

1,131 lines (1,117 loc) 83.6 kB
// src/definitions/Enums.ts var TagName = /* @__PURE__ */ ((TagName2) => { TagName2["Fomod"] = "fomod"; TagName2["Config"] = "config"; TagName2["ModuleName"] = "moduleName"; TagName2["ModuleImage"] = "moduleImage"; TagName2["ModuleDependencies"] = "moduleDependencies"; TagName2["Dependencies"] = "dependencies"; TagName2["FileDependency"] = "fileDependency"; TagName2["FlagDependency"] = "flagDependency"; TagName2["GameDependency"] = "gameDependency"; TagName2["FOMMDependency"] = "fommDependency"; TagName2["FOSEDependency"] = "foseDependency"; TagName2["RequiredInstallFiles"] = "requiredInstallFiles"; TagName2["File"] = "file"; TagName2["Folder"] = "folder"; TagName2["InstallSteps"] = "installSteps"; TagName2["InstallStep"] = "installStep"; TagName2["Visible"] = "visible"; TagName2["OptionalFileGroups"] = "optionalFileGroups"; TagName2["Group"] = "group"; TagName2["Plugins"] = "plugins"; TagName2["Plugin"] = "plugin"; TagName2["Description"] = "description"; TagName2["Image"] = "image"; TagName2["ConditionFlags"] = "conditionFlags"; TagName2["Flag"] = "flag"; TagName2["Files"] = "files"; TagName2["TypeDescriptor"] = "typeDescriptor"; TagName2["Type"] = "type"; TagName2["DependencyType"] = "dependencyType"; TagName2["DefaultType"] = "defaultType"; TagName2["Patterns"] = "patterns"; TagName2["Pattern"] = "pattern"; TagName2["ConditionalFileInstalls"] = "conditionalFileInstalls"; return TagName2; })(TagName || {}); var AttributeName = /* @__PURE__ */ ((AttributeName2) => { AttributeName2["Color"] = "colour"; AttributeName2["Colour"] = "colour"; AttributeName2["Path"] = "path"; AttributeName2["Position"] = "position"; AttributeName2["Height"] = "height"; AttributeName2["ShowFade"] = "showFade"; AttributeName2["ShowImage"] = "showImage"; AttributeName2["Operator"] = "operator"; AttributeName2["Source"] = "source"; AttributeName2["Destination"] = "destination"; AttributeName2["Priority"] = "priority"; AttributeName2["AlwaysInstall"] = "alwaysInstall"; AttributeName2["InstallIfUsable"] = "installIfUsable"; AttributeName2["Order"] = "order"; AttributeName2["Name"] = "name"; AttributeName2["Type"] = "type"; AttributeName2["File"] = "file"; AttributeName2["State"] = "state"; AttributeName2["Flag"] = "flag"; AttributeName2["Value"] = "value"; AttributeName2["Version"] = "version"; return AttributeName2; })(AttributeName || {}); var GroupBehaviorType = /* @__PURE__ */ ((GroupBehaviorType2) => { GroupBehaviorType2["SelectAny"] = "SelectAny"; GroupBehaviorType2["SelectAtLeastOne"] = "SelectAtLeastOne"; GroupBehaviorType2["SelectAtMostOne"] = "SelectAtMostOne"; GroupBehaviorType2["SelectExactlyOne"] = "SelectExactlyOne"; GroupBehaviorType2["SelectAll"] = "SelectAll"; return GroupBehaviorType2; })(GroupBehaviorType || {}); var OptionType = /* @__PURE__ */ ((OptionType2) => { OptionType2["NotUsable"] = "NotUsable"; OptionType2["CouldBeUsable"] = "CouldBeUsable"; OptionType2["Optional"] = "Optional"; OptionType2["Recommended"] = "Recommended"; OptionType2["Required"] = "Required"; return OptionType2; })(OptionType || {}); var SortingOrder = /* @__PURE__ */ ((SortingOrder2) => { SortingOrder2["Ascending"] = "Ascending"; SortingOrder2["Descending"] = "Descending"; SortingOrder2["Explicit"] = "Explicit"; return SortingOrder2; })(SortingOrder || {}); var FileDependencyState = /* @__PURE__ */ ((FileDependencyState2) => { FileDependencyState2["Missing"] = "Missing"; FileDependencyState2["Inactive"] = "Inactive"; FileDependencyState2["Active"] = "Active"; return FileDependencyState2; })(FileDependencyState || {}); var DependencyGroupOperator = /* @__PURE__ */ ((DependencyGroupOperator2) => { DependencyGroupOperator2["Or"] = "Or"; DependencyGroupOperator2["And"] = "And"; return DependencyGroupOperator2; })(DependencyGroupOperator || {}); var ModuleNamePosition = /* @__PURE__ */ ((ModuleNamePosition2) => { ModuleNamePosition2["Left"] = "Left"; ModuleNamePosition2["Right"] = "Right"; ModuleNamePosition2["RightOfImage"] = "RightOfImage"; return ModuleNamePosition2; })(ModuleNamePosition || {}); var BooleanString = /* @__PURE__ */ ((BooleanString3) => { BooleanString3["true"] = "true"; BooleanString3["false"] = "false"; return BooleanString3; })(BooleanString || {}); // src/DomUtils.ts function getOrCreateElementByTagNameSafe(fromElement, tagName) { return getOrCreateElementByTagName(fromElement, tagName); } function getOrCreateElementByTagName(fromElement, tagName) { const existingElement = fromElement.getElementsByTagName(tagName)[0]; if (existingElement) return existingElement; const newElement = fromElement.ownerDocument.createElement(tagName); return fromElement.appendChild(newElement); } function ensureXmlDoctype(doc) { switch (doc.contentType) { case "application/xml": case "text/xml": case "application/xhtml+xml": case "image/svg+xml": break; default: throw new TypeError("Provided document is not an XML document"); } } var BlankModuleConfig = '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />'; var BlankInfoDoc = '<fomod xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />'; var XmlNamespaces = /* @__PURE__ */ ((XmlNamespaces2) => { XmlNamespaces2["XMLNS"] = "http://www.w3.org/2000/xmlns/"; XmlNamespaces2["XSI"] = "http://www.w3.org/2001/XMLSchema-instance"; return XmlNamespaces2; })(XmlNamespaces || {}); function attrToObject(attributes) { const arr = Array.from(attributes); const entries = arr.map((attr) => [attr.name, attr.value]); return Object.fromEntries(entries); } // src/definitions/lib/XmlRepresentation.ts var Verifiable = class { }; var ElementObjectMap = /* @__PURE__ */ new WeakMap(); var XmlRepresentation = class extends Verifiable { static tagName; /** A [weak map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) of documents to this representation's associated element within that document. * * Without this map, in-place editing would be significantly more difficult. */ documentMap = /* @__PURE__ */ new WeakMap(); /** Assigns an element for a document. * * @throws If the tag name of the element does not match the tag name of this class. * @throws If an element has already been assigned for this document * @throws If the element has already been assigned to a different instance of any Fomod class. */ assignElement(element) { const document = element.ownerDocument; ensureXmlDoctype(element.ownerDocument); if (element.tagName !== this.tagName && element.tagName !== this.tagName.toUpperCase()) throw new Error(`Cannot assign an element with tag name "${element.tagName}" to a class with tag name "${this.tagName}".`); if (this.documentMap.has(document)) throw new Error("Cannot assign an XmlRepresentation to an element from a given document more than once."); if (ElementObjectMap.has(element)) throw new Error("Cannot assign an element that has already been assigned to a different instance of a Fomod class."); this.documentMap.set(document, element); ElementObjectMap.set(element, this); } /** Returns the element assigned to the document. Will create a new one if it does not exist. */ getElementForDocument(document) { ensureXmlDoctype(document); const existing = this.documentMap.get(document); if (existing) return existing; const newElement = document.createElement(this.tagName); this.documentMap.set(document, newElement); return newElement; } static parse(element, config) { throw new Error("This static method must be implemented by the child class!"); } }; // src/definitions/module/dependencies/Dependency.ts var Dependency = class extends XmlRepresentation { }; // src/definitions/module/dependencies/DependenciesGroup.ts var DependenciesGroup = class _DependenciesGroup extends Dependency { constructor(tagName, operator = "And" /* And */, dependencies = /* @__PURE__ */ new Set()) { super(); this.tagName = tagName; this.operator = operator; this.dependencies = dependencies; } static tagName = ["dependencies" /* Dependencies */, "moduleDependencies" /* ModuleDependencies */, "visible" /* Visible */]; isValid() { return Object.values(DependencyGroupOperator).includes(this.operator) && Array.from(this.dependencies).every((d) => d.isValid()); } reasonForInvalidity(...tree) { tree.push(this); if (!Object.values(DependencyGroupOperator).includes(this.operator)) return { reason: "dependencies.operator.unknown" /* DependenciesUnknownOperator */, offendingValue: this.operator, tree }; for (const dependency of this.dependencies) { const reason = dependency.reasonForInvalidity(...tree); if (reason) return reason; } return null; } associateWithDocument(document) { this.dependencies.forEach((d) => d.associateWithDocument?.(document)); } asElement(document, config = {}, knownOptions = []) { const element = this.getElementForDocument(document); this.associateWithDocument(document); element.setAttribute("operator" /* Operator */, this.operator); for (const dependency of this.dependencies) element.appendChild(dependency.asElement(document, config, knownOptions)); return element; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const tagName = element.tagName; const operator = element.getAttribute("operator" /* Operator */) ?? "And" /* And */; const dependencies = Array.from(element.children).map((e) => Dependency.parse(e, config)).filter((d) => d !== null); const obj = new _DependenciesGroup(tagName, operator, new Set(dependencies)); obj.assignElement(element); return obj; } decommission(currentDocument) { this.dependencies.forEach((d) => d.decommission?.(currentDocument)); } }; // src/definitions/module/dependencies/FileDependency.ts var FileDependency = class _FileDependency extends Dependency { constructor(filePath = "", desiredState = "Active" /* Active */) { super(); this.filePath = filePath; this.desiredState = desiredState; } static tagName = "fileDependency" /* FileDependency */; tagName = "fileDependency" /* FileDependency */; isValid() { return Object.values(FileDependencyState).includes(this.desiredState); } reasonForInvalidity(...tree) { tree.push(this); if (!Object.values(FileDependencyState).includes(this.desiredState)) return { reason: "dependencies.file.state.unknown" /* DependencyFileInvalidState */, offendingValue: this.desiredState, tree }; return null; } associateWithDocument(document) { return; } asElement(document) { const element = this.getElementForDocument(document); this.associateWithDocument(document); element.setAttribute("file" /* File */, this.filePath); element.setAttribute("state" /* State */, this.desiredState); return element; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const filePath = element.getAttribute("file" /* File */) ?? ""; const desiredState = element.getAttribute("state" /* State */) ?? "Active" /* Active */; const obj = new _FileDependency(filePath, desiredState); obj.assignElement(element); return obj; } decommission; }; // src/definitions/module/dependencies/VersionDependency.ts var VersionDependency = class extends Dependency { constructor(desiredVersion = "") { super(); this.desiredVersion = desiredVersion; } isValid() { return true; } asElement(document) { const element = this.getElementForDocument(document); this.associateWithDocument(document); element.setAttribute("version" /* Version */, this.desiredVersion); return element; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && (existing instanceof ScriptExtenderVersionDependency || existing instanceof GameVersionDependency || existing instanceof ModManagerVersionDependency)) return existing; const desiredVersion = element.getAttribute("version" /* Version */) ?? ""; let obj = null; switch (element.tagName) { case ScriptExtenderVersionDependency.tagName: obj = new ScriptExtenderVersionDependency(desiredVersion); break; case GameVersionDependency.tagName: obj = new GameVersionDependency(desiredVersion); break; case ModManagerVersionDependency.tagName: obj = new ModManagerVersionDependency(desiredVersion); break; } obj?.assignElement(element); return obj; } decommission; reasonForInvalidity() { return null; } associateWithDocument(document) { return; } }; var GameVersionDependency = class _GameVersionDependency extends VersionDependency { static tagName = "gameDependency" /* GameDependency */; tagName = "gameDependency" /* GameDependency */; constructor(desiredVersion) { super(desiredVersion); } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const desiredVersion = element.getAttribute("version" /* Version */) ?? ""; const obj = new _GameVersionDependency(desiredVersion); obj.assignElement(element); return obj; } }; var ScriptExtenderVersionDependency = class _ScriptExtenderVersionDependency extends VersionDependency { static tagName = "foseDependency" /* FOSEDependency */; tagName = "foseDependency" /* FOSEDependency */; constructor(desiredVersion) { super(desiredVersion); } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const desiredVersion = element.getAttribute("version" /* Version */) ?? ""; const obj = new _ScriptExtenderVersionDependency(desiredVersion); obj.assignElement(element); return obj; } }; var ModManagerVersionDependency = class _ModManagerVersionDependency extends VersionDependency { static tagName = "fommDependency" /* FOMMDependency */; tagName = "fommDependency" /* FOMMDependency */; constructor(desiredVersion) { super(desiredVersion); } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const desiredVersion = element.getAttribute("version" /* Version */) ?? ""; const obj = new _ModManagerVersionDependency(desiredVersion); obj.assignElement(element); return obj; } }; // src/definitions/module/dependencies/FlagDependency.ts var FlagDependency = class _FlagDependency extends Dependency { static tagName = "flagDependency" /* FlagDependency */; tagName = "flagDependency" /* FlagDependency */; flagInstance; get flagKey() { return this.flagInstance.name; } set flagKey(value) { this.flagInstance.name = value; } get desiredValue() { return this.flagInstance.usedValue; } set desiredValue(value) { this.flagInstance.usedValue = value; } constructor(flagName = "", desiredValue = "") { super(); this.flagInstance = new FlagInstance(flagName, desiredValue, false); } isValid() { return true; } reasonForInvalidity() { return null; } associateWithDocument(document) { this.flagInstance.associateWithDocument(document); } decommission(currentDocument) { this.flagInstance.decommission(currentDocument); } asElement(document, config, knownOptions = []) { const element = this.getElementForDocument(document); this.associateWithDocument(document); if (typeof this.flagKey === "string") { if (typeof this.desiredValue !== "string") throw new Error("Flag dependency `name` value is a string but `value` is a boolean! Expected string.", { cause: this }); element.setAttribute("flag" /* Flag */, this.flagKey); element.setAttribute("value" /* Value */, this.desiredValue); } else { if (this.desiredValue !== true) throw new Error("Flag dependency `name` value is an Option but `value` is a string! Expected boolean.", { cause: this }); const setter = this.flagKey.getOptionFlagSetter(document, config, knownOptions); element.setAttribute("flag" /* Flag */, setter.name); element.setAttribute("value" /* Value */, setter.value); } return element; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const flagName = element.getAttribute("flag" /* Flag */) ?? ""; const desiredValue = element.getAttribute("value" /* Value */) ?? ""; const obj = new _FlagDependency(flagName, desiredValue); obj.assignElement(element); return obj; } }; // src/definitions/module/dependencies/index.ts function parseDependency(element, config = {}) { switch (element.tagName) { case DependenciesGroup.tagName[0]: case DependenciesGroup.tagName[1]: case DependenciesGroup.tagName[2]: return DependenciesGroup.parse(element, config); case FileDependency.tagName: return FileDependency.parse(element, config); case FlagDependency.tagName: return FlagDependency.parse(element, config); case GameVersionDependency.tagName: case ScriptExtenderVersionDependency.tagName: case ModManagerVersionDependency.tagName: return VersionDependency.parse(element, config); default: return null; } } Dependency.parse = parseDependency; // src/definitions/lib/InvalidityReporting.ts var InvalidityReason = /* @__PURE__ */ ((InvalidityReason2) => { InvalidityReason2["FomodModuleNameMetadataInvalidPosition"] = "fomod.module_name_metadata.position"; InvalidityReason2["FomodModuleNameMetadataColorHexNotHexNumber"] = "fomod.module_name_metadata.color_hex.not_hex_number"; InvalidityReason2["FomodModuleNameMetadataColorHexNotEvenNumberOfHexDigits"] = "fomod.module_name_metadata.color_hex.odd_digit_count"; InvalidityReason2["FomodModuleImageMetadataShowFadeNotBool"] = "fomod.module_image_metadata.show_fade.not_boolean"; InvalidityReason2["FomodModuleImageMetadataShowImageNotBool"] = "fomod.module_image_metadata.show_image.not_boolean"; InvalidityReason2["DependenciesUnknownOperator"] = "dependencies.operator.unknown"; InvalidityReason2["DependencyFileInvalidState"] = "dependencies.file.state.unknown"; InvalidityReason2["StepUnknownGroupSortingOrder"] = "step.sorting_order.unknown"; InvalidityReason2["GroupUnknownBehaviorType"] = "group.behavior_type.unknown"; InvalidityReason2["GroupUnknownOptionSortingOrder"] = "group.sorting_order.unknown"; InvalidityReason2["OptionTypeDescriptorUnknownOptionType"] = "option.type_descriptor.option_type.unknown"; InvalidityReason2["InstallPriorityNotInteger"] = "install.priority.not_integer"; InvalidityReason2["InstallAlwaysInstallNotBoolean"] = "install.always_install.not_boolean"; InvalidityReason2["InstallInstallIfUsableNotBoolean"] = "install.install_if_usable.not_boolean"; return InvalidityReason2; })(InvalidityReason || {}); // src/definitions/module/Install.ts var InstallInstancesByDocument = /* @__PURE__ */ new WeakMap(); var Install = class _Install extends XmlRepresentation { constructor(fileSource = "", fileDestination = null, priority = "0", document, installIfUsable = "false" /* false */, alwaysInstall = "false" /* false */) { super(); this.fileSource = fileSource; this.fileDestination = fileDestination; this.priority = priority; this.installIfUsable = installIfUsable; this.alwaysInstall = alwaysInstall; if (document) this.attachDocument(document); } static tagName = ["file" /* File */, "folder" /* Folder */]; tagName = "file" /* File */; // Very interchangeable; /** A list of documents this install is a part of */ documents = /* @__PURE__ */ new Set(); isValid() { try { BigInt(this.priority); } catch { return false; } return Object.values(BooleanString).includes(this.alwaysInstall) && Object.values(BooleanString).includes(this.installIfUsable); } reasonForInvalidity(...tree) { tree.push(this); try { BigInt(this.priority); } catch { return { reason: "install.priority.not_integer" /* InstallPriorityNotInteger */, offendingValue: this.priority, tree }; } if (!Object.values(BooleanString).includes(this.alwaysInstall)) return { reason: "install.always_install.not_boolean" /* InstallAlwaysInstallNotBoolean */, offendingValue: this.priority, tree }; if (!Object.values(BooleanString).includes(this.installIfUsable)) return { reason: "install.install_if_usable.not_boolean" /* InstallInstallIfUsableNotBoolean */, offendingValue: this.priority, tree }; return null; } /** Generates an XML element from this object. */ asElement(document, config = {}) { if (this.fileSource.endsWith("/") || this.fileSource.endsWith("\\")) { if (this.fileDestination && (!this.fileDestination.endsWith("/") && !this.fileDestination.endsWith("\\"))) throw new Error("Source is a folder but destination is not", { cause: this }); this.tagName = "folder" /* Folder */; } else if (this.fileDestination && (this.fileDestination.endsWith("/") || this.fileDestination.endsWith("\\"))) throw new Error("Destination is a folder but source is not", { cause: this }); else this.tagName = "file" /* File */; const element = this.getElementForDocument(document); this.associateWithDocument(document); element.setAttribute("source" /* Source */, this.fileSource); if (this.fileDestination) element.setAttribute("destination" /* Destination */, this.fileDestination); if (this.priority !== "0") element.setAttribute("priority" /* Priority */, this.priority); else element.removeAttribute("priority" /* Priority */); if (this.alwaysInstall !== "false" /* false */) element.setAttribute("alwaysInstall" /* AlwaysInstall */, this.alwaysInstall); else element.removeAttribute("alwaysInstall" /* AlwaysInstall */); if (this.installIfUsable !== "false" /* false */) element.setAttribute("installIfUsable" /* InstallIfUsable */, this.installIfUsable); else element.removeAttribute("installIfUsable" /* InstallIfUsable */); return element; } static parse(element, config = {}) { let source = element.getAttribute("source" /* Source */) ?? ""; let destination = element.getAttribute("destination" /* Destination */) ?? null; if (element.tagName === "folder" /* Folder */) { if (!source.endsWith("/")) source += "/"; if (destination && !destination.endsWith("/")) destination += "/"; } const install = new _Install(source, destination, element.getAttribute("priority" /* Priority */) ?? "0"); install.assignElement(element); install.alwaysInstall = element.getAttribute("alwaysInstall" /* AlwaysInstall */) ?? "false" /* false */; install.installIfUsable = element.getAttribute("installIfUsable" /* InstallIfUsable */) ?? "false" /* false */; return install; } // Overwrite to handle the interchangeable `file` and `folder` tags getElementForDocument(document) { ensureXmlDoctype(document); const existingElement = super.getElementForDocument(document); if (existingElement.tagName === this.tagName) return existingElement; this.documentMap.delete(document); const newElement = this.getElementForDocument(document); for (let i = 0 - 1; i < existingElement.attributes.length; i++) newElement.setAttributeNode(existingElement.attributes[i].cloneNode(true)); newElement.replaceChildren(...existingElement.children); existingElement.replaceWith(newElement); return newElement; } // Overwrite to handle the interchangeable `file` and `folder` tags assignElement(element) { ensureXmlDoctype(element.ownerDocument); if (element.tagName === "file" /* File */ || element.tagName === "folder" /* Folder */) this.tagName = element.tagName; super.assignElement(element); } /** Attaches this flag instance to a document */ attachDocument(document) { this.documents.add(document); let instancesForDoc = InstallInstancesByDocument.get(document); if (!instancesForDoc) { instancesForDoc = { all: /* @__PURE__ */ new Set(), bySource: /* @__PURE__ */ new Map(), byDestination: /* @__PURE__ */ new Map() }; InstallInstancesByDocument.set(document, instancesForDoc); } instancesForDoc.all.add(this); let instancesBySourceSet = instancesForDoc.bySource.get(this.fileSource); if (!instancesBySourceSet) { instancesBySourceSet = /* @__PURE__ */ new Set(); instancesForDoc.bySource.set(this.fileSource, instancesBySourceSet); } instancesBySourceSet.add(this); let instancesByDestinationSet = instancesForDoc.byDestination.get(this.fileDestination ?? this.fileSource); if (!instancesByDestinationSet) { instancesByDestinationSet = /* @__PURE__ */ new Set(); instancesForDoc.byDestination.set(this.fileDestination ?? this.fileSource, instancesByDestinationSet); } instancesByDestinationSet.add(this); } /** Removes this flag instance from a document */ removeFromDocument(document) { const instancesForDoc = InstallInstancesByDocument.get(document); if (!instancesForDoc) { this.documents.delete(document); return; } instancesForDoc.all.delete(this); instancesForDoc.bySource.get(this.fileSource)?.delete(this); instancesForDoc.byDestination.get(this.fileDestination ?? this.fileSource)?.delete(this); this.documents.delete(document); } associateWithDocument(document) { if (!this.documents.has(document)) this.attachDocument(document); } decommission(currentDocument) { if (currentDocument) this.removeFromDocument(currentDocument); else this.documents.forEach((document) => this.removeFromDocument(document)); } }; var InstallPatternFilesWrapper = class _InstallPatternFilesWrapper extends XmlRepresentation { constructor(installs = /* @__PURE__ */ new Set()) { super(); this.installs = installs; } static tagName = "files" /* Files */; tagName = "files" /* Files */; asElement(document, config = {}) { const el = this.getElementForDocument(document); this.associateWithDocument(document); for (const install of this.installs.values()) el.appendChild(install.asElement(document, config)); return el; } isValid() { for (const install of this.installs.values()) if (!install.isValid()) return false; return true; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const installs = /* @__PURE__ */ new Set(); for (const child of element.children) { const install = Install.parse(child, config); installs.add(install); } const obj = new _InstallPatternFilesWrapper(installs); obj.assignElement(element); return obj; } reasonForInvalidity(...tree) { tree.push(this); for (const install of this.installs.values()) { const reason = install.reasonForInvalidity(...tree); if (reason) return reason; } return null; } associateWithDocument(document) { this.installs.forEach((i) => i.associateWithDocument(document)); } decommission(currentDocument) { this.installs.forEach((install) => install.decommission(currentDocument)); } }; var InstallPattern = class _InstallPattern extends XmlRepresentation { constructor(dependencies = new DependenciesGroup("dependencies" /* Dependencies */), filesWrapper = new InstallPatternFilesWrapper()) { super(); this.dependencies = dependencies; this.filesWrapper = filesWrapper; } static tagName = "pattern" /* Pattern */; tagName = "pattern" /* Pattern */; asElement(document, config = {}, knownOptions = []) { const el = this.getElementForDocument(document); this.associateWithDocument(document); el.appendChild(this.dependencies.asElement(document, config, knownOptions)); el.appendChild(this.filesWrapper.asElement(document, config)); return el; } isValid() { return this.filesWrapper.isValid() && (this.dependencies ? this.dependencies.isValid() : true); } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const dependenciesElement = element.querySelector(`:scope > ${"dependencies" /* Dependencies */}`); const dependencies = dependenciesElement ? DependenciesGroup.parse(dependenciesElement) : void 0; const filesElement = element.querySelector(`:scope > ${"files" /* Files */}`); const filesWrapper = filesElement ? InstallPatternFilesWrapper.parse(filesElement, config) : void 0; const obj = new _InstallPattern(dependencies, filesWrapper); obj.assignElement(element); return obj; } reasonForInvalidity(...tree) { tree.push(this); for (const install of this.filesWrapper.installs.values()) { const reason = install.reasonForInvalidity(...tree); if (reason) return reason; } if (this.dependencies && !this.dependencies.isValid()) { const reason = this.dependencies.reasonForInvalidity(...tree); if (reason) return reason; } return null; } decommission(currentDocument) { this.filesWrapper.decommission(currentDocument); this.dependencies?.decommission(currentDocument); } associateWithDocument(document) { this.filesWrapper.installs.forEach((i) => i.associateWithDocument(document)); this.dependencies?.associateWithDocument(document); } }; // src/definitions/lib/FlagInstance.ts var FlagInstanceStoresByDocument = /* @__PURE__ */ new WeakMap(); var FlagInstance = class { constructor(_name, usedValue, write, document) { this._name = _name; this.usedValue = usedValue; this.write = write; if (typeof _name === "string" && typeof usedValue !== "string") throw new Error(`FlagInstance's 'usedValue' property must be a string when name is a string`); else if (_name instanceof Option && usedValue !== true) throw new Error(`FlagInstance's 'usedValue' property must be \`true\` when name is an Option`); this.usedValue = usedValue; if (document) this.attachDocument(document); } static isOptionFlagGetter(flag) { return flag.usedValue === true; } static isRegularFlagOrOptionFlagSetter(flag) { return flag.usedValue !== true; } get name() { return this._name; } set name(value) { if (this.name === value) { this._name = value; return; } this.documents.forEach((document) => { const instances = FlagInstanceStoresByDocument.get(document); if (!instances) return; instances.byName.get(this.name)?.delete(this); let instancesByName = instances.byName.get(value); if (!instancesByName) { instancesByName = /* @__PURE__ */ new Set(); instances.byName.set(value, instancesByName); } instancesByName.add(this); }); this._name = value; } /** A list of documents this flag is a part of */ documents = /* @__PURE__ */ new Set(); /** If this flag instance is the setter for an option flag (not the getter, which would make TIsOption = true), * this is a map of documents to the option instance that this flag represents. */ optionFlagOptionByDocument = /* @__PURE__ */ new WeakMap(); /** Attaches this flag instance to a document */ attachDocument(document) { this.documents.add(document); let instancesForDoc = FlagInstanceStoresByDocument.get(document); if (!instancesForDoc) { instancesForDoc = { all: /* @__PURE__ */ new Set(), byName: /* @__PURE__ */ new Map() }; FlagInstanceStoresByDocument.set(document, instancesForDoc); } instancesForDoc.all.add(this); let instancesByNameSet = instancesForDoc.byName.get(this.name); if (!instancesByNameSet) { instancesByNameSet = /* @__PURE__ */ new Set(); instancesForDoc.byName.set(this.name, instancesByNameSet); } instancesByNameSet.add(this); } /** Removes this flag instance from a document */ removeFromDocument(document) { const instancesForDoc = FlagInstanceStoresByDocument.get(document); if (!instancesForDoc) { this.documents.delete(document); return; } instancesForDoc.all.delete(this); instancesForDoc.byName.get(this.name)?.delete(this); this.documents.delete(document); } /** Cleans up the document map */ delete() { this.documents.forEach((document) => this.removeFromDocument(document)); } decommission(currentDocument) { currentDocument ? this.removeFromDocument(currentDocument) : this.delete(); } associateWithDocument(document) { if (!this.documents.has(document)) this.attachDocument(document); } }; // src/definitions/lib/FomodDocumentConfig.ts var DefaultFomodDocumentConfig = { includeInfoSchema: true, flattenConditionalInstalls: false, flattenConditionalInstallsNoDependencies: false, removeEmptyConditionalInstalls: true, optionSelectedValue: "OPTION_SELECTED", parseOptionFlags: true, generateNewOptionFlagNames: false }; // src/definitions/module/Option.ts var Option = class _Option extends XmlRepresentation { constructor(name = "", description = "", image = null, typeDescriptor = new TypeDescriptor(), flagsToSet = /* @__PURE__ */ new Set(), installsToSet = new InstallPattern()) { super(); this.name = name; this.description = description; this.image = image; this.typeDescriptor = typeDescriptor; this.flagsToSet = flagsToSet; this.installsToSet = installsToSet; } static tagName = "plugin" /* Plugin */; tagName = "plugin" /* Plugin */; asElement(document, config = {}, knownOptions = []) { const element = this.getElementForDocument(document); this.associateWithDocument(document); if (config.generateNewOptionFlagNames ?? DefaultFomodDocumentConfig.generateNewOptionFlagNames) { for (const option of knownOptions) { option.existingOptionFlagSetterByDocument.get(document)?.decommission(); option.existingOptionFlagSetterByDocument.delete(document); } config = Object.assign({}, config, { generateNewOptionFlagNames: false }); } element.setAttribute("name", this.name); const description = getOrCreateElementByTagNameSafe(element, "description" /* Description */); description.textContent = this.description; element.appendChild(description); if (this.image === null) element.getElementsByTagName("image" /* Image */)[0]?.remove(); else { const image = getOrCreateElementByTagNameSafe(element, "image" /* Image */); image.setAttribute("path" /* Path */, this.image); element.appendChild(image); } const selfFlag = this.getOptionFlagSetter(document, config, knownOptions); this.flagsToSet.add(selfFlag); if (this.flagsToSet.size > 0) { const flagsElement = getOrCreateElementByTagNameSafe(element, "conditionFlags" /* ConditionFlags */); for (const flag of this.flagsToSet) flagsElement.appendChild(flag.asElement(document, config)); element.appendChild(flagsElement); } else { element.getElementsByTagName("conditionFlags" /* ConditionFlags */)[0]?.remove(); } this.flagsToSet.delete(selfFlag); if (this.installsToSet.filesWrapper.installs.size > 0) { const filesElement = getOrCreateElementByTagNameSafe(element, "files" /* Files */); for (const install of this.installsToSet.filesWrapper.installs) filesElement.appendChild(install.asElement(document, config)); element.appendChild(filesElement); } else if (this.flagsToSet.size === 0) { const filesElement = getOrCreateElementByTagNameSafe(element, "files" /* Files */); filesElement.replaceChildren(); element.appendChild(filesElement); } else { element.getElementsByTagName("files" /* Files */)[0]?.remove(); } element.appendChild(this.typeDescriptor.asElement(document, config)); return element; } isValid() { return this.typeDescriptor.isValid(); } reasonForInvalidity(...tree) { tree.push(this); return this.typeDescriptor.reasonForInvalidity(tree, this); } existingOptionFlagSetterByDocument = /* @__PURE__ */ new WeakMap(); getOptionFlagSetter(document, config = {}, knownOptions = []) { const existing = this.existingOptionFlagSetterByDocument.get(document); if (existing) return existing; const flagInstances = FlagInstanceStoresByDocument.get(document) ?? { all: /* @__PURE__ */ new Set(), byName: /* @__PURE__ */ new Map() }; const baseName = "OPTION_" + this.name.replace(/[^a-zA-Z0-9]/g, "_"); let thisIndex = void 0; const existingFlagNames = new Set(Array.from(flagInstances.byName.keys()).map((nameOrOption) => { if (typeof nameOrOption === "string") return nameOrOption; thisIndex ??= knownOptions.indexOf(this); if (thisIndex === -1 || knownOptions.indexOf(nameOrOption) >= thisIndex) return void 0; nameOrOption.getOptionFlagSetter(document, config, knownOptions).name; })); const option = this; const tryNextName = function tryNextName2(suffix) { const name = baseName + `--${suffix}`; if (!existingFlagNames.has(name)) { const flagInstance = new FlagInstance(name, config.optionSelectedValue ?? DefaultFomodDocumentConfig.optionSelectedValue, true, document); flagInstance.optionFlagOptionByDocument.set(document, option); return new FlagSetter(flagInstance); } return tryNextName2(suffix + 1n); }; const setter = tryNextName(1n); this.existingOptionFlagSetterByDocument.set(document, setter); return setter; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const name = element.getAttribute("name" /* Name */); if (name === null) return null; const description = element.getElementsByTagName("description" /* Description */)[0]?.textContent ?? ""; const image = element.getElementsByTagName("image" /* Image */)[0]?.getAttribute("path" /* Path */) ?? null; const typeDescriptorElement = element.getElementsByTagName("typeDescriptor" /* TypeDescriptor */)[0]; const typeDescriptor = typeDescriptorElement ? TypeDescriptor.parse(typeDescriptorElement, config) : void 0; const option = new _Option(name, description, image, typeDescriptor); option.assignElement(element); const dependencyOnThis = new FlagDependency(option, true); const flagsToSet = /* @__PURE__ */ new Set(); const conditionFlagElements = element.getElementsByTagName("conditionFlags" /* ConditionFlags */)[0]; if (conditionFlagElements) { for (const flagElement of conditionFlagElements.getElementsByTagName("flag" /* Flag */)) { const flag = FlagSetter.parse(flagElement, config); if (flag) flagsToSet.add(flag); } } option.flagsToSet = flagsToSet; const installsToSet = new InstallPattern(); const filesElement = element.getElementsByTagName("files" /* Files */)[0]; if (filesElement) { for (const flagElement of filesElement.getElementsByTagName("file" /* File */)) { const install = Install.parse(flagElement, config); if (install) installsToSet.filesWrapper.installs.add(install); } for (const flagElement of filesElement.getElementsByTagName("folder" /* Folder */)) { const install = Install.parse(flagElement, config); if (install) installsToSet.filesWrapper.installs.add(install); } } installsToSet.dependencies?.dependencies.add(dependencyOnThis); option.installsToSet = installsToSet; return option; } decommission(currentDocument) { this.flagsToSet.forEach((flag) => flag.decommission?.(currentDocument)); this.installsToSet.decommission?.(currentDocument); this.typeDescriptor.decommission?.(currentDocument); } associateWithDocument(document) { this.flagsToSet.forEach((flag) => flag.associateWithDocument(document)); this.installsToSet.associateWithDocument(document); this.typeDescriptor.associateWithDocument(document); } }; var FlagSetter = class _FlagSetter extends XmlRepresentation { constructor(flagInstance) { super(); this.flagInstance = flagInstance; } static tagName = "flag" /* Flag */; tagName = "flag" /* Flag */; get name() { return this.flagInstance.name; } set name(name) { this.flagInstance.name = name; } get value() { return this.flagInstance.usedValue; } set value(value) { this.flagInstance.usedValue = value; } asElement(document, config = {}) { const element = this.getElementForDocument(document); this.associateWithDocument(document); element.setAttribute("name" /* Name */, this.flagInstance.name); element.textContent = this.flagInstance.usedValue; return element; } isValid() { return true; } reasonForInvalidity() { return null; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const flagName = element.getAttribute("name" /* Name */); if (flagName === null) return null; const obj = new _FlagSetter(new FlagInstance(flagName, element.textContent ?? "", true, element.ownerDocument)); obj.assignElement(element); return obj; } decommission(currentDocument) { this.flagInstance.decommission(currentDocument); } associateWithDocument(document) { this.flagInstance.associateWithDocument(document); } }; var TypeDescriptor = class _TypeDescriptor extends XmlRepresentation { constructor(defaultTypeNameDescriptor = new TypeNameDescriptor("defaultType" /* DefaultType */, "Optional" /* Optional */, false), patterns = []) { super(); this.defaultTypeNameDescriptor = defaultTypeNameDescriptor; this.patterns = patterns; } static tagName = "typeDescriptor" /* TypeDescriptor */; tagName = "typeDescriptor" /* TypeDescriptor */; asElement(document, config = {}) { const element = this.getElementForDocument(document); this.associateWithDocument(document); if (this.patterns.length === 0) { this.defaultTypeNameDescriptor.tagName = "type" /* Type */; element.appendChild(this.defaultTypeNameDescriptor.asElement(document, config)); return element; } this.defaultTypeNameDescriptor.tagName = "defaultType" /* DefaultType */; const patternsContainer = getOrCreateElementByTagNameSafe(element, "patterns" /* Patterns */); for (const pattern of this.patterns) patternsContainer.appendChild(pattern.asElement(document, config)); return element; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const typeDescriptor = new _TypeDescriptor(); typeDescriptor.assignElement(element); const dependencyTypeEl = element.querySelector(`:scope > ${"dependencyType" /* DependencyType */}`); if (!dependencyTypeEl) { const typeElement = element.querySelector(`:scope > ${"type" /* Type */}`); if (typeElement) typeDescriptor.defaultTypeNameDescriptor = TypeNameDescriptor.parse(typeElement, config); } else { const defaultTypeNameDescriptorElement = dependencyTypeEl.querySelector(`:scope > ${"defaultType" /* DefaultType */}`); if (defaultTypeNameDescriptorElement) typeDescriptor.defaultTypeNameDescriptor = TypeNameDescriptor.parse(defaultTypeNameDescriptorElement, config); for (const patternElement of dependencyTypeEl.querySelectorAll(`:scope > ${"pattern" /* Pattern */}`)) typeDescriptor.patterns.push(TypeDescriptorPattern.parse(patternElement, config)); } return typeDescriptor; } isValid() { return this.defaultTypeNameDescriptor.isValid() && this.patterns.every((p) => p.isValid()); } reasonForInvalidity(...tree) { tree.push(this); const defaultTypeNameDescriptorInvalidityReason = this.defaultTypeNameDescriptor.reasonForInvalidity(...tree); if (defaultTypeNameDescriptorInvalidityReason) return defaultTypeNameDescriptorInvalidityReason; const patternInvalidityReason = this.patterns.map((pattern) => pattern.reasonForInvalidity(...tree)).find((reason) => reason !== null); if (patternInvalidityReason) return patternInvalidityReason; return null; } decommission(currentDocument) { this.defaultTypeNameDescriptor.decommission?.(currentDocument); this.patterns.forEach((pattern) => pattern.decommission?.(currentDocument)); } associateWithDocument(document) { this.defaultTypeNameDescriptor.associateWithDocument(document); this.patterns.forEach((pattern) => pattern.associateWithDocument(document)); } }; var TypeDescriptorPattern = class _TypeDescriptorPattern extends XmlRepresentation { constructor(typeNameDescriptor = new TypeNameDescriptor("type" /* Type */, "Optional" /* Optional */, true), dependencies = new DependenciesGroup("dependencies" /* Dependencies */)) { super(); this.typeNameDescriptor = typeNameDescriptor; this.dependencies = dependencies; } static tagName = "pattern" /* Pattern */; tagName = "pattern" /* Pattern */; asElement(document, config = {}) { const element = this.getElementForDocument(document); this.associateWithDocument(document); element.appendChild(this.typeNameDescriptor.asElement(document, config)); element.appendChild(this.dependencies.asElement(document, config)); return element; } static parse(element, config = {}) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const typeDescriptorPattern = new _TypeDescriptorPattern(); typeDescriptorPattern.assignElement(element); const typeNameDescriptorElement = element.querySelector(`:scope > ${"type" /* Type */}`); if (typeNameDescriptorElement) typeDescriptorPattern.typeNameDescriptor = TypeNameDescriptor.parse(typeNameDescriptorElement, config, true); const dependenciesElement = element.querySelector(`:scope > ${"dependencies" /* Dependencies */}`); if (dependenciesElement) typeDescriptorPattern.dependencies = DependenciesGroup.parse(dependenciesElement); return typeDescriptorPattern; } isValid() { return this.typeNameDescriptor.isValid() && this.dependencies.isValid(); } reasonForInvalidity(...tree) { tree.push(this); const typeNameDescriptorInvalidityReason = this.typeNameDescriptor.reasonForInvalidity(...tree); if (typeNameDescriptorInvalidityReason) return typeNameDescriptorInvalidityReason; const dependenciesInvalidityReason = this.dependencies.reasonForInvalidity(...tree); if (dependenciesInvalidityReason) return dependenciesInvalidityReason; return null; } decommission(currentDocument) { this.typeNameDescriptor.decommission?.(currentDocument); this.dependencies.decommission?.(currentDocument); } associateWithDocument(document) { this.typeNameDescriptor.associateWithDocument(document); this.dependencies.associateWithDocument(document); } }; var TypeNameDescriptor = class _TypeNameDescriptor extends XmlRepresentation { constructor(_tagName, targetType = "Optional" /* Optional */, tagNameIsReadonly) { super(); this._tagName = _tagName; this.targetType = targetType; this.tagNameIsReadonly = tagNameIsReadonly; } static tagName = ["type" /* Type */, "defaultType" /* DefaultType */]; get tagName() { return this._tagName; } set tagName(tagName) { if (!this.tagNameIsReadonly) this._tagName = tagName; else throw new Error(`Attempted to set read-only property 'tagName' on a TypeNameDescriptor instance`); } asElement(document, config = {}) { const element = this.getElementForDocument(document); this.associateWithDocument(document); element.setAttribute("name" /* Name */, this.targetType); return element; } static parse(element, config = {}, tagNameIsReadonly) { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const typeDescriptorName = new _TypeNameDescriptor(element.tagName, "Optional", tagNameIsReadonly); typeDescriptorName.assignElement(element); typeDescriptorName.targetType = element.getAttribute("name" /* Name */) ?? "Optional" /* Optional */; return typeDescriptorName; } isValid() { return Object.values(OptionType).includes(this.targetType); } reasonForInvalidity(...tree) { tree.push(this); if (!Object.values(OptionType).includes(this.targetType)) return { offendingValue: this.targetType, tree, reason: "option.type_descriptor.option_type.unknown" /* OptionTypeDescriptorUnknownOptionType */ }; return null; } // Overwrite to handle the interchangeable `file` and `folder` tags getElementForDocument(document) { ensureXmlDoctype(document); const existingElement = super.getElementForDocument(document); if (existingElement.tagName === this.tagName) return existingElement; this.documentMap.delete(document); const newElement = this.getElementForDocument(document); for (let i = 0; i < existingElement.attributes.length; i++) newElement.setAttributeNode(existingElement.attributes[i].cloneNode(true)); newElement.replaceChildren(...existingElement.children);