Source: page.js

"use strict";

/** @fileOverview Implementation of the *page* object-oriented parser generator system.
  * @author Axel T. Schreiner <ats@cs.rit.edu>
  * @version 1.5.1
  */

/** Object-oriented parser generator system supporting BNF and EBNF grammar
  * representations and tokenizer, LL(1), and naive LR(1) parser generation.
  * @namespace
  */ 
var page = {
  
  /** Used for type-checking and other assertion checking.
    * Template method, could be overwritten in any baseclass.
    *
    * @param {string} _marker to identify position across a re-throw.
    * @param {boolean} [_condition] should be true.
    *
    * @throws {Error} if a condition is not present or not met.
    *
    * @see page.baseclass
    */
  assert: function (_marker, _condition) {
    if (!_condition) throw new Error('assert: ' + _marker);
  },
  
  /** Prefixes message with `error:` and delegates to {@link page.message}.
    * Increments `this.errors` if it exists.
    * Template method, could be overwritten in any baseclass.
    *
    * @param {string} arguments message strings, will be joined with blanks.
    * 
    * @example
    * aBNF.error('example', 'message')
    *
    * @see page.baseclass
    */
  error: function () {
    if (this && 'errors' in this) ++ this.errors;
    page.message('error: ' + [].slice.call(arguments).join(' '));
  },
  
  /** Prints a message.
    * Template method, could be overwritten in any baseclass.
    *
    * @param {string} arguments message strings, will be joined with blanks.
    *
    * @see page.baseclass
    */
  message: function () {
    page.assert('__WHERE__', arguments.length > 0);

    print([].slice.call(arguments).join(' '));
  },
  
  /** Provides `assert`, `error`, `message`, and `subclass` methods and `className` property for a new class.
    *
    * @param {Function} _constructor new class' constructor function.
    * @param {string} _className new class' name.
    *
    * @example
    * page.baseclass(page.BNF, 'page.BNF');
    *
    * @see page.assert
    * @see page.error
    * @see page.message
    * @see page.subclass
    */
  baseclass: function (_constructor, _className) {
    page.assert('__WHERE__', typeof _constructor == 'function');
    page.assert('__WHERE__', typeof _className == 'string');
    
    _constructor.prototype.assert = page.assert;
    _constructor.prototype.error = page.error;
    _constructor.prototype.message = page.message;
    _constructor.prototype.subclass = page.subclass;
    
    _constructor.prototype.className = _className;
  },

  /** Connects subclass to superclass. 
    * Arranges for inheritance, `className`, and `instanceof`.
    * Template method, could be overwritten in any baseclass.
    * Taken from [here](http://www.golimojo.com/etc/js-subclass.html).
    *
    * Note that there can't be a method similar to `super` in Java
    * because `super` is implemented with a lexical search above the
    * current class — it can't be applied to `this`.
    *
    * @param {Function} _constructor subclass' constructor function.
    * @param {string} _className new subclass' name.
    * @param {Function} _superclass superclass' constructor function.
    *
    * @example
    * page.subclass(page.BNF.Lit, 'page.BNF.Lit', page.BNF.T);
    */
  subclass: function (_constructor, _className, _superclass) {
    page.assert('__WHERE__', typeof _constructor == 'function');
    page.assert('__WHERE__', typeof _className == 'string');
    
  	function surrogateConstructor () { } // placeholder for _superclass
  	surrogateConstructor.prototype = _superclass.prototype; // inherit from _superclass

  	var prototypeObject = new surrogateConstructor(); // replacement for subclass' prototype
  	prototypeObject.constructor = _constructor;       // attach subclass' constructor
  	prototypeObject.className = _className;           // attach subclass' class name

  	_constructor.prototype = prototypeObject;         // replace subclass' prototype
  },
  
  /** Escapes non-ASCII and invisible characters using backslash.
    *
    * @param {string} _s string to escape.
    *
    * @returns {string} single-quoted, escaped string.
    */
  escape: function (_s) {
    if (_s == null) return '';
  
    page.assert('__WHERE__', typeof _s == 'string');
  
    var result = '\'';
  
    for (var i = 0; i < _s.length; ++ i) {
      var c = _s.charAt(i);
      var cc = '\b\f\n\r\t\v\\\''.indexOf(c);
      if (cc >= 0)
        result += '\\' + 'bfnrtv\\\''.charAt(cc);
      else if (c >= ' ' && c <= '~')
        result += c;
      else if ((cc = _s.charCodeAt(i)) < 16)
        result += '\\x0' + cc.toString(16);
      else if (cc < 256)
        result += '\\x' + cc.toString(16);
      else if (cc < 16 * 256)
        result += '\\u0' + cc.toString(16);
      else
        result += '\\u' + cc.toString(16);
    }
    return result + '\'';
  },

  /** Unescapes the result of {@link page.escape} and removes leading and trailing delimiter character.
    *
    * @param {string} _s string to unescape.
    *
    * @returns {string} unquoted, unescaped string.
    */
  unescape: function (_s) {
    page.assert('__WHERE__', typeof _s == 'string');
    page.assert('__WHERE__', _s.length >= 2);
    page.assert('__WHERE__', _s.charAt(0) == _s.charAt(_s.length-1));

    var result = '';
    var c;

    for (var i = 1; i < _s.length-1; )
      if ((c = _s.charAt(i++)) != '\\')
        result += c;
      
      else if (i >= _s.length-1)
        result += '\\'; // trailing backslash in literal
      
      else if ((c = 'bfnrtv\\\''.indexOf(_s.charAt(i++))) >= 0)
        result += '\b\f\n\r\t\v\\\''.charAt(c);
    
      else switch (c = _s.charAt(i-1)) {
      case 'x':
        if (i + 1 < _s.length-1 &&
            '0123456789abcdef'.indexOf(_s.charAt(i)) >= 0 && 
            '0123456789abcdef'.indexOf(_s.charAt(i+1)) >= 0) {
          result += String.fromCharCode(parseInt(_s.substr(i, 2), 16));
          i += 2;
        } else
          result += 'x'; // bad \x
        break;
          
      case 'u':
        if (i + 3 < _s.length-1 &&
            '0123456789abcdef'.indexOf(_s.charAt(i)) >= 0 && 
            '0123456789abcdef'.indexOf(_s.charAt(i+1)) >= 0 &&
            '0123456789abcdef'.indexOf(_s.charAt(i+2)) >= 0 &&
            '0123456789abcdef'.indexOf(_s.charAt(i+3)) >= 0) {
          result += String.fromCharCode(parseInt(_s.substr(i, 4), 16));
          i += 4;
        } else
          result += 'u'; // bad \u
        break;
      
      default: // bad \
        result += c;
      }

    return result;
  },
  
  /** Rescursively displays a recursive `Array` or `object`
    * aggregate containing boolean, number, or string values as leaves.
    * This is similar to `JSON.stringify` but perhaps prettier.
    *
    * @param {object} _o object to display.
    * @param {string} [_indent] indentation prefix.
    * 
    * @returns {string}
    *   
    * @example
    * // the operation can be reversed
    * eval('obj = '+page.uneval(obj))
    */
  uneval: function (_o, _indent) {
    page.assert('__WHERE__', _o instanceof Array || typeof _o == 'object');
    page.assert('__WHERE__', !_indent || typeof _indent == 'string');
  
    if (!_indent) _indent = '';
    var delim = ' ';

    if (_o instanceof Array) {
      var s = _indent + '[';
    
      for (var i in _o)
        switch (typeof _o[i]) {
        case 'boolean':
        case 'number':
          s += delim + _o[i];
          delim = ',\n  ' + _indent;
          continue;
      
        case 'string':
          s += delim + page.escape(_o[i]);
          delim = ',\n  ' + _indent;
          continue;
    
        case 'object':
          if (delim.length > 1) s += ',';
          s += '\n' + page.uneval(_o[i], _indent + '  ');
          delim = ',\n  ' + _indent;
        }
    
      return s + ' ]';
    }
    
    var s = _indent + '{';
  
    for (var i in _o)
      switch (typeof _o[i]) {
      case 'boolean':
      case 'number':
        s += delim + i + ': ' + _o[i];
        delim = ',\n  ' + _indent;
        continue;
      
      case 'string':
        s += delim + i + ': ' + page.escape(_o[i]);
        delim = ',\n  ' + _indent;
        continue;
    
      case 'object':
        s += delim + i + ':\n' + page.uneval(_o[i], _indent + '    ');
        delim = ',\n  ' + _indent;
        continue;
      }
    
    return s + ' }';
  },

  /** Hashtable with strings or numbers as keys.
    *
    * @typedef Map
    * @type object
    *
    * @see page.overlap
    */
  /** Compares two maps, i.e., checks if two objects contain properties with the same key.
    *
    * @param {Map} _a to check.
    * @param {Map} _b to check.
    *
    * @returns {boolean} `true` if there are common property keys.
    */
  overlap: function (_a, _b) {
    for (var a in _a) if (a in _b) return true;
    return false;
  },
  
  /** Adds one map to another.
    *
    * @param {Map} _a to add to.
    * @param {Map} _b to add.
    *
    * @returns {boolean} `true` if one or more keys were added.
    */
  merge: function (_a, _b) {
    var result = false;
    for (var b in _b)
      if (b in _a) {} else {
        result = true;
        _a[b] = _b[b];
      }
    return result;
  },
  
  /** Creates a function which can observe any parsing function and
    * display the messages after it passes them through to another
    * observer, if any. It will return the value from the observer,
    * the information for an `error` action, or `null`.
    * The function is serially reusable; when
    * first accessed it will print a line to label the output columns.
    *
    * @param {Observer} [_observe] next observer function to delegate to, if any.
    * @param {RegExp} [_match] if specified, selects the actions to be printed.
    *
    * @returns {Observer} a function which can observe a parsing algorithm.
    *
    * @see page.LL1#parse
    * @see page.LL1#parser
    * @see page.SLR1#parse
    */
  trace: function (_observe, _match) {
    if (!_match && _observe instanceof RegExp) {
      _match = _observe;
      _observe = null;
    
    } else {
      !_observe || page.assert('__WHERE__', typeof _observe == 'function');
      !_match || page.assert('__WHERE__', _match instanceof RegExp);
    }
    
    var first = true;

    // closure
    return function (_at, _tuple, _action, _info, _sender) {
      page.assert('__WHERE__', typeof _action == 'string');
      !_info || page.assert('__WHERE__', typeof _info == 'string' || typeof _info == 'number'
                                        || _info instanceof page.BNF.Rule || _info instanceof page.BNF.T);

      if (first) {
        print('\nAT  TUPLE            ACTION                          RETURN');
        first = false;
      }

      var result = _observe ? _observe(_at, _tuple, _action, _info, _sender) : (_action == 'error' ? _info : null);
  
      if (!_match || _match.test(_action)) {
        var w = (_at             + '    ')                                     .substr(0, 4);
        var t = (_tuple + '               ')                                   .substr(0, 15);
        var a = (_action         + '      ')                                   .substr(0, 6);
        var i = (String(_info).replace(/\n.*/, '') + '                       ').substr(0, 23);
        var r = (result == null ? 'null' :
                 result instanceof Array ? '[ ] ' + result :
                 (typeof result).substr(0, 3) +' '+ result).replace(/\n.*/, '');
        if (r.length > 23) r = r.substr(0, 23);

        print(w + t +'  '+ a +' '+ i +'  '+ r);
      }
      
      return result;
    };
  }
};