redeyed.js 8.9 KB
;(function () {

'use strict';
/*jshint laxbreak: true,  browser:true */
/*global define*/

var esprima
  , exportFn
  , toString = Object.prototype.toString
  ;

if (typeof module === 'object' && typeof module.exports === 'object' && typeof require === 'function') {
  // server side
  esprima = require('esprima');
  exportFn = function (redeyed) { module.exports = redeyed; };
  bootstrap(esprima, exportFn);
} else if (typeof define === 'function' && define.amd) {
  // client side
  // amd
  define(['esprima'], function (esprima) {
      return bootstrap(esprima);
  });
} else if (typeof window === 'object') {
  // no amd -> attach to window if it exists
  // Note that this requires 'esprima' to be defined on the window, so that script has to be loaded first
  window.redeyed = bootstrap(window.esprima);
}

function bootstrap(esprima, exportFn) {

  function isFunction (obj) {
    return toString.call(obj) === '[object Function]';
  }

  function isString (obj) {
    return toString.call(obj) === '[object String]';
  }

  function isNumber (obj) {
    return toString.call(obj) === '[object Number]';
  }

  function isObject (obj) {
    return toString.call(obj) === '[object Object]';
  }

  function surroundWith (before, after) {
    return function (s) { return before + s + after; };
  }

  function isNonCircular(key) { 
    return key !== '_parent'; 
  }

  function objectizeString (value) {
    var vals = value.split(':');

    if (0 === vals.length || vals.length > 2) 
      throw new Error(
        'illegal string config: ' + value +
        '\nShould be of format "before:after"'
      );

    if (vals.length === 1 || vals[1].length === 0) {
      return vals.indexOf(':') < 0 ? { _before: vals[0] } : { _after: vals[0] };
    } else {
      return { _before: vals[0], _after: vals[1] };
    }
  }

  function objectize (node) {

    // Converts 'bef:aft' to { _before: bef, _after: aft } 
    // and resolves undefined before/after from parent or root

    function resolve (value, key) {
      // resolve before/after from root or parent if it isn't present on the current node
      if (!value._parent) return undefined;
      
      // Immediate parent
      if (value._parent._default && value._parent._default[key]) return value._parent._default[key];

      // Root
      var root = value._parent._parent;
      if (!root) return undefined;

      return root._default ? root._default[key] : undefined;
    }

    function process (key) {
      var value = node[key];

      if (!value) return;
      if (isFunction(value)) return;

      // normalize all strings to objects
      if (isString(value)) {
        node[key] = value = objectizeString(value);
      }
      
      value._parent = node;
      if (isObject(value)) {
        if (!value._before && !value._after) return objectize (value);

        // resolve missing _before or _after from parent(s) 
        // in case we only have either one on this node
        value._before =  value._before || resolve(value, '_before');
        value._after  =  value._after  || resolve(value, '_after');
        
        return;
      } 

      throw new Error('nodes need to be either {String}, {Object} or {Function}.' + value + ' is neither.');
    }

    // Process _default ones first so children can resolve missing before/after from them
    if (node._default) process('_default');

    Object.keys(node)
      .filter(function (key) {
        return isNonCircular(key) 
          && node.hasOwnProperty(key)
          && key !== '_before' 
          && key !== '_after' 
          && key !== '_default';
      })
      .forEach(process);
  }

  function functionize (node) {
    Object.keys(node)
      .filter(function (key) { 
        return isNonCircular(key) && node.hasOwnProperty(key);
      })
      .forEach(function (key) {
        var value = node[key];

        if (isFunction(value)) return;

        if (isObject(value)) {

          if (!value._before && !value._after) return functionize(value);

          // at this point before/after were "inherited" from the parent or root
          // (see objectize)
          var before = value._before || '';
          var after = value._after || '';

          node[key] = surroundWith (before, after);
          return node[key];
        }
      });
  }

  function normalize (root) {
    objectize(root);
    functionize(root);
  }

  function mergeTokensAndComments(tokens, comments) {
    var all = {};

    function addToAllByRangeStart(t) { all[ t.range[0] ] = t; }

    tokens.forEach(addToAllByRangeStart);
    comments.forEach(addToAllByRangeStart);

    // keys are sorted automatically
    return Object.keys(all)
      .map(function (k) { return all[k]; });
  }

  function redeyed (code, config, opts) {
    opts = opts || {};
    var parser = opts.parser || esprima;
    var buildAst = !!opts.buildAst;

    var hashbang =  ''
      , ast
      , tokens
      , comments
      , lastSplitEnd = 0
      , splits = []
      , transformedCode
      , all
      , info
      ;

    // Replace hashbang line with empty whitespaces to preserve token locations
    if (code[0] === '#' && code[1] === '!') {
      hashbang = code.substr(0, code.indexOf('\n') + 1);
      code = Array.apply(0, Array(hashbang.length)).join(' ') + '\n' + code.substr(hashbang.length);
    }

    if (buildAst) {
      ast = parser.parse(code, { tokens: true, comment: true, range: true, tolerant: true });
      tokens = ast.tokens;
      comments = ast.comments;
    } else {
      tokens = [];
      comments = [];
      parser.tokenize(code, { range: true, comment: true }, function (token) {
        if (token.type === 'LineComment') {
          token.type = 'Line';
          comments.push(token)
        } else if (token.type === 'BlockComment') {
          token.type = 'Block';
          comments.push(token)
        } else {
          // Optimistically upgrade 'static' to a keyword
          if (token.type === 'Identifier' && token.value === 'static') token.type = 'Keyword';
          tokens.push(token);
        }
      });
    }

    normalize(config);

    function tokenIndex(tokens, tkn, start) {
      var current
        , rangeStart = tkn.range[0];

      for (current = start; current < tokens.length; current++) {
        if (tokens[current].range[0] === rangeStart) return current;
      }

      throw new Error('Token %s not found at or after index: %d', tkn, start);
    }

    function process(surround) {
      var result
        , currentIndex
        , nextIndex
        , skip = 0
        , splitEnd
        ;

      result = surround(code.slice(start, end), info);
      if (isObject(result)) {
        splits.push(result.replacement);

        currentIndex =  info.tokenIndex;
        nextIndex    =  tokenIndex(info.tokens, result.skipPastToken, currentIndex);
        skip         =  nextIndex - currentIndex;
        splitEnd     =  skip > 0 ? tokens[nextIndex - 1].range[1] : end;
      } else {
        splits.push(result);
        splitEnd = end;
      }

      return { skip: skip, splitEnd: splitEnd  };
    }

    function addSplit (start, end, surround, info) {
      var result
        , nextIndex
        , skip = 0
        ;

      if (start >= end) return;
      if (surround) {
        result       =  process(surround);
        skip         =  result.skip;
        lastSplitEnd =  result.splitEnd;
      } else {
        splits.push(code.slice(start, end));
        lastSplitEnd = end;
      }

      return skip;
    }

    all = mergeTokensAndComments(tokens, comments);
    for (var tokenIdx = 0; tokenIdx < all.length; tokenIdx++) {
      var token = all[tokenIdx]
        , surroundForType = config[token.type]
        , surround
        , start
        , end;
      
      // At least the type (e.g., 'Keyword') needs to be specified for the token to be surrounded
      if (surroundForType) {

        // root defaults are only taken into account while resolving before/after otherwise
        // a root default would apply to everything, even if no type default was specified
        surround = surroundForType 
          && surroundForType.hasOwnProperty(token.value) 
          && surroundForType[token.value]
          && isFunction(surroundForType[token.value])
            ? surroundForType[token.value] 
            : surroundForType._default;

        start = token.range[0];
        end = token.range[1];

        addSplit(lastSplitEnd, start);
        info = { tokenIndex: tokenIdx, tokens: all, ast: ast, code: code };
        tokenIdx += addSplit(start, end, surround, info);
      }
    }

    if (lastSplitEnd < code.length) {
      addSplit(lastSplitEnd, code.length);
    }

  if (!opts.nojoin) {
    transformedCode = splits.join('');
    if (hashbang.length > 0) {
      transformedCode = hashbang + transformedCode.substr(hashbang.length);
    }
  }

    return {
        ast      :  ast
      , tokens   :  tokens
      , comments :  comments
      , splits   :  splits
      , code     :  transformedCode
    };
  }

  return exportFn ? exportFn(redeyed) : redeyed;
}
})();