src/lib/transformers/DisplayTransformer.js
import { statSync } from 'fs';
import { Buffer } from 'buffer';
import { extname } from 'path';
import { parseString as parseXML, Builder as XMLBuilder } from 'xml2js';
import { replaceExtension } from 'gulp-util';
import Transformer from './Transformer';
import NodeType from '../def/NodeType';
/**
* Splits atvise displays into their SVG and JavaScript parts. A config file is used to describe
* dependencies and parameters.
*/
export default class DisplayTransformer extends Transformer {
/**
* Creates a new display transformer. Currently no options are applied.
*/
constructor() {
super();
/**
* The XMLBuilder to use when writing to the filesystem. Uses OS specific defaults.
* @type {XMLBuilder}
*/
this.inputXMLBuilder = new XMLBuilder({ cdata: false });
/**
* The XMLBuilder to use when writing back to the database. Uses atvise specific defaults.
* @type {XMLBuilder}
*/
this.outputXMLBuilder = new XMLBuilder({
renderOpts: {
pretty: true,
indent: ' ',
newline: '\r\n', // TODO: Replace with require('os').EOL once atvise is cross-platform
},
xmldec: {
version: '1.0',
encoding: 'UTF-8',
standalone: false,
},
cdata: false,
});
}
/**
* Parses display XML files and splits them into SVG, JavaScript and a configuration file.
* @param {NodeStream} stream The stream to use.
* @param {Node} node The node to split.
* @param {String} enc The encoding used.
* @param {function(err: Error, node: Node)} cb Callback to call after transformation.
*/
transformFromDB(stream, node, enc, cb) {
parseXML(node.contents, (err, results) => {
const xml = results;
const document = results.svg;
const config = {};
// Extract JavaScript
if (document.script && document.script.length > 0) {
document.script.forEach(script => {
if (script.$ && script.$.src) {
// is external script, link in config
if (!config.dependencies) { config.dependencies = []; }
config.dependencies.push(script.$.src);
} else {
// is display script, save in own file
stream.push(Transformer.encapsuledFile(node, '.js', script._));
}
});
delete xml.svg.script;
}
// Extract metadata
if (document.metadata && document.metadata.length > 0) {
const meta = document.metadata[0];
// - Parameters
if (meta['atv:parameter'] && meta['atv:parameter'].length > 0) {
config.parameters = [];
meta['atv:parameter'].forEach(param => config.parameters.push(param.$));
delete xml.svg.metadata[0]['atv:parameter'];
}
}
// Save config
stream.push(Transformer.encapsuledFile(node, '.json', JSON.stringify(config, null, ' ')));
// Save pure SVG code
stream.push(Transformer.encapsuledFile(node, '.svg', this.inputXMLBuilder.buildObject(xml)));
cb();
});
}
/**
* Buffers the contents of the files a display was split into.
* @param {Stream} stream The stream used.
* @param {AtviseFile} file The file to buffer.
* @param {String} enc The encoding used.
* @param {function(err: Error, file: AtviseFile)} cb Callback to call after transformation.
*/
transformFromFilesystem(stream, file, enc, cb) {
// Buffer contents
const display = replaceExtension(file.relative, '');
if (!stream._displays) { stream._displays = {}; }
if (!stream._displays[display]) { stream._displays[display] = {}; }
const ext = extname(file.path); // .split('.').reverse()[0];
stream._displays[display][ext] = file;
cb();
}
/**
* Creates an XML display from buffered SVG, JavaScript and configuration code.
* @param {Stream} stream The stream used.
* @param {{}} display The buffered display content.
*/
createDisplay(stream, display) {
const config = JSON.parse(display['.json'].contents);
const svgFile = display['.svg'];
let mtime = Math.max(svgFile.stat.mtime, display['.json'].stat.mtime);
parseXML(svgFile.contents, (err, svg) => {
if (err) {
console.log('Error parsing svg at ' + svgFile.path);
throw err;
}
const result = svg;
// Insert dependencies
result.svg.script = [];
if (config.dependencies) {
config.dependencies.forEach(p => result.svg.script.push({
$: { src: p },
}));
}
// Insert script
const scriptFile = display['.js'];
if (scriptFile) {
mtime = Math.max(mtime, scriptFile.stat.mtime);
result.svg.script.push({
$: { type: 'text/ecmascript' },
_: scriptFile.contents,
});
}
// Insert metadata
// - Parameters
if (config.parameters && config.parameters.length > 0) {
if (!result.svg.metadata || !result.svg.metadata[0]) {
result.svg.metadata = [{}];
}
if (!result.svg.metadata[0]['atv:parameter']) {
result.svg.metadata[0]['atv:parameter'] = [];
}
// FIXME: Parameters should come before `atv:gridconfig` and `atv:snapconfig`
config.parameters
.forEach(param => result.svg.metadata[0]['atv:parameter'].push({ $: param }));
}
const displayFile = svgFile.clone({ contents: false });
displayFile.stat.mtime = new Date(Math.max(mtime, statSync(displayFile.dirname).mtime));
displayFile.path = displayFile.dirname;
displayFile.contents = Buffer.from(this.outputXMLBuilder.buildObject(result));
stream.push(displayFile);
});
}
/**
* Called after all display files were buffered. Creates display XMLs and writes them to the
* stream.
* @param {Stream} stream The stream used.
* @param {function(err: Error)} cb Callback to call after flushing.
*/
flushFromFilesystem(stream, cb) {
for (const key in stream._displays) {
if (stream._displays[key]['.svg']) { // FIXME: Error if SVG doesn't exist
this.createDisplay(stream, stream._displays[key]);
}
}
cb();
}
/**
* This transformer is only applied to {@link NodeType.Display} node types.
* @type {Map<NodeType, Boolean>}
*/
static get applyToTypes() {
return {
[NodeType.Display]: true,
};
}
}