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
JavaScript
// 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);