UNPKG

fomod

Version:

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

124 lines (94 loc) 5.39 kB
import { getOrCreateElementByTagNameSafe } from "../../DomUtils"; import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting"; import { Option } from "./Option"; import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/XmlRepresentation"; import { AttributeName, GroupBehaviorType, SortingOrder, TagName } from "../Enums"; import { DefaultFomodDocumentConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; import { parseOptionFlags } from "../lib/ParseOptionFlags"; import { gatherFlagDependencies } from "../lib/utils"; import type { MaybeStrictString } from "../../TypeUtils"; export class Group<TStrict extends boolean> extends XmlRepresentation<TStrict> { static override readonly tagName = TagName.Group; readonly tagName = TagName.Group; constructor( public name: string = '', public behaviorType: MaybeStrictString<GroupBehaviorType, TStrict> = GroupBehaviorType.SelectExactlyOne, public sortingOrder: MaybeStrictString<SortingOrder, TStrict> = SortingOrder.Ascending, public options: Set<Option<TStrict>> = new Set(), ) { 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); element.setAttribute(AttributeName.Type, this.behaviorType); const optionsContainer = getOrCreateElementByTagNameSafe(element, TagName.Plugins); optionsContainer.setAttribute(AttributeName.Order, this.sortingOrder); for (const option of this.options) optionsContainer.appendChild(option.asElement(document, config)); return element; } isValid(): this is Group<true> { return Object.values(GroupBehaviorType).includes(this.behaviorType as any) && Object.values(SortingOrder).includes(this.sortingOrder as any) && Array.from(this.options).every(option => option.isValid() ); } reasonForInvalidity(...tree: Omit<Verifiable<false>, 'isValid' | 'reasonForInvalidity'>[]): InvalidityReport | null { tree.push(this); if (!Object.values(GroupBehaviorType).includes(this.behaviorType as any)) return { reason: InvalidityReason.GroupUnknownBehaviorType, offendingValue: this.behaviorType, tree }; if (!Object.values(SortingOrder).includes(this.sortingOrder as any)) return { reason: InvalidityReason.GroupUnknownOptionSortingOrder, offendingValue: this.sortingOrder, tree }; for (const option of this.options) { const reason = option.reasonForInvalidity(...tree); if (reason !== null) return reason; } return null; } gatherOptions(): Option<TStrict>[] { return Array.from(this.options); } static override parse(element: Element, config: FomodDocumentConfig = {}): Group<boolean> { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const name = element.getAttribute(AttributeName.Name); const behaviorType = element.getAttribute(AttributeName.Type); const group = new Group<false>(name ?? '', behaviorType ?? ''); group.assignElement(element); const optionsContainer = element.querySelector(`:scope > ${TagName.Plugins}`); if (optionsContainer === null) return group; const sortingOrder = optionsContainer.getAttribute(AttributeName.Order); if (sortingOrder !== null) group.sortingOrder = sortingOrder; let configForOptions = config; if (configForOptions.parseOptionFlags) configForOptions = Object.assign({}, configForOptions, {parseOptionFlags: false}); for (const optionElement of optionsContainer.querySelectorAll(`:scope > ${TagName.Plugin}`)) { const option = Option.parse(optionElement, configForOptions); if (option !== null) group.options.add(option); } if (config.parseOptionFlags ?? DefaultFomodDocumentConfig.parseOptionFlags) { const dependencies = []; const options = group.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 group; } decommission(currentDocument?: Document) { this.options.forEach(option => option.decommission(currentDocument)); } override associateWithDocument(document: Document) { this.options.forEach(option => option.associateWithDocument(document)); } }