Home Manual Reference Source Test

src/lib/transformers/ScriptTransformer.js

import { extname } from 'path';
import { parseString as parseXML, Builder as XMLBuilder } from 'xml2js';
import { replaceExtension, log, colors } from 'gulp-util';

import Transformer from './Transformer';
import NodeType from '../def/NodeType';

/**
 * Extracts JavaScript code from atvise (serverside) scripts. A config file is used to describe
 * parameters.
 */
export default class ScriptTransformer extends Transformer {

  /**
   * Creates a new script transformer. Currently no options are applied.
   */
  constructor() {
    super();

    /**
     * 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',
      },
      cdata: true,
    });
  }

  /**
   * Parses script XML files and splits them a 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) => {
      let document = {};

      if (err || !results || !results.script) {
        if (err) {
          log(colors.red('Error parsing script'), colors.cyan(node.relative));
          log(colors.red(err.message.split('\n').join(' ')));
        } else if (!results) {
          log(colors.red('Invalid script XML'), colors.cyan(node.relative));
          log(colors.red('Empty document'));
        } else if (!results.script) {
          log(colors.red('Invalid script XML'), colors.cyan(node.relative));
          log(colors.red('Tag <script> not found'));
        }
      } else {
        document = results.script;
      }

      const config = {};
      let code = '';

      // Extract metadata
      if (document.metadata && document.metadata.length === 1) {
        const meta = document.metadata[0];

        if (meta.icon && meta.icon.length === 1) {
          const icon = meta.icon[0];
          config.icon = icon.$ || {};
          config.icon.content = icon._ || '';
        }

        if (meta.visible && meta.visible.length === 1) {
          config.visible = Boolean(meta.visible[0]);
        }

        if (meta.title && meta.title.length === 1) {
          config.title = meta.title[0];
        }

        if (meta.description && meta.description.length === 1) {
          config.description = meta.description[0];
        }
      }

      // Extract parameters
      if (document.parameter) {
        config.parameters = [];
        document.parameter.forEach(param => config.parameters.push(param.$));
      }

      // Extract JavaScript
      if (document.code && document.code.length === 1) {
        code = document.code[0];
      }

      // Write files
      stream.push(Transformer.encapsuledFile(node, '.json', JSON.stringify(config, null, '  ')));
      stream.push(Transformer.encapsuledFile(node, '.js', code));

      cb();
    });
  }

  /**
   * Buffers the contents of the files a script 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 script = replaceExtension(file.relative, '');

    if (!stream._scripts) { stream._scripts = {}; }
    if (!stream._scripts[script]) { stream._scripts[script] = {}; }

    const ext = extname(file.path);
    stream._scripts[script][ext] = file;

    cb();
  }

  /**
   * Called after all script files were buffered. Creates script XMLs and pushes 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._scripts) {
      if (stream._scripts[key]['.js']) { // FIXME: Error if Script file doesn't exist
        this.createScriptCode(stream, stream._scripts[key]);
      }
    }

    cb();
  }

  /**
   * Creates an XML script file from buffered JavaScript and configuration code.
   * @param {Stream} stream The stream used.
   * @param {{}} script The buffered script content.
   */
  createScriptCode(stream, script) {
    const paramsFile = script['.json'];
    const jsFile = script['.js'];
    const config = JSON.parse(paramsFile.contents);
    const code = jsFile.contents.toString();

    const result = {
      script: {},
    };

    if (paramsFile.rc.typeDefinition === NodeType.QuickDynamic) {
      const icon = config.icon.content;
      delete config.icon.content;

      result.script.metadata = {
        icon: {
          $: config.icon,
          _: icon || '',
        },
        visible: config.visible ? 1 : 0,
        title: config.title,
        description: config.description,
      };
    }

    result.script.parameter = config.parameters ?
      config.parameters.map(param => ({ $: param })) :
      [];

    result.script.code = code;

    const scriptFile = paramsFile.clone({ contents: false });
    scriptFile.stat.mtime = new Date(Math.max(jsFile.stat.mtime, paramsFile.stat.mtime));

    scriptFile.path = scriptFile.dirname;
    scriptFile.contents = Buffer.from(this.outputXMLBuilder.buildObject(result));
    stream.push(scriptFile);
  }

  /**
   * This transformer is only applied to {@link NodeType.Script} and {@link NodeType.QuickDynamic}
   * node types.
   * @type {Map<NodeType, Boolean>}
   */
  static get applyToTypes() {
    return {
      [NodeType.Script]: true,
      [NodeType.QuickDynamic]: true,
    };
  }

}