Home Manual Reference Source Test

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,
    };
  }
}