Source: compiler.js

/**
 * Compiles a template Tree into JavaScript.
 *
 * @module compiler
 * @requires shared/register_partial
 * @requires shared/register_filter
 * @requires shared/map
 * @requires shared/encode
 * @requires utils/type
 * @requires utils/create_object
 */
define(function(require, exports, module) {
  "use strict";

  // Shared.
  var registerPartial = require("./shared/register_partial");
  var registerFilter = require("./shared/register_filter");
  var map = require("./shared/map");
  var encode = require("./shared/encode");

  // Utils.
  var type = require("./utils/type");
  var createObject = require("./utils/create_object");

  // Support.
  require("./support/array/map");
  require("./support/array/reduce");

  // Borrowed from Underscore.js template function.
  var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;

  // Borrowed from Underscore.js template function.
  var escapes = {
    "'": "'",
    "\\": "\\",
    "\r": "r",
    "\n": "n",
    "\t": "t",
    "\u2028": "u2028",
    "\u2029": "u2029"
  };

  /**
   * Escapes passed values.
   *
   * @private
   * @param {string} value - The value to escape.
   * @returns {string} The value escaped.
   */
  function escapeValue(value) {
    return value.replace(escaper, function(match) {
      return "\\" + escapes[match];
    });
  }

  /**
   * Normalizes properties in the identifier to be looked up via hash-style
   * instead of dot-notation.
   *
   * @private
   * @param {string} identifier - The identifier to normalize.
   * @returns {string} The identifier normalized.
   */
  function normalizeIdentifier(identifier) {
    if (identifier === ".") {
      return "data['.']";
    }

    return "data" + identifier.split(".").map(function(property) {
      return "['" + property + "']";
    }).join("");
  }

  /**
   * Represents a Compiler.
   *
   * @class
   * @memberOf module:compiler
   * @param {Tree} tree - A template [Tree]{@link module:tree.Tree} to compile.
   */
  function Compiler(tree) {
    this.tree = tree;
    this.string = "";

    var compiledSource = this.process(this.tree.nodes);

    // The compiled function body.
    var body = [];

    // If there is a function, concatenate it to the default empty value.
    if (compiledSource) {
      compiledSource = " + " + compiledSource;
    }

    // Include map and its dependencies.
    if (compiledSource.indexOf("map(") > -1) {
      body.push(createObject, type, map);
    }

    // Include encode and its dependencies.
    if (compiledSource.indexOf("encode(") > -1) {
      body.push(type, encode);
    }

    // The compiled function body.
    body = body.concat([
      // Return the evaluated contents.
      "return ''" + compiledSource
    ]).join(";\n");

    // Create the JavaScript function from the source code.
    this.func = new Function("data", "partials", "filters", body);

    // toString the function to get its raw source and expose.
    this.source = [
      "{",
        "_partials: {},",
        "_filters: {},",
        "registerPartial: " + registerPartial + ",",
        "registerFilter: " + registerFilter + ",",
        "render: function(data) {",
          "return " + this.func + "(data, this._partials, this._filters)",
        "}",
      "}"
    ].join("\n");
  }

  /**
   * A recursively called method to detect how to compile each Node in the
   * Tree.
   *
   * @memberOf module:compiler.Compiler
   * @param {array} nodes - An Array of Tree nodes to process.
   * @param {array} keyVal - An optional array of meta condition keys.
   * @return {string} Joined compiled nodes representing the template body.
   */
  Compiler.prototype.process = function(nodes, keyVal) {
    var commands = [];
    var length = nodes.length - 1;

    // Parse the Tree and execute the respective compile to JavaScript method.
    nodes.map(function(node, index) {
      switch (node.type) {
        case "RawProperty": {
          commands.push(this.compileProperty(node, false));
          break;
        }

        case "Property": {
          commands.push(this.compileProperty(node, true));
          break;
        }

        case "ConditionalExpression": {
          commands.push(this.compileConditional(node));
          break;
        }

        case "LoopExpression": {
          commands.push(this.compileLoop(node));
          break;
        }

        case "PartialExpression": {
          commands.push(this.compilePartial(node));
          break;
        }

        case "ExtendExpression": {
          commands.push(this.compileExtend(node));
          break;
        }

        default: {
          // Special work necessary to handle new lines within loops.
          var indexOf = node.value.indexOf("\n");
          var escaped = escapeValue(node.value);
          var trimmed = escapeValue(node.value.trim());
          var hasNew = keyVal && indexOf > -1;
          var position = keyVal && "data['" + keyVal[0] + "']";

          // If there is a new line and this is the first element in the list
          // we should remove it as it's not actually part of the loop body.
          if (hasNew && index === 0) {
            commands.push([
              "(",
                position, "===", 0,
                "?", "'", trimmed, "'",
                ":", "'", escaped,
              "')"
            ].join(""));
          }
          // If we are at the last element in the loop, trim all excess
          // whitespace.
          else if (hasNew && index === length) {
            commands.push([
              "(",
                position, "===", length,
                "?", "'", trimmed, "'",
                ":", "'", trimmed,
              "')"
            ].join(""));
          }
          // Otherwise, simply pass through the escaped value.
          else {
            commands.push("'" + escaped + "'");
          }
        }
      }
    }, this);

    return commands.join("+");
  };

  /**
   * Compiles a property into JavaScript.
   *
   * @memberOf module:compiler.Compiler
   * @param {object} node - The property node to compile.
   * @return {string} The compiled JavaScript source string value.
   */
  Compiler.prototype.compileProperty = function(node, encode) {
    var identifier = node.value;

    // Normalize string property values that contain single or double quotes.
    if (identifier.indexOf("'") === -1 && identifier.indexOf("\"") === -1) {
      identifier = normalizeIdentifier(node.value);
    }

    // Build the initial identifier value check.
    var value = [
      "(",
        // If the identifier is a function, then invoke, otherwise return
        // identifier.
        "typeof", identifier, "===", "'function'",
          "?", encode ? "encode(" + identifier + "(" + node.args + "))" :
            identifier + "()",
          ":", encode ? "encode(" + identifier + ")" : identifier,
      ")"
    ].join(" ");

    // Find any filters and nest them.
    value = node.filters.reduce(function(memo, filter) {
      var args = filter.args.length ? ", " + filter.args.map(function(value) {
        if (value.indexOf("'") === -1 && value.indexOf("\"") === -1) {
          if (!Number(value)) {
            return normalizeIdentifier(value);
          }
        }

        return value;
      }).join(", ") : "";
      return "filters['" + filter.value + "']" + "(" + memo + args + ")";
    }, value);

    return value;
  };

  /**
   * Compiles a conditional into JavaScript.
   *
   * @memberOf module:compiler.Compiler
   * @param {object} node - The conditional node to compile.
   * @return {string} The compiled JavaScript source string value.
   */
  Compiler.prototype.compileConditional = function(node) {
    if (node.conditions.length === 0) {
      throw new Error("Missing conditions to if statement.");
    }

    var condition = node.conditions.map(function(condition) {
      switch (condition.type) {
        case "Identifier": {
          return normalizeIdentifier(condition.value);
        }

        case "Not": {
          return "!";
        }

        case "Literal": {
          return condition.value;
        }

        case "Equality": {
          return condition.value;
        }
      }
    }).join("");

    // If an else was provided, hook into it.
    var els = node.els ? this.process(node.els.nodes) : null;

    // If an elsif was provided, hook into it.
    var elsif = node.elsif ? this.compileConditional(node.elsif) : null;

    return [
      "(", "(", condition, ")", "?", this.process(node.nodes), ":",

      els || elsif || "''",

      ")"
    ].join("");
  };

  /**
   * Compiles a loop into JavaScript.
   *
   * @memberOf module:compiler.Compiler
   * @param {object} node - The loop node to compile.
   * @return {string} The compiled JavaScript source string value.
   */
  Compiler.prototype.compileLoop = function(node) {
    var conditions = node.conditions;

    var keyVal = [
      // Key
      (conditions[3] ? conditions[3].value : "i"),

      // Value.
      (conditions[2] ? conditions[2].value : ".")
    ];

    // Normalize the value to the condition if it exists.
    var value = conditions.length && conditions[0].value;

    // Construct the loop, utilizing map because it will return back the
    // template as an array and ready to join into the template.
    var loop = [
      "map(", value ? normalizeIdentifier(value) : "data", ",",

        // Index keyword.
        "'", keyVal[0], "'", ",",

        // Value keyword.
        "'", value ? keyVal[1] : "", "'", ",",

        // Outer scope data object.
        "data", ",",

        // The iterator function.
        "function(data) {",
          "return " + this.process(node.nodes, keyVal),
        "}",
      ").join('')"
    ].join("");

    return loop;
  };

  /**
   * Compiles a partial into JavaScript.
   *
   * @memberOf module:compiler.Compiler
   * @param {object} node - The partial node to compile.
   * @return {string} The compiled JavaScript source string value.
   */
  Compiler.prototype.compilePartial = function(node) {
    return [
      "(",
        "partials['" + node.value + "'].render(",
          node.args.length ? normalizeIdentifier(node.args[0]) : node.data,
        ")",
      ")"
    ].join("");
  };

  /**
   * Compiles a render into JavaScript.
   *
   * @memberOf module:compiler.Compiler
   * @param {object} node - The partial node to compile.
   * @return {string} The compiled JavaScript source string value.
   */
  Compiler.prototype.compileExtend = function(node) {
    return [
      "(",
        // Register this template as the child partial of the parent.
        "partials['", node.value.template.trim(), "'].registerPartial",
          "('", node.value.partial.trim(), "', { render: function(_data) {",
            "data = _data || data;",
            "return ", this.process(node.nodes), ";",
          "}, data: data}),",
        // Invoke the parent template with the passed data.
        "partials['", node.value.template.trim(), "'].render(data)",
      ")"
    ].join("");
  };

  module.exports = Compiler;
});