UNPKG

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
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); } }