UNPKG

@selenite/commons

Version:

This typescript package provides a set of frequently used utilities, types and svelte actions for building projects with Typescript and Svelte.

231 lines (230 loc) 8.33 kB
/** * Utils to parse, format and write XML. * @module */ import { XMLParser } from 'fast-xml-parser'; import { isEqual } from 'lodash-es'; export function parseXMLArray(xml) { try { return JSON.parse(xml .replaceAll('{', '[') .replaceAll('}', ']') .replaceAll(/[a-zA-Z0-9.\-_/]+/g, (t) => { if (!isNaN(parseFloat(t)) && !t.includes('e')) return t; // if (t === '') return ''; // if (t === ',') return ','; return `"${t}"`; })); } catch { return undefined; } } export function formatXMLArray(arr) { return JSON.stringify(arr) .replaceAll('[', '{') .replaceAll(']', '}') .replaceAll(/"([^"]+)"/g, '$1'); } const fxpSettings = { preserveOrder: true, attributeNamePrefix: '', ignoreAttributes: false, commentPropName: '#comment', parseAttributeValue: false, trimValues: true, format: true, ignoreDeclaration: false }; function parseXml(xml) { const parser = new XMLParser({ ...fxpSettings }); const parsed = parser.parse(xml); return parsed; } export function buildXml({ parsedXml, indent = 2, baseSpace = '', cursorTag, endWithNewLine = true }) { const space = ' '.repeat(indent); const res = []; for (const element of parsedXml) { const attrs = []; let tag = ''; let childXml = ''; for (const [key, props] of Object.entries(element)) { if (key === cursorTag) continue; switch (key) { case cursorTag: break; case '#comment': res.push({ xml: `${baseSpace}<!--${props[0]['#text']}-->` }); break; case ':@': for (const [attr, value] of Object.entries(props)) { attrs.push(`${baseSpace}${space}${attr}="${value}"`); } break; case '#text': break; default: { tag = key; const children = props; childXml = buildXml({ parsedXml: children, indent, baseSpace: baseSpace + space, cursorTag, endWithNewLine: false }); break; } } } const preppedAttrs = attrs.length > 1 ? `\n${attrs.join('\n')}` : attrs.length === 1 ? ` ${attrs[0].trim()}` : ''; if (tag) if (childXml.length === 0) res.push({ xml: baseSpace + `<${tag}${preppedAttrs}${tag === '?xml' ? '?' : ' /'}>${tag === '?xml' ? '\n' : ''}`, newLine: attrs.length > 1 }); else res.push({ xml: `${baseSpace}<${tag}${preppedAttrs}>\n${childXml}${childXml.at(-1) !== '\n' ? '\n' : ''}${baseSpace}</${tag}>`, newLine: true }); } let xml = res.values().map(({ xml, newLine }) => (newLine ? xml + '\n' : xml)) .reduce((a, b) => a + (a ? '\n' : '') + b, ''); if (endWithNewLine && xml.at(-1) !== '\n') xml += '\n'; return xml; } export function getElementFromParsedXml(xml) { for (const key in xml) { if ([':@', '#text', '#comment'].includes(key)) continue; return key; } return null; } /** * Returns the different paths to the possible merge positions * @param param0 * @returns */ export function findPossibleMergePositions({ baseXml, element, typesPaths, cursorTag }) { function rec(elementPath, path, xml) { if (elementPath.length === 0) return [{ ...path, withCursor: cursorTag ? path.withCursor || cursorTag in xml : false }]; const possiblePathsEntryPoints = xml.filter((xmlNode) => getElementFromParsedXml(xmlNode) === elementPath[0]); if (possiblePathsEntryPoints.length === 0) { return [{ ...path, withCursor: cursorTag ? path.withCursor || cursorTag in xml : false }]; } return xml.entries() .filter(([, xmlNode]) => getElementFromParsedXml(xmlNode) === elementPath[0]) .map(([i, xmlNode]) => rec(elementPath.slice(1), { path: [...path.path, { pos: i, key: getElementFromParsedXml(xmlNode) }], withCursor: cursorTag ? path.withCursor || cursorTag in xml[i] : false }, xml[i][getElementFromParsedXml(xml[i])])) .reduce((a, b) => [...a, ...b], []); } const path = typesPaths.get(element); if (!path) { console.error("Couldn't find type path"); return []; } if (path === 'infinite') { console.error("Don't know where to insert element with infinite possibilities."); return []; } if (path.length > 1) { console.error('Mutltiple possible paths not handled'); return []; } return rec(path.at(0) ?? [], { path: [], withCursor: false }, baseXml); } export function getXmlAttributes(xml) { return ':@' in xml ? xml[':@'] : {}; } function mergeRec(base, toAdd) { toAdd.forEach((xmlNode) => { const key = getElementFromParsedXml(xmlNode); if (!key) return; const elementMergeCandidate = base.values() .filter((base_xmlNode) => getElementFromParsedXml(base_xmlNode) == key) .take(1) .toArray() .at(0); if (elementMergeCandidate && !('name' in getXmlAttributes(elementMergeCandidate)) && isEqual(getXmlAttributes(elementMergeCandidate), getXmlAttributes(xmlNode))) mergeRec(elementMergeCandidate[key], xmlNode[key]); else return base.push(xmlNode); }); } export function mergeParsedXml({ baseXml, newXml, typesPaths, cursorTag }) { const res = JSON.parse(JSON.stringify(baseXml)); newXml.forEach((newXmlNode) => { const element = getElementFromParsedXml(newXmlNode); if (!element) return; const mergePositions = findPossibleMergePositions({ baseXml, element, typesPaths, cursorTag }); if (mergePositions.length === 0) throw new Error('No merge position found'); let selectedMergePosition = mergePositions[0]; if (mergePositions.length > 1) { selectedMergePosition = mergePositions.values() .filter(({ withCursor }) => withCursor) .reduce((a, b) => { if (a !== undefined) throw new Error('Too many selected merge positions'); return b; }, undefined); } const elementPath = typesPaths.get(element); if (!elementPath) { console.error("Couldn't find type path"); return; } if (elementPath === 'infinite') { console.error("Don't know where to insert element with infinite possibilities."); return; } if (elementPath.length > 1) { console.error('Mutltiple possible paths not handled'); return; } const mergePath = selectedMergePosition.path; const target = mergePath.reduce(({ ePath, base }, mergePathStep) => { return { base: base[mergePathStep.pos][mergePathStep.key], ePath: ePath.slice(1) }; }, { ePath: elementPath.at(0) ?? [], base: res }); let toGlue = newXmlNode; for (const step of target.ePath.toReversed()) { toGlue = { [step]: [toGlue] }; } // Now we have a glue path ready, we can merge // We have to find the elements that can be merged like identic tags and attributes, // without names mergeRec(target.base, [ toGlue ]); }); return res; } function formatXml({ xml, indent = 2 }) { const parsedXml = parseXml(xml); return buildXml({ parsedXml, indent }); } function formatComment(comment) { return `<!-- ${comment.trim()} -->`; } export { parseXml, formatXml, formatComment };