/* * This file is part of TREB. * * TREB is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any * later version. * * TREB is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along * with TREB. If not, see . * * Copyright 2022-2026 trebco, llc. * info@treb.app * */ import { Unescape } from './unescape_xml'; import { type X2jOptions, XMLBuilder, type XmlBuilderOptions } from 'fast-xml-parser'; export const XMLTagProcessor = (name: string, value: string): string => Unescape(value); export interface DOMContent { [index: string]: string|DOMContent|string[]|DOMContent[]|number|number[]|undefined; } // --- take 2 ------------------------------------------------------------------ export interface XMLKeys { a$?: Record; t$?: string; } export type DOMElementType = number|number[]|string|string[]|(BaseDOM & XMLKeys)|(BaseDOM & XMLKeys)[]; export interface BaseDOM { [key: string]: DOMElementType; } export type GenericDOMElement = BaseDOM & XMLKeys; export const GenericDOMArray = (element: GenericDOMElement|GenericDOMElement[]): GenericDOMElement[] => element ? Array.isArray(element) ? element : [element] : []; // ----------------------------------------------------------------------------- /** * not sure why we have to do this, but filter attributes that * have value === undefined */ export const ScrubXML = (dom: DOMContent) => { if (dom) { if (Array.isArray(dom)) { for (const entry of dom) { ScrubXML(entry); } } else if (typeof dom === 'object') { for (const [key, value] of Object.entries(dom)) { if (key === 'a$') { if (typeof value === 'object') { const replacement: DOMContent = {}; for (const [attr_name, attr_value] of Object.entries(value)) { if (attr_value !== undefined) { replacement[attr_name] = attr_value; } } dom[key] = replacement; } } else { ScrubXML(value as DOMContent); } } } } return dom; }; export const PatchXMLBuilder = (options: Partial) => { const builder = new XMLBuilder(options); const build = builder.build; builder.build = (arg: DOMContent) => { return build.call(builder, ScrubXML(arg)); // get the "this" value right } return builder; }; ////////////////// /* export const XMLOptions: Partial = { ignoreAttributes: false, attributeNamePrefix: '__', trimValues: false, textNodeName: 'text__', tagValueProcessor: XMLTagProcessor, ignoreDeclaration: true, }; */ /** * group attributes under `a$`, and don't add attribute prefixes (should be * implicit on that option, but hey). */ export const XMLOptions2: Partial = { ignoreAttributes: false, attributesGroupName: 'a$', attributeNamePrefix: '', textNodeName: 't$', trimValues: false, ignoreDeclaration: true, // arrayMode: false, // this was removed in FXP, but false is default anyway isArray: (tagName: string) => { return /Relationship$/.test(tagName); }, tagValueProcessor: XMLTagProcessor, }; /** * some utility functions for working with the xml/json * objects we get from fast-xml-parser. */ export class XMLUtils { /** * @deprecated * * use the array version. it will run in approximately the same * amount of time for non-array structures, and it's safer in the * event you have an array somewhere in the node hierarchy. */ public static FindChild(root: any = {}, path: string) { const elements = path.split('/'); for (const element of elements) { root = root[element]; if (!root) { return undefined; } } return root; } /** * the aim of this function is to handle the case where we don't * know where the arrays are -- any element in the path could be * multiple. for example, the path * * a/b/c * * could be reflected in xml as * * * * * * * * * or it could be * * * * * * * * * * * in either case we want both "c" elements. */ public static FindAll(root: DOMContent = {}, path: string): any[] { const components = path.split('/'); // allow proper paths starting with ./ if (components[0] === '.') { components.shift(); } // don't allow going up the stack (we don't have it) if (components[0] === '..') { throw new Error(`invalid path (no access to parent)`); } // root path would be written like /x/a (I think) if (components[0] === '') { throw new Error(`invalid path (no access to root)`); } // TODO if (components[0] === '**') { throw new Error('ENOTIMPL'); } return this.FindAllTail(root, components); } /** * how hard would it be to support wildcards? ... * basically if you see a wildcard, just treat every element as a * match -- right? */ public static FindAllTail(root: DOMContent|DOMContent[], elements: string[]): DOMContent[] { if (Array.isArray(root)) { return root.reduce((composite, element) => { return composite.concat(this.FindAllTail(element, elements)); }, [] as DOMContent[]); } for (let i = 0; i < elements.length; i++) { const element = elements[i]; // wildcard should be handled here... if element is a wildard, // then either continue (single element) or recurse (multiple elements). // we need to avoid the attribute and text children. // NOTE we still have some code using the old options type, which // maps attributes and text differently... hopefully they won't use // wildcards if (element === '*') { const pairs = Object.entries(root) as Array<[string, DOMContent]>; root = pairs.reduce((result, [key, value]) => { if (key !== 'a$' && key !== 't$') { result.push(value); } return result; }, [] as DOMContent[]); /* // two loops, really? come on root = Object.keys(flat). filter(key => (key !== 'a$' && key !== 't$')). map(key => flat[key]) as DOMContent[]; */ } else { root = root[element] as DOMContent; } if (!root) { return []; // no matching element } if (Array.isArray(root)) { // if this is the target node, then return the array; // otherwise, we need to recurse. also we can check for // zero-length array. if (i === elements.length - 1 || root.length === 0) { return root; } const step = elements.slice(1); return root.reduce((composite, element) => { return composite.concat(this.FindAllTail(element, step)); }, [] as DOMContent[]); } } // if we get here, root must be a single element so wrap it up return [root]; } }