UNPKG

fume-fhir-converter

Version:

FHIR-Utilized Mapping Engine - Community

249 lines (219 loc) 8.09 kB
/** * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved * Project name: FUME-COMMUNITY */ import { XMLParser } from 'fast-xml-parser'; const ATTRIBUTE_PREFIX = '@_'; export const parseXml = (xml: string) => { const json = parseXmlWithXhtmlHandling(xml); const values: any[] = []; if (Object.keys(json).length === 1) { const rootKey = Object.keys(json)[0]; if (Array.isArray(json[rootKey])) { for (const jsonValue of json[rootKey]) { values.push(standardizeJson(jsonValue, rootKey)); } } else { values.push(standardizeJson(json[rootKey], rootKey)); } } if (Object.keys(json).length > 1) { for (const rootKey of Object.keys(json)) { values.push(standardizeJson(json[rootKey], rootKey)); } } return values.length === 1 ? values[0] : values; }; const parseXmlWithXhtmlHandling = (xml: string): any => { const options: Record<string, any> = { ignoreAttributes: false, ignoreDeclaration: true, attributeNamePrefix: ATTRIBUTE_PREFIX, allowBooleanAttributes: true, alwaysCreateTextNode: true, numberParseOptions: { leadingZeros: false, hex: false, skipLike: /\*/ } }; const firstParser = new XMLParser(options); const firstParsing = firstParser.parse(xml); const xhtmlPaths = getXhtmlPaths('', firstParsing, []); if (xhtmlPaths.length === 0) { return firstParsing; } options.stopNodes = [...new Set(xhtmlPaths)]; // remove duplications; const secondParser = new XMLParser(options); const secondParsing = secondParser.parse(xml); return secondParsing; }; const getXhtmlPaths = (currentPath, currentValue, xhtmlPaths) => { if (Array.isArray(currentValue)) { for (let i = 0; i < currentValue.length; i++) { getXhtmlPaths(`${currentPath}`, currentValue[i], xhtmlPaths); } } else if (typeof currentValue === 'object') { if (currentValue[`${ATTRIBUTE_PREFIX}xmlns`] === 'http://www.w3.org/1999/xhtml') { xhtmlPaths.push(currentPath); } else { for (const key of Object.keys(currentValue)) { getXhtmlPaths(`${currentPath ? `${currentPath}.` : ''}${key}`, currentValue[key], xhtmlPaths); } } } return xhtmlPaths; }; const standardizeJson = (json, rootKey: string): Record<string, any> => { const parsedXml = recursiveStandardize(json, rootKey); const namespaceIndex = rootKey.indexOf(':'); const baseRootKey = namespaceIndex === -1 ? rootKey : rootKey.slice(namespaceIndex + 1); const value = parsedXml[baseRootKey]; if (typeof value === 'object') { const namespaceIndex = rootKey.indexOf(':'); if (namespaceIndex !== -1) { value._namespace = rootKey.slice(0, namespaceIndex); if (!value.resourceType) { value._xmlTagName = baseRootKey; } } else if (!value.resourceType) { value._xmlTagName = baseRootKey; } } return value; }; const recursiveStandardize = (node: any, key: string) => { const newNode: Record<string, any | any[]> = {}; let newKey: string = key; // extract values and attributes let textValue: any | undefined; const complexValue = {}; let valueAttribute: string | undefined; const attributes: Record<string, any> = {}; for (const key of Object.keys(node)) { if (key === '#text' && (typeof node[key] !== 'string' || node[key].length > 0)) { textValue = String(node[key]); } else if (key.startsWith(ATTRIBUTE_PREFIX)) { attributes[key.slice(ATTRIBUTE_PREFIX.length)] = node[key]; if (key === `${ATTRIBUTE_PREFIX}value`) { valueAttribute = node[key]; } } else { complexValue[key] = node[key]; } } // extract namespace const namespaceIndex = key.indexOf(':'); let namespaceValue; if (namespaceIndex !== -1) { newKey = key.slice(namespaceIndex + 1); namespaceValue = key.slice(0, namespaceIndex); } // extract complex childs const complexChildsObject = createComplexChildsObject(complexValue); // replace xmlns="http://hl7.org/fhir" with resourceType if (attributes.xmlns === 'http://hl7.org/fhir') { attributes.resourceType = newKey; delete attributes.xmlns; } // build new node if (textValue && attributes.xmlns === 'http://www.w3.org/1999/xhtml') { textValue = `<${key}${buildAttributesString(attributes)}>${textValue}</${key}>`; newNode[newKey] = textValue; } else if (textValue) { newNode[newKey] = textValue; addInnerKeys(newNode, `_${newKey}`, { _namespace: namespaceValue }); addInnerKeys(newNode, `_${newKey}`, attributes); } else if (valueAttribute) { newNode[newKey] = valueAttribute; addInnerKeys(newNode, `_${newKey}`, { _namespace: namespaceValue }); delete attributes.value; addInnerKeys(newNode, `_${newKey}`, attributes); addInnerKeys(newNode, `_${newKey}`, complexChildsObject); } else if (Object.keys(complexChildsObject).length > 0) { addInnerKeys(newNode, newKey, complexChildsObject); addInnerKeys(newNode, newKey, { _namespace: namespaceValue }); addInnerKeys(newNode, newKey, attributes); } else if (Object.keys(attributes).length > 0) { addInnerKeys(newNode, newKey, { _namespace: namespaceValue }); addInnerKeys(newNode, newKey, attributes); } return newNode; }; const createComplexChildsObject = (complexChilds) => { const complexChildsObject = {}; for (const childKey of Object.keys(complexChilds)) { if (Array.isArray(complexChilds[childKey])) { const childValues = complexChilds[childKey]; for (const childValue of childValues) { const child = recursiveStandardize(childValue, childKey); addChildToParent(complexChildsObject, child); } } else { const child = recursiveStandardize(complexChilds[childKey], childKey); addChildToParent(complexChildsObject, child); } } return complexChildsObject; }; // /_xmlTagName const addChildToParent = (parentNode, child) => { const childKey: string | undefined = Object.keys(child).find(key => !key.startsWith('_')); const childAttKey: string | undefined = Object.keys(child).find(key => key.startsWith('_')); if (childKey === undefined && childAttKey === undefined) return; const key = childKey ?? childAttKey!.slice(1); const attKey = childAttKey ?? `_${childKey}`; if ((parentNode[key]) || (parentNode[attKey])) { const keySize = parentNode[key] ? (Array.isArray(parentNode[key]) ? parentNode[key].length : 1) : 0; const attKeySize = parentNode[attKey] ? (Array.isArray(parentNode[attKey]) ? parentNode[attKey].length : 1) : 0; if (keySize > 1) { parentNode[key].push(child[key] ?? null); } else if (keySize === 1) { parentNode[key] = [parentNode[key], child[key] ?? null]; } else if (childKey) { parentNode[key] = Array.from({ length: attKeySize }, (x, i) => null); parentNode[key].push(child[key] ?? null); } if (attKeySize > 1) { parentNode[attKey].push(child[attKey] ?? null); } else if (attKeySize === 1) { parentNode[attKey] = [parentNode[attKey], child[attKey] ?? null]; } else if (childAttKey) { parentNode[attKey] = Array.from({ length: keySize }, (x, i) => null); parentNode[attKey].push(child[attKey] ?? null); } } else { if (childKey) { parentNode[key] = child[key]; } if (childAttKey) { parentNode[attKey] = child[attKey]; } } }; const addInnerKeys = (object, key, innerObject) => { for (const innerKey of Object.keys(innerObject)) { addInnerKey(object, key, innerKey, innerObject[innerKey]); } }; const addInnerKey = (object, key, innerKey, innerValue) => { if (innerValue === undefined) return; if (object[key] === undefined) { object[key] = {}; } if (innerKey === 'resourceType') { object[key] = { resourceType: innerValue, ...object[key] }; } else { object[key][innerKey] = innerValue; } }; const buildAttributesString = (attributs) => { let string = ''; for (const key of Object.keys(attributs)) { string += ` ${key}="${attributs[key]}"`; } return string; };