Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(menu/exportCommunication): Allow export of communication section #1044

Merged
merged 9 commits into from
Nov 24, 2022
9 changes: 9 additions & 0 deletions public/js/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,13 @@ export const officialPlugins = [
requireDoc: false,
position: 'bottom',
},
{
name: 'Export Communication Section',
src: '/src/menu/ExportCommunication.js',
icon: 'sim_card_download',
default: false,
kind: 'menu',
requireDoc: true,
position: 'middle'
}
];
19 changes: 19 additions & 0 deletions src/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2797,6 +2797,25 @@ export function newLnInstGenerator(
};
}

/**
* Format xml string in "pretty print" style and return as a string
* @param xml - xml document as a string
* @param tab - character to use as a tab
* @returns string with pretty print formatting
*/
export function formatXml(xml: string, tab?: string): string {
let formatted = '',
indent = '';

if (!tab) tab = '\t';
xml.split(/>\s*</).forEach(function (node) {
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
formatted += indent + '<' + node + '>\r\n';
if (node.match(/^<?\w[^>]*[^/]$/)) indent += tab;
});
return formatted.substring(1, formatted.length - 3);
}

declare global {
interface ElementEventMap {
['pending-state']: PendingStateEvent;
Expand Down
101 changes: 101 additions & 0 deletions src/menu/ExportCommunication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { LitElement, property } from 'lit-element';
import { get } from 'lit-translate';

import { formatXml, newLogEvent } from '../foundation.js';

function cloneAttributes(destElement: Element, sourceElement: Element) {
let attr;
const attributes = Array.prototype.slice.call(sourceElement.attributes);
while ((attr = attributes.pop())) {
destElement.setAttribute(attr.nodeName, attr.nodeValue);
}
}
ca-d marked this conversation as resolved.
Show resolved Hide resolved

/**
* Take an XMLDocument and pretty-print, format it, attach it to a document link and then download it.
* @param doc - The XML document
* @param document - The element to attach to within the DOM
* @param filename - The filename to produce
* @returns The blob object that is serialised
*/
export function saveXmlBlob(
doc: XMLDocument,
document: Document,
filename: string
): void {
const blob = new Blob(
[formatXml(new XMLSerializer().serializeToString(doc))],
{
type: 'application/xml',
}
);

const a = document.createElement('a');
a.download = filename;
a.href = URL.createObjectURL(blob);
a.dataset.downloadurl = ['application/xml', a.download, a.href].join(':');
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () {
URL.revokeObjectURL(a.href);
}, 5000);
}

/**
* Plug-in to allow exporting of the Communication SCL element as an XML file.
*/
export default class ExportCommunication extends LitElement {
/** The document being edited as provided to plugins by [[`OpenSCD`]]. */
@property({ attribute: false }) doc!: XMLDocument;
@property({ attribute: false }) docName!: string;

/** Entry point for this plug-in */
async run(): Promise<void> {
// create document
const sclNamespace = 'http://www.iec.ch/61850/2003/SCL';
const sclDoc = document.implementation.createDocument(
sclNamespace,
'SCL',
null
);
const pi = sclDoc.createProcessingInstruction(
'xml',
'version="1.0" encoding="UTF-8"'
);
sclDoc.insertBefore(pi, sclDoc.firstChild);

// ensure schema revision and namespace definitions are transferred
cloneAttributes(sclDoc.documentElement, this.doc.documentElement);

const communicationSection = this.doc.querySelector(
':root > Communication'
);

if (communicationSection) {
const header = this.doc.querySelector(':root > Header')?.cloneNode(true);
const communication = this.doc
.querySelector(':root > Communication')
?.cloneNode(true);

if (header) sclDoc.documentElement.appendChild(<Node>header);
sclDoc.documentElement.appendChild(<Node>communication);

const ending = this.docName.slice(0, -4);
let docName = `${this.docName}-Communication.scd`;
// use filename extension if there seems to be one
if (ending.slice(0, 1) === '.') {
docName = `${this.docName.slice(0, -4)}-Communication${ending}`;
}
saveXmlBlob(sclDoc, document, docName);
} else {
this.dispatchEvent(
newLogEvent({
kind: 'warning',
title: get('exportCommunication.noCommunicationSection'),
})
);
}
}
}
3 changes: 3 additions & 0 deletions src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,9 @@ export const de: Translations = {
smvopts: 'Optionale Felder',
},
},
exportCommunication: {
noCommunicationSection: 'Kein Export als Abschnitt Kommunikation leer',
},
add: 'Hinzufügen',
new: 'Neu',
remove: 'Entfernen',
Expand Down
3 changes: 3 additions & 0 deletions src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,9 @@ export const en = {
smvopts: 'Optional Fields',
},
},
exportCommunication: {
noCommunicationSection: 'No export as Communication section empty',
},
add: 'Add',
new: 'New',
remove: 'Remove',
Expand Down
15 changes: 15 additions & 0 deletions test/integration/__snapshots__/open-scd.test.snap.js
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,21 @@ snapshots["open-scd looks like its snapshot"] =
</mwc-icon>
Compare IED
</mwc-check-list-item>
<mwc-check-list-item
aria-disabled="false"
class="official"
graphic="control"
hasmeta=""
left=""
mwc-list-item=""
tabindex="-1"
value="/src/menu/ExportCommunication.js"
>
<mwc-icon slot="meta">
sim_card_download
</mwc-icon>
Export Communication Section
</mwc-check-list-item>
<li
divider=""
inset=""
Expand Down
37 changes: 37 additions & 0 deletions test/integration/menu/ExportCommunication.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, fixture, html } from '@open-wc/testing';

import '../../mock-wizard-editor.js';
import { MockWizardEditor } from '../../mock-wizard-editor.js';

import ExportCommunication from '../../../src/menu/ExportCommunication.js';

describe('Export Communication section functions', () => {
if (customElements.get('export-communication') === undefined)
customElements.define('export-communication', ExportCommunication);

let parent: MockWizardEditor;
let element: ExportCommunication;
let doc: XMLDocument;

beforeEach(async () => {
doc = await fetch('/test/testfiles/communication.scd')
.then(response => response.text())
.then(str => new DOMParser().parseFromString(str, 'application/xml'));

parent = await fixture(html`
<mock-wizard-editor
><export-communication></export-communication
></mock-wizard-editor>
`);

element = <ExportCommunication>(
parent.querySelector('export-communication')!
);

element.doc = doc;
element.docName = 'CommunicationTest.scd';
element.run();
await parent.requestUpdate();
});
danyill marked this conversation as resolved.
Show resolved Hide resolved

});