fomod
Version:
A library for creating, parsing, editing, and validating XML-based Fomod installers, widely popularized in the Bethesda modding scene
136 lines (102 loc) • 5.97 kB
text/typescript
import { getOrCreateElementByTagNameSafe } from "../../DomUtils";
import { Group } from "./Group";
import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting";
import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/XmlRepresentation";
import { AttributeName, SortingOrder, TagName } from "../Enums";
import { DefaultFomodDocumentConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig";
import { Option } from "./Option";
import { parseOptionFlags } from "../lib/ParseOptionFlags";
import { gatherFlagDependencies } from "../lib/utils";
import { DependenciesGroup } from "./dependencies/DependenciesGroup";
import type { MaybeStrictString } from "../../TypeUtils";
export class Step<TStrict extends boolean> extends XmlRepresentation<TStrict> {
static override readonly tagName = TagName.InstallStep;
readonly tagName = TagName.InstallStep;
constructor(
public name: string = '',
public sortingOrder: MaybeStrictString<typeof SortingOrder[keyof typeof SortingOrder], TStrict> = SortingOrder.Ascending,
public groups: Set<Group<TStrict>> = new Set(),
public visibilityDeps: DependenciesGroup<TagName.Visible, TStrict> = new DependenciesGroup(TagName.Visible),
) {
super();
}
asElement(document: Document, config: FomodDocumentConfig = {}, knownOptions: Option<boolean>[] = this.gatherOptions()): Element {
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(AttributeName.Name, this.name);
const dependenciesContainer = this.visibilityDeps.getElementForDocument(document) || getOrCreateElementByTagNameSafe(element, TagName.Visible);
if (this.visibilityDeps.dependencies.size > 0) element.appendChild(this.visibilityDeps.asElement(document, config));
else dependenciesContainer.remove();
const groupsContainer = getOrCreateElementByTagNameSafe(element, TagName.OptionalFileGroups);
groupsContainer.setAttribute(AttributeName.Order, this.sortingOrder);
for (const group of this.groups) groupsContainer.appendChild(group.asElement(document, config));
element.appendChild(groupsContainer);
return element;
}
isValid(): this is Step<true> {
return (
Object.values(SortingOrder).includes(this.sortingOrder as any) &&
Array.from(this.groups).every(group => group.isValid())
);
}
reasonForInvalidity(...tree: Omit<Verifiable<false>, 'isValid' | 'reasonForInvalidity'>[]): InvalidityReport | null {
tree.push(this);
if (!Object.values(SortingOrder).includes(this.sortingOrder as any))
return { reason: InvalidityReason.StepUnknownGroupSortingOrder, offendingValue: this.sortingOrder, tree };
for (const group of this.groups) {
const reason = group.reasonForInvalidity(...tree, this);
if (reason !== null) return reason;
}
return null;
}
gatherOptions(): Option<TStrict>[] {
const options: Option<TStrict>[] = [];
for (const group of this.groups) options.push(...group.gatherOptions());
return options;
}
static override parse(element: Element, config: FomodDocumentConfig = {}): Step<boolean> {
const existing = ElementObjectMap.get(element);
if (existing && existing instanceof this) return existing;
const name = element.getAttribute(AttributeName.Name);
const step = new Step<false>(name ?? '');
step.assignElement(element);
const dependenciesContainer = element.querySelector(`:scope > ${TagName.Visible}`);
if (dependenciesContainer !== null) step.visibilityDeps = DependenciesGroup.parse<TagName.Visible>(dependenciesContainer, config);
const groupsContainer = element.querySelector(`:scope > ${TagName.OptionalFileGroups}`);
if (groupsContainer === null) return step;
const sortingOrder = groupsContainer.getAttribute(AttributeName.Order);
if (sortingOrder !== null) step.sortingOrder = sortingOrder;
let configForGroups = config;
if (configForGroups.parseOptionFlags) configForGroups = Object.assign({}, configForGroups, {parseOptionFlags: false});
for (const groupElement of groupsContainer.querySelectorAll(TagName.Group)) {
const group = Group.parse(groupElement, configForGroups);
if (group !== null) step.groups.add(group);
}
if (config.parseOptionFlags ?? DefaultFomodDocumentConfig.parseOptionFlags) {
const dependencies = Array.from(gatherFlagDependencies(step.visibilityDeps));
const options = step.gatherOptions();
for (const option of options) {
for (const pattern of option.typeDescriptor.patterns) {
dependencies.push(...gatherFlagDependencies(pattern.dependencies));
}
}
parseOptionFlags(options, element.ownerDocument!, config, dependencies);
}
return step;
}
override decommission(currentDocument?: Document | undefined) {
this.visibilityDeps.decommission(currentDocument);
for (const group of this.groups) group.decommission(currentDocument);
}
override associateWithDocument(document: Document) {
this.visibilityDeps.associateWithDocument(document);
for (const group of this.groups) group.associateWithDocument(document);
}
}