UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

834 lines 40.4 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { Widget } from '../widgets/Widget.js'; import { toKebabCase } from '../helpers/toKebabCase.js'; import { WHITESPACE_REGEX } from '../helpers/whitespace-regex.js'; const RESERVED_PARAMETER_MODES = ['value', 'text', 'widget']; const RESERVED_ELEMENT_NAMES = ['script', 'ui-tree']; const RESERVED_IMPORTS = ['context', 'window', 'globalThis']; /** * The base lazy-widgets XML namespace. All lazy-widgets namespaces will be * prefixed with this. lazy-widgets elements must use this namespace. * * @category XML */ export const XML_NAMESPACE_BASE = 'lazy-widgets'; /** * Makes sure a map-like value, such as a Record, is transformed to a Map. * * @param record - The map-like value to transform. If nothing is supplied, then an empty Map is created automatically * @returns Returns a new Map that is equivalent to the input, or the input if the input is already a Map * @internal */ function normalizeToMap(record = new Map()) { if (!(record instanceof Map)) { const orig = record; record = new Map(); for (const key of Object.getOwnPropertyNames(orig)) { record.set(key, orig[key]); } } return record; } /** * A bare-bones XML UI parser. This must not be used directly as this is an * extensible parser; you are supported to create a subclass of this and add all * the features/validators that you need. * * You won't need to create your own parser unless you have an XML format that * is not compatible with the default format. Most times it's enough to use * {@link XMLUIParser} and register new features if necessary. * * @category XML */ export class BaseXMLUIParser { constructor() { /** The DOMParser to actually parse the XML into nodes */ this.domParser = new DOMParser(); /** A map which assigns a factory function to an element name. */ this.factories = new Map(); /** * A map which assigns a validator function to a unique name, allowing a * validator to be referred to by string. Referred to as built-in * validators. */ this.validators = new Map(); /** * A map which assigns a single character string prefix to a string * deserializer. */ this.attributeValueDeserializers = new Map(); /** A map which assigns an attribute namespace to a handler function. */ this.attributeNamespaceHandlers = new Map(); /** A map which assigns an element name to a an XML element deserializer. */ this.elementDeserializers = new Map(); /** A map which defines custom parameter modes. */ this.parameterModes = new Map; /** A list of functions that modify a factory's parameter list. */ this.argumentModifiers = new Array; /** * A list of functions that are invoked after a widget is instanced, so that * the instance can be modified post-initialization. */ this.postInitHooks = new Array; } /** * Parse a value in an attribute. The value will be deserialized according * to its prefix. If there is no prefix, the value is treated as a string. * * @param rawValue - The value in the attribute, with the prefix included * @param context - The current parser context, which will be passed to a deserializer if the value is prefixed with a registered deserializer prefix */ parseAttributeValue(rawValue, context) { if (rawValue.length === 0) { return rawValue; } const deserializer = this.attributeValueDeserializers.get(rawValue[0]); if (deserializer) { // encoded value return deserializer(this, context, rawValue.slice(1)); } else { // just a string return rawValue; } } /** * Find the next unset parameter of a given mode. * * @param paramConfig - The input mapping of the widget being built * @param parametersSet - A list containing which of the parameters in the input mapping are already set * @param mode - The parameter mode to find * @returns Returns the index of the next unset parameter of the wanted mode. If none are found, -1 is returned. */ findNextParamOfType(paramConfig, parametersSet, mode) { const paramCount = paramConfig.length; let canBeList = false; if (RESERVED_PARAMETER_MODES.indexOf(mode) > 0) { canBeList = mode === 'widget'; } else { const parameterModeConfig = this.parameterModes.get(mode); if (parameterModeConfig === undefined) { throw new Error(`Invalid parameter mode "${mode}"`); } canBeList = parameterModeConfig[1]; } for (let i = 0; i < paramCount; i++) { const param = paramConfig[i]; if (param.mode === mode && (!parametersSet[i] || (canBeList && param.list))) { return i; } } return -1; } /** Create a new widget instance given a config and context */ instantiateWidget(inputConfig, paramNames, paramValidators, factory, factoryName, context, elem) { // parse parameters and options const paramCount = inputConfig.length; const instantiationContext = {}; const parameters = new Array(paramCount); const setParameters = new Array(paramCount).fill(false); const setViaName = new Array(paramCount).fill(false); for (const attribute of elem.attributes) { const namespace = attribute.namespaceURI; if (namespace === null || namespace === XML_NAMESPACE_BASE) { // this attribute sets a parameter's value const index = paramNames.get(attribute.localName); if (index === undefined) { throw new Error(`Can't set parameter "${attribute.localName}"; parameter does not exist in "${factoryName}" widget`); } if (setParameters[index]) { throw new Error(`Can't set parameter "${attribute.localName}"; parameter was already set in "${factoryName}" widget`); } setParameters[index] = true; setViaName[index] = true; const arg = this.parseAttributeValue(attribute.value, context); const paramConfig = inputConfig[index]; if (paramConfig.mode === 'value') { if (arg === undefined) { if (!paramConfig.optional) { throw new Error(`Required parameters (${paramConfig.name}) can't be undefined in "${factoryName}" widget`); } } else { const validator = paramValidators.get(index); if (validator === undefined) { parameters[index] = arg; } else { parameters[index] = validator(arg); } } } else if (paramConfig.mode === 'widget') { if (arg === undefined) { if (!paramConfig.optional) { throw new Error(`Required parameters (${paramConfig.name}) can't be undefined in "${factoryName}" widget`); } } else { const validator = paramConfig.validator; if (paramConfig.list) { if (!Array.isArray(arg)) { throw new Error(`Parameter "${paramConfig.name}" must be an array of Widgets in "${factoryName}" widget`); } if (validator) { const validArg = []; for (const widget of arg) { validArg.push(validator(widget)); } parameters[index] = validArg; } else { parameters[index] = arg; } } else { if (!(arg instanceof Widget)) { throw new Error(`Parameter "${paramConfig.name}" must be a Widget in "${factoryName}" widget`); } if (validator) { parameters[index] = validator(arg); } else { parameters[index] = arg; } } } } else if (paramConfig.mode === 'text') { if (arg === undefined) { throw new Error(`Text parameters (${paramConfig.name}) can't be undefined in "${factoryName}" widget`); } else { if (typeof arg !== 'string') { throw new Error(`Text parameters (${paramConfig.name}) must be strings in "${factoryName}" widget`); } parameters[index] = arg; } } else { const paramModeConfig = this.parameterModes.get(paramConfig.mode); if (!paramModeConfig) { throw new Error(`Unknown parameter mode "${paramConfig.mode}"; this is a bug, since there is an earlier check for this, please report it`); } const [validator, canBeList, canBeOptional] = paramModeConfig; if (arg === undefined) { if (!canBeOptional || !paramConfig.optional) { throw new Error(`Required parameters (${paramConfig.name}) can't be undefined in "${factoryName}" widget`); } } else { if (canBeList && paramConfig.list) { if (!Array.isArray(arg)) { throw new Error(`Parameter "${paramConfig.name}" must be an array in "${factoryName}" widget`); } if (validator) { const validArg = []; for (const value of arg) { validArg.push(validator(this, context, paramConfig, value)); } parameters[index] = validArg; } else { parameters[index] = arg; } } else if (validator) { parameters[index] = validator(this, context, paramConfig, arg); } else { parameters[index] = arg; } } } } else { const deserializer = this.attributeNamespaceHandlers.get(namespace); if (deserializer) { deserializer(this, context, instantiationContext, attribute); } } } // parse child elements (widgets, layers and text nodes) let textContent = ''; for (const childNode of elem.childNodes) { const nodeType = childNode.nodeType; if (nodeType === Node.DOCUMENT_NODE) { throw new Error(`Unexpected document node as child of "${factoryName}" widget in XML file`); } else if (nodeType === Node.DOCUMENT_TYPE_NODE) { throw new Error(`Unexpected document type node as child of "${factoryName}" widget in XML file`); } else if (nodeType === Node.DOCUMENT_FRAGMENT_NODE) { throw new Error(`Unexpected document fragment node as child of "${factoryName}" widget in XML file`); } else if (nodeType === Node.TEXT_NODE || nodeType === Node.CDATA_SECTION_NODE) { textContent += childNode.data; } else if (nodeType === Node.ELEMENT_NODE) { const childElem = childNode; if (childElem.namespaceURI !== XML_NAMESPACE_BASE) { continue; } const lowerNodeName = childElem.nodeName.toLowerCase(); const deserializerTuple = this.elementDeserializers.get(lowerNodeName); if (deserializerTuple) { const [deserializer, parameterMode] = deserializerTuple; const parameterModeTuple = this.parameterModes.get(parameterMode); if (!parameterModeTuple) { throw new Error(`Parameter mode "${parameterMode}" for element deserializer "${lowerNodeName}" is missing; this is a bug, please report it`); } const canBeList = parameterModeTuple[1]; const index = this.findNextParamOfType(inputConfig, setParameters, parameterMode); if (index < 0) { throw new Error(`Too many parameters passed as XML child elements; tried to find next unset parameter of mode "${parameterMode}" in "${factoryName}" widget, but none found`); } const value = deserializer(this, context, childElem); setParameters[index] = true; if (canBeList && inputConfig[index].list) { if (parameters[index] === undefined) { parameters[index] = [value]; } else { parameters[index].push(value); } } else { parameters[index] = value; } } else { // validator const index = this.findNextParamOfType(inputConfig, setParameters, 'widget'); if (index < 0) { throw new Error(`Too many widgets passed as XML child elements for "${factoryName}" widget`); } const childWidget = this.parseWidgetElem(context, childElem); setParameters[index] = true; if (inputConfig[index].list) { if (parameters[index] === undefined) { parameters[index] = [childWidget]; } else { parameters[index].push(childWidget); } } else { parameters[index] = childWidget; } } } } // add text parameter if needed const textIndex = this.findNextParamOfType(inputConfig, setParameters, 'text'); if (textIndex >= 0) { parameters[textIndex] = textContent; setParameters[textIndex] = true; } else if (!WHITESPACE_REGEX.test(textContent)) { throw new Error(`XML "${factoryName}" widget has a child text node, but there are no text parameters left to fill`); } // check if all required parameters are set for (let i = 0; i < paramCount; i++) { if (!setParameters[i]) { const param = inputConfig[i]; const mode = param.mode; if (RESERVED_PARAMETER_MODES.indexOf(mode) >= 0) { if (!param.optional) { throw new Error(`Parameter "${param.name}" with mode "${mode}" is not set in "${factoryName}" widget`); } } else { const modeConfig = this.parameterModes.get(mode); if (modeConfig === undefined) { throw new Error(`Unknown parameter mode "${mode}"; this is a bug, since there is an earlier check for this, please report it`); } if (!modeConfig[2] || !param.optional) { throw new Error(`Required parameter "${param.name}" not set in "${factoryName}" widget`); } } } } // modify parameters for (const modifier of this.argumentModifiers) { modifier(this, context, instantiationContext, parameters); } // instantiate widget const instance = factory(...parameters); // map widget back to ID if (instance.id !== null) { if (context.idMap.has(instance.id)) { throw new Error(`Widget ID "${instance.id}" is already taken in "${factoryName}" widget`); } context.idMap.set(instance.id, instance); } // post-init hooks for (const hook of this.postInitHooks) { hook(this, context, instantiationContext, instance); } return instance; } /** * Register a widget factory to an element name, with a given input mapping. * * @param nameOrWidgetClass - The camelCase or PascalCase name of the widget, which will be converted to kebab-case and be used as the element name for the widget. If a widget class is passed, then the class name will be used and converted to kebab-case. * @param inputMapping - The input mapping for the widget factory * @param factory - A function which creates a new instance of a widget */ registerFactory(nameOrWidgetClass, inputMapping, factory) { // handle constructors as names let factoryName = nameOrWidgetClass; if (typeof factoryName !== 'string') { factoryName = factoryName.name; } // make sure name is in kebab-case; element names are case-insensitive, // but just toLowerCase'ing it makes the tag names unreadable if the // string originally in camelCase or PascalCase factoryName = toKebabCase(factoryName); // make sure the name is not reserved/taken if (RESERVED_ELEMENT_NAMES.indexOf(factoryName) >= 0) { throw new Error(`The factory name "${factoryName}" is reserved`); } if (this.factories.has(factoryName)) { throw new Error(`The factory name "${factoryName}" is already taken by another factory`); } if (this.elementDeserializers.has(factoryName)) { throw new Error(`The factory name "${factoryName}" is already taken by an element deserializer`); } // validate parameter config and build parameter name map let hasTextNodeParam = false; const traps = new Set(); const paramValidators = new Map(); const paramNames = new Map(); const paramCount = inputMapping.length; for (let i = 0; i < paramCount; i++) { const paramGeneric = inputMapping[i]; if (paramGeneric.mode === 'value') { const param = paramGeneric; if (param.validator !== undefined) { let validators; // split validators into validation functions and strings if (typeof param.validator === 'string') { validators = param.validator.split(':'); } else if (typeof param.validator === 'function') { validators = [param.validator]; } else if (Array.isArray(param.validator)) { validators = []; for (const subValidator of param.validator) { if (typeof subValidator === 'string') { validators.push(...subValidator.split(':')); } else if (typeof subValidator === 'function') { validators.push(subValidator); } else { throw new Error(`Invalid validator type: ${typeof subValidator}`); } } } else { throw new Error(`Invalid validator type: ${typeof param.validator}`); } // convert built-in validators (strings) to functions const validatorCount = validators.length; if (validatorCount > 0) { for (let v = 0; v < validatorCount; v++) { const rawValidator = validators[v]; if (rawValidator === '') { throw new Error('Leading or trailing ":" in validator list'); } else if (typeof rawValidator === 'string') { const func = this.validators.get(rawValidator); if (func === undefined) { throw new Error(`Built-in validator "${rawValidator}" does not exist`); } validators[v] = func; } } // merge validators into a single validator paramValidators.set(i, (inputValue) => { let value = inputValue; for (let v = 0, stop = false; !stop && v < validatorCount; v++) { [value, stop] = validators[v](value); } return value; }); } } paramNames.set(param.name, i); } else if (paramGeneric.mode === 'text') { const param = paramGeneric; if (hasTextNodeParam) { throw new Error('Cannot add another "text" mode parameter; there can only be one text parameter. If you have more string parameters, add them as "value" mode parameters with a "string" validator, and only keep the most important string parameter as the "text" mode parameter'); } if (param.name !== undefined) { paramNames.set(param.name, i); } hasTextNodeParam = true; } else if (paramGeneric.mode === 'widget') { const param = paramGeneric; if (traps.has('widget')) { throw new Error('Cannot add another "widget" mode parameter; there is already a previous widget parameter that is optional or a list'); } if (param.list || param.optional) { traps.add('widget'); } if (param.name !== undefined) { paramNames.set(param.name, i); } } else { const param = paramGeneric; const paramMode = param.mode; const paramConfig = this.parameterModes.get(paramMode); if (paramConfig === undefined) { throw new Error(`Unknown parameter mode "${paramMode}"`); } const canBeList = paramConfig[1]; const canBeOptional = paramConfig[2]; if (traps.has(paramMode)) { let msgEnd; if (canBeList && canBeOptional) { msgEnd = 'optional or a list'; } else if (canBeList) { msgEnd = 'a list'; } else { msgEnd = 'optional'; } throw new Error(`Cannot add another "${paramMode}" parameter mode; there is already a previous parameter with the same mode that is ${msgEnd}`); } if ((canBeList && param.list) || (canBeOptional && param.optional)) { traps.add(paramMode); } paramNames.set(param.name, i); } } // register factory this.factories.set(factoryName, (context, elem) => this.instantiateWidget(inputMapping, paramNames, paramValidators, factory, factoryName, context, elem)); } /** * Register a built-in validator; assigns a string to a validator function, * so that the validator function can be referred to via a string instead of * via a function. * * @param key - The validator key - the string that will be used instead of the function * @param validator - A function which can throw an error on an invalid value, and transform an input value. Can be chained */ registerValidator(key, validator) { if (this.validators.has(key)) { throw new Error(`Built-in validator key "${key}" already taken`); } this.validators.set(key, validator); } /** * Register a attribute value deserializer; assigns a deserializer function * to a single character prefix. The value will be passed to the * deserializer without the prefix. * * @param prefix - A single character prefix that decides which deserializer to use. Must be unique * @param deserializer - A function that transforms a string without the prefix into any value */ registerAttributeValueDeserializer(prefix, deserializer) { if (prefix.length !== 1) { throw new Error('Attribute deserializer prefix must be a single character'); } if (this.attributeValueDeserializers.has(prefix)) { throw new Error(`Attribute deserializer prefix "${prefix}" already taken`); } this.attributeValueDeserializers.set(prefix, deserializer); } /** * Register a attribute namespace handler; assigns a handler function to a * unique namespace. The attribute object will be passed to the handler * function. * * @param namespace - A unique namespace. When this namespace is found in an attribute, instead of ignoring the attribute, the attribute will be passed to the handler * @param handler - A function that provides custom functionality when an attribute with the wanted namespace is used */ registerAttributeNamespaceHandler(namespace, handler) { if (namespace.length === 0) { throw new Error('Namespace must not be empty'); } if (namespace === XML_NAMESPACE_BASE) { throw new Error(`Namespace must not be the base namespace ("${XML_NAMESPACE_BASE}")`); } if (this.attributeNamespaceHandlers.has(namespace)) { throw new Error(`Attribute namespace "${namespace}" already taken`); } this.attributeNamespaceHandlers.set(namespace, handler); } /** * Register an element deserializer function; elements with the wanted name * will be treated as parameters serialized as an element instead of an * attribute. * * @param nodeName - A unique node name. Widgets will not be able to be registered with this name * @param parameterMode - The parameter mode to treat the value as * @param deserializer - The deserializer function that turns the XML element into a value */ registerElementDeserializer(nodeName, parameterMode, deserializer) { if (nodeName.length === 0) { throw new Error('Element deserializer node name must not be an empty string'); } if (this.elementDeserializers.has(nodeName)) { throw new Error(`Element deserializer node name "${nodeName}" already taken by another element deserializer`); } if (this.factories.has(nodeName)) { throw new Error(`Element deserializer node name "${nodeName}" already taken by a widget factory`); } this.elementDeserializers.set(nodeName, [deserializer, parameterMode]); } /** * Registers a parameter mode; defines whether the parameter mode can be a * list, can be optional and how it's validated. * * @param parameterMode - A string that is used as a key for this parameter mode * @param validator - A function that validates whether a deserialized value is valid for this mode * @param canBeList - If true, when a parameter with this mode has a config with a `list` field set to true, the parameter will be treated as a list * @param canBeOptional - If true, when a parameter with this mode has a config with a `optional` field set to true, the parameter will be optional (no error thrown if undefined) */ registerParameterMode(parameterMode, validator, canBeList, canBeOptional) { if (parameterMode.length === 0) { throw new Error('Parameter mode must not be an empty string'); } if (this.parameterModes.has(parameterMode)) { throw new Error(`Parameter mode "${parameterMode}" already taken`); } this.parameterModes.set(parameterMode, [validator, canBeList, canBeOptional]); } /** * Auto-register a factory for a given widget. Instead of passing an input * mapping and name, these are instead supplied in the * {@link Widget.autoXML} field of the widget class. If it's null, an error * is thrown. * * @param widgetClass - The class to auto-register */ autoRegisterFactory(widgetClass) { var _a; if (widgetClass.autoXML === null) { throw new Error('Widget class does not have an automatic XML factory config object set. Must be manually registered'); } const config = widgetClass.autoXML; const factory = (_a = config.factory) !== null && _a !== void 0 ? _a : ((...args) => new widgetClass(...args)); this.registerFactory(config.name, config.inputConfig, factory); } /** * Register an argument modifier. * * @param modifier - A function that modifies an argument list passed to a factory. The function will be added to the end of the modifier list */ registerArgumentModifier(modifier) { this.argumentModifiers.push(modifier); } /** * Register a post-initialization hook. * * @param hook - A function that will be called after a widget instance is created. The instance will be passed to the function, so it can be used to modify the instance post-initialization */ registerPostInitHook(hook) { this.postInitHooks.push(hook); } /** * Parse an XML element which is expected to represent a widget. If the XML * element doesn't represent a widget, then an error is thrown; this will * happen if no factory is registered to the element name. * * @param context - The current parser context, shared with all other initializations * @param elem - The element to parse * @returns Returns the new widget instance */ parseWidgetElem(context, elem) { // get factory for this element name const name = elem.nodeName.toLowerCase(); const factory = this.factories.get(name); if (factory === undefined) { throw new Error(`No factory registered to name (${name})`); } // generate widget return factory(context, elem); } /** * Parse a <ui-tree> element. Expected to contain at least one widget * element, and can contain <script> elements. Scripts must finish execution * or this will never return. * * @param uiTreeElem - The <ui-tree> element to parse * @param context - The current parser context, shared with all other initializations * @returns Returns the new widget instance. All scripts are finished executing when the widget is returned. */ parseUITreeElem(uiTreeElem, context) { // iterate children. there should only be one child element with the // wanted namespace that represents a widget. there can be many script // elements let topWidget = null; for (const child of uiTreeElem.childNodes) { const nodeType = child.nodeType; if (nodeType === Node.ELEMENT_NODE) { const childElem = child; if (childElem.namespaceURI !== XML_NAMESPACE_BASE) { continue; } // is this a widget or a script? if (childElem.localName === 'script') { // script, check if we have permission to run it if (context.scriptImports === null) { throw new Error('Scripts are disabled'); } // create script context const scriptContext = { variables: context.variableMap, ids: context.idMap }; // concatenate all text let text = ''; for (const grandChild of childElem.childNodes) { const gcNodeType = grandChild.nodeType; if (gcNodeType === Node.TEXT_NODE || gcNodeType === Node.CDATA_SECTION_NODE) { text += grandChild.data; } else { throw new Error('Unexpected XML non-text node inside script node'); } } // exec in the global scope, passing the script context and // defining all imports const params = ['context']; const args = [scriptContext]; for (const [key, value] of context.scriptImports) { params.push(key); args.push(value); } (new Function(...params, `"use strict"; ${String(text)}`))(...args); } else { // widget, parse it if (topWidget !== null) { throw new Error('XML UI tree can only have one top-most widget'); } topWidget = this.parseWidgetElem(context, childElem); } } else if (nodeType === Node.TEXT_NODE) { if (!WHITESPACE_REGEX.test(child.data)) { throw new Error('Unexpected text node as UI tree child'); } } else if (nodeType === Node.CDATA_SECTION_NODE) { throw new Error('Unexpected CDATA node as UI tree child'); } else if (nodeType === Node.DOCUMENT_NODE) { throw new Error('Unexpected document node as UI tree child'); } else if (nodeType === Node.DOCUMENT_FRAGMENT_NODE) { throw new Error('Unexpected document fragment node as UI tree child'); } } if (topWidget === null) { throw new Error('Expected a XML widget definition in the UI tree, none found'); } return topWidget; } /** * Parse an XML document which can contain multiple <ui-tree> descendants. * * @param xmlDoc - The XML document to parse * @param config - The configuration object to use for the parser * @returns Returns a pair containing, respectively, a Map which maps a UI tree name to a widget, and the parser context after all UI trees are parsed */ parseFromXMLDocument(xmlDoc, config) { // find all UI tree nodes const uiTrees = xmlDoc.getElementsByTagNameNS(XML_NAMESPACE_BASE, 'ui-tree'); if (uiTrees.length === 0) { throw new Error('No UI trees found in document'); } // setup context let scriptImports = null, variableMap; if (config) { if (config.allowScripts) { scriptImports = normalizeToMap(config.scriptImports); for (const name of scriptImports.keys()) { if (RESERVED_IMPORTS.indexOf(name) >= 0) { throw new Error(`The script import name "${name}" is reserved`); } } } variableMap = normalizeToMap(config.variables); } else { scriptImports = new Map(); variableMap = new Map(); } const context = { scriptImports, variableMap, idMap: new Map() }; // parse UI trees const trees = new Map(); for (const uiTree of uiTrees) { const nameAttr = uiTree.attributes.getNamedItemNS(null, 'name'); if (nameAttr === null) { throw new Error('UI trees must be named with a "name" attribute'); } const name = nameAttr.value; if (trees.has(name)) { throw new Error(`A UI tree with the name "${name}" already exists`); } const widget = this.parseUITreeElem(uiTree, context); trees.set(name, widget); } return [trees, context]; } /** * Parse an XML string. {@link BaseXMLUIParser#parseFromXMLDocument} will be * called. * * @param str - A string containing an XML document * @param config - The configuration object to use for the parser * @returns Returns a pair containing, respectively, a Map which maps a UI tree name to a widget, and the parser context after all UI trees are parsed */ parseFromString(str, config) { const xmlDoc = this.domParser.parseFromString(str, 'text/xml'); const errorNode = xmlDoc.querySelector('parsererror'); if (errorNode) { throw new Error('Invalid XML'); } return this.parseFromXMLDocument(xmlDoc, config); } /** * Parse an XML string from a URL. {@link BaseXMLUIParser#parseFromString} * will be called. * * @param resource - The URL to download the XML from * @param config - The configuration object to use for the parser * @param requestOptions - Options to use for the HTTP request * @returns Returns a pair containing, respectively, a Map which maps a UI tree name to a widget, and the parser context after all UI trees are parsed. Returned asynchronously as a promise */ parseFromURL(resource, config, requestOptions) { return __awaiter(this, void 0, void 0, function* () { const response = yield fetch(resource, requestOptions); if (!response.ok) { throw new Error(`Response not OK (status code ${response.status})`); } const str = yield response.text(); return this.parseFromString(str, config); }); } } //# sourceMappingURL=BaseXMLUIParser.js.map