Jump To …

serializer.js

var xjst = require('../../xjst'),
    utils = xjst.utils,

    XJSTCompiler = xjst.ometa.XJSTCompiler;

function Serializer (options)

@options {Object} Compiler options object

Serializer @constructor

function Serializer(options) {
  this.options = options;
  this.fnList = xjst.helpers.flist.create(this, options);
  this.hashList = xjst.helpers.hlist.create(this, options);
  this.merges = {};
}

function create (options)

@options {Object} Compiler options object

Returns Serializer instance

exports.create = function create(options) {
  return new Serializer(options);
};

Serializer.prototype.addContext = function addContext(body) {

Store context if it's used

  if (/__this/.test(body)) {
    return 'var __this = this;' + body;
  } else {
    return body;
  }
};

function serialize (node, tails, _parents)

@node {Object} AST Node

@tails {Object} Tails

@_parents {Array} internal (previous parents' ids)

Returns serialized body of node

Serializer.prototype.serialize  = function serialize(node, tails, _parents) {
  var self = this,
      options = this.options;

  if (!tails) tails = {};

Returns a stringified path from root to current node

  function getParents() {
    return utils.stringify(_parents.map(function(parent) {
      return '$' + parent;
    }));
  }

Returns the path from root to current node + current node's id

  function parents() {
    return options.merge ? _parents.concat(node.longId || node.id) : _parents;
  }

  var res = [];

If we already seen a node with the same id Just call it by it's name

  if (this.merges[node.id] !== undefined) {
    tails[node.id] = node;

    res.push('return ', this.fnList.getName(node) + '.call(this');

Add parents information for merging (experimental)

    if (options.merge && node.unexpected) {
      res.push(',', getParents());
    }

    res.push(');');

    return res.join('');
  }

If current is not a leaf (i.e. it's a switch)

  if (node.tag) {

Compile all predicate values and bodies

    var tag = XJSTCompiler.match(node.tag, 'skipBraces'),
        cases = node.cases.map(function(branch) {
          return [
            XJSTCompiler.match(branch[0], 'skipBraces'),
            self.serialize(branch[1], tails, parents()),
            null // Allocate space for id generation
          ];
        }),
        def = this.serialize(node['default'], tails, parents());

Set ids for equal cases

    if (cases.length > 1) {
      cases.sort(function(branch) {
        return branch[1] > branch[2] ? 1 : branch[1] === branch[2] ? 0 : -1;
      });
      cases.reduce(function(prev, curr) {
        if (prev[2] === null) prev[2] = 0;
        if (curr[1] === prev[1]) {
          curr[2] = prev[2];
        } else {
          curr[2] = prev[2] + 1;
        }

        return curr;
      });
    }

Just put default body

    if (cases.length === 0) {
      res.push(def);

Generate a simple if/else statement if it has only one case

    } else if (cases.length === 1) {
      var c = cases[0];
      res.push(
          'if (', tag, ' === ', c[0], ') {\n',
              c[1],
          '} else {\n',
              def,
          '}\n'
      );

Generate multiple if/else if/else statements TODO: determine optimal cases' length maximum

    } else if (cases.length < 32) {
      res.push('var __t = ', tag, '; \n');

      var grouped = [];
      cases.map(function(branch) {
        return [['__t === ' + branch[0]], branch[1], branch[2]];
      }).reduce(function(prev, curr) {
        if (prev !== null && prev[2] === curr[2]) {
          prev[0] = prev[0].concat(curr[0]);
          return prev;
        }

        grouped.push(curr);
        return curr;
      }, null);

      grouped.forEach(function(branch, i) {
        if (i !== 0) res.push(' else ');
        res.push(
          '  if (', branch[0].join(' || '), ') {\n',
          branch[1],
          '  } \n'
        );
      });
      res.push(
        ' else {\n',
        def,
        '}'
      );

    } else {

Turn switch in the hashmap lookup

      res.push(this.hashList.add(node.id, tag, cases, def));
    }

    if (node.fn) {
      var fnName = this.fnList.add(node.id, res.join(''), node);

      res = ['return ', fnName, '.call(this);'];
    }

Compile statement or wrap it into a function

  } else {
    var body = XJSTCompiler.match(node.stmt, 'skipBraces') + ';\nreturn;';

We should wrap into a function, only if statement is big or if we was directly asked to do this

    if (node.size > 1 || node.fn) {

Save function body

      var fnName = this.fnList.add(node.id, body, node);

      res.push('return ', fnName, '.call(this');

Tagged statements should be called with a parents list (needed for client-side merging)

      if (node.unexpected) {
        res.push(',', getParents());
      }

      res.push(');');
    } else {
      res.push(body);
    }
  }

  return res.join('');
}

function shiftTails (tails)

@tails {Object} tails

Get the first tail from the tails list (ordered by keys)

Serializer.prototype.shiftTails = function shiftTails(tails) {
  var ids = Object.keys(tails).sort(function(a, b) { return a - b; }),
      res = tails[ids[0]];

  delete tails[ids[0]];

  return res;
}

function detectJoins (node, joins)

@node {Object} AST Node

@joins {Object} internal, may be empty

Count all nodes' id occurrences

Serializer.prototype.detectJoins = function detectJoins(node, joins) {
  var self = this;
  joins || (joins = {});

  if (!node.id) return joins;

  if (joins[node.id] !== undefined) {
    joins[node.id]++;
  } else {
    joins[node.id] = 1;
    if (node.tag) {
      node.cases.forEach(function(branch) {
        self.detectJoins(branch[1], joins);
      });
      this.detectJoins(node['default'], joins);
    }
  }

  return joins;
};

function render (ast)

@ast {AST} ast

@type {String|undefined} Result type ('full' by default)

Renders tree or it's part

Serializer.prototype.render = function render(ast, type) {
  if (!type) type = 'full';

  var res = [],
      joins = this.detectJoins(ast),
      merges = this.merges;

Create functions for each join that was occurred more than once

  var labels = Object.keys(joins).filter(function(key) {
    return joins[key] > 1;
  }).sort(function(a, b) { return b - a; });

  labels.forEach(function(key) {
    merges[key] = true;
  });

Start with one tail

  var tails = {};
  tails[ast.id] = ast;

Main serialization loop

  var node,
      first = true,
      id,
      body;

  while (node = this.shiftTails(tails)) {
    delete merges[id = node.id];
    var body = this.serialize(node, tails, []);

    if (first) {
      first = false;
      res.push(body);
    } else {
      if (!this.fnList.has(id)) this.fnList.add(id, body, node);
    }
  }

If type === 'partial' - return body without functions and hashmaps

  if (type === 'partial') return res.join('');

  return [
    this.options.merge ?
        'var _c = exports.config = {};' +

Insert .mergeWith template function

        'exports.mergeWith = ' + utils.mergeWith.toString() + ';'
        :
        '',
    'exports.apply = apply;',
    'function apply()',
  ].concat(
      '{',
      this.addContext(res.join('')),
      '};',

Insert all switches that was translated to the hashmaps

      this.hashList.render(),

Insert all switches that was wrapped in the functions

      this.fnList.render()
  ).join('');
};