fomod
Version:
A library for creating, parsing, editing, and validating XML-based Fomod installers, widely popularized in the Bethesda modding scene
84 lines (64 loc) • 4.06 kB
text/typescript
import { ensureXmlDoctype } from "../../DomUtils";
import { TagName } from "../Enums";
import { FomodDocumentConfig } from "./FomodDocumentConfig";
import { InvalidityReport } from "./InvalidityReporting";
/** The foundation of a class that can be validated against a schema.
*
* This model is important because it allows for the parsing of unsafe documents while providing maximum type safety for generated documents.
*/
export abstract class Verifiable<TStrict extends boolean> {
/** Checks if this item is compliant with the schema. Can be used both for type narrowing and input validation. */
abstract isValid(): this is Verifiable<true>;
/** Query the reason why the given item is invalid. Returns `null` if the object is valid.
*
* This method tends to be very, very, very slow. If you are only interested in whether or not the object is valid, please use `isValid()` instead.
*
* Depending on how often you expect the object to be invalid, it may be faster to use `isValid()` as a fail-fast check before calling this method.
*/
abstract reasonForInvalidity(...tree: Omit<Verifiable<false>, 'isValid' | 'reasonForInvalidity'>[]): InvalidityReport | null;
}
export const ElementObjectMap = new WeakMap<Element, XmlRepresentation<boolean>>();
/** A representation of an XML element for use with various Fomod classes. helps to facilitate in-place editing of Fomod documents.
*
*/
export abstract class XmlRepresentation<TStrict extends boolean> extends Verifiable<TStrict> {
static readonly tagName: TagName|TagName[];
abstract readonly tagName: 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: WeakMap<Document, Element> = 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: 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: Document): Element {
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;
}
/** Preforms non-serializing operations to associate this object with a document. */
abstract associateWithDocument(document: Document): unknown;
abstract decommission?(currentDocument?: Document): unknown;
/** Generates an XML element from this object. */
abstract asElement(document: Document, config?: FomodDocumentConfig): Element;
static parse(element: Element, config?: FomodDocumentConfig): XmlRepresentation<boolean> | null {
throw new Error('This static method must be implemented by the child class!');
}
}