@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
JavaScript
/**
* 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 };