@selenite/graph-editor
Version:
A graph editor for visual programming, based on rete and svelte.
193 lines (192 loc) • 7.1 kB
JavaScript
import { ErrorWNotif } from '../global/todo.svelte';
import { XMLParser } from 'fast-xml-parser';
import 'regenerator-runtime/runtime';
import { get } from 'svelte/store';
import wu from 'wu';
import * as __ from 'lodash-es';
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 }) {
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
});
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
});
}
return wu(res)
.map(({ xml, newLine }) => (newLine ? xml + '\n' : xml))
.reduce((a, b) => a + (a ? '\n' : '') + b, '');
}
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 wu(xml.entries())
.filter(([i, 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], []);
}
return rec(typesPaths[element], { path: [], withCursor: false }, baseXml);
}
export function getXmlAttributes(xml) {
return ':@' in xml ? xml[':@'] : {};
}
function mergeRec(base, toAdd) {
wu(toAdd).forEach((xmlNode) => {
const key = getElementFromParsedXml(xmlNode);
const elementMergeCandidate = wu(base)
.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, geosContext, cursorTag }) {
const typesPaths = get(geosContext.typesPaths);
if (!typesPaths)
throw new ErrorWNotif('No typesPaths in geosContext');
const res = JSON.parse(JSON.stringify(baseXml));
wu(newXml).forEach((newXmlNode) => {
const element = getElementFromParsedXml(newXmlNode);
const mergePositions = findPossibleMergePositions({
baseXml,
element,
typesPaths,
cursorTag
});
if (mergePositions.length === 0)
throw new ErrorWNotif('No merge position found');
let selectedMergePosition = mergePositions[0];
if (mergePositions.length > 1) {
selectedMergePosition = wu(mergePositions)
.filter(({ withCursor }) => withCursor)
.reduce((a, b) => {
if (a !== undefined)
throw new ErrorWNotif('Too many selected merge positions');
return b;
}, undefined);
}
const elementPath = typesPaths[element];
const mergePath = selectedMergePosition.path;
const target = wu(mergePath).reduce(({ ePath, base }, mergePathStep) => {
return {
base: base[mergePathStep.pos][mergePathStep.key],
ePath: ePath.slice(1)
};
}, { ePath: elementPath, 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 };