UNPKG

@selenite/graph-editor

Version:

A graph editor for visual programming, based on rete and svelte.

193 lines (192 loc) 7.1 kB
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 };