Commits

Chris Leonello committed eefe680

directory reorg

Comments (0)

Files changed (21)

extras/beautify/HTML-Beautify.js

+/*
+
+ Style HTML
+---------------
+
+  Written by Nochum Sossonko, (nsossonko@hotmail.com)
+
+  Based on code initially developed by: Einar Lielmanis, <elfz@laacz.lv>
+    http://jsbeautifier.org
+
+
+  You are free to use this in any way you want, in case you find this useful or working for you.
+
+  Usage:
+    style_html(html_source);
+
+*/
+
+function style_html(html_source, indent_size, indent_character, max_char) {
+//Wrapper function to invoke all the necessary constructors and deal with the output.
+
+  var Parser, multi_parser;
+
+  function Parser() {
+
+    this.pos = 0; //Parser position
+    this.token = '';
+    this.current_mode = 'CONTENT'; //reflects the current Parser mode: TAG/CONTENT
+    this.tags = { //An object to hold tags, their position, and their parent-tags, initiated with default values
+      parent: 'parent1',
+      parentcount: 1,
+      parent1: ''
+    };
+    this.tag_type = '';
+    this.token_text = this.last_token = this.last_text = this.token_type = '';
+
+
+    this.Utils = { //Uilities made available to the various functions
+      whitespace: "\n\r\t ".split(''),
+      single_token: 'br,input,link,meta,!doctype,basefont,base,area,hr,wbr,param,img,isindex,?xml,embed'.split(','), //all the single tags for HTML
+      extra_liners: 'head,body,/html'.split(','), //for tags that need a line of whitespace before them
+      in_array: function (what, arr) {
+        for (var i=0; i<arr.length; i++) {
+          if (what === arr[i]) {
+            return true;
+          }
+        }
+        return false;
+      }
+    }
+
+    this.get_content = function () { //function to capture regular content between tags
+
+      var input_char = '';
+      var content = [];
+      var space = false; //if a space is needed
+      while (this.input.charAt(this.pos) !== '<') {
+        if (this.pos >= this.input.length) {
+          return content.length?content.join(''):['', 'TK_EOF'];
+        }
+
+        input_char = this.input.charAt(this.pos);
+        this.pos++;
+        this.line_char_count++;
+
+
+        if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+          if (content.length) {
+            space = true;
+          }
+          this.line_char_count--;
+          continue; //don't want to insert unnecessary space
+        }
+        else if (space) {
+          if (this.line_char_count >= this.max_char) { //insert a line when the max_char is reached
+            content.push('\n');
+            for (var i=0; i<this.indent_level; i++) {
+              content.push(this.indent_string);
+            }
+            this.line_char_count = 0;
+          }
+          else{
+            content.push(' ');
+            this.line_char_count++;
+          }
+          space = false;
+        }
+        content.push(input_char); //letter at-a-time (or string) inserted to an array
+      }
+      return content.length?content.join(''):'';
+    }
+
+    this.get_script = function () { //get the full content of a script to pass to js_beautify
+
+      var input_char = '';
+      var content = [];
+      var reg_match = new RegExp('\<\/script' + '\>', 'igm');
+      reg_match.lastIndex = this.pos;
+      var reg_array = reg_match.exec(this.input);
+      var end_script = reg_array?reg_array.index:this.input.length; //absolute end of script
+      while(this.pos < end_script) { //get everything in between the script tags
+        if (this.pos >= this.input.length) {
+          return content.length?content.join(''):['', 'TK_EOF'];
+        }
+
+        input_char = this.input.charAt(this.pos);
+        this.pos++;
+
+
+        content.push(input_char);
+      }
+      return content.length?content.join(''):''; //we might not have any content at all
+    }
+
+    this.record_tag = function (tag){ //function to record a tag and its parent in this.tags Object
+      if (this.tags[tag + 'count']) { //check for the existence of this tag type
+        this.tags[tag + 'count']++;
+        this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+      }
+      else { //otherwise initialize this tag type
+        this.tags[tag + 'count'] = 1;
+        this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+      }
+      this.tags[tag + this.tags[tag + 'count'] + 'parent'] = this.tags.parent; //set the parent (i.e. in the case of a div this.tags.div1parent)
+      this.tags.parent = tag + this.tags[tag + 'count']; //and make this the current parent (i.e. in the case of a div 'div1')
+    }
+
+    this.retrieve_tag = function (tag) { //function to retrieve the opening tag to the corresponding closer
+      if (this.tags[tag + 'count']) { //if the openener is not in the Object we ignore it
+        var temp_parent = this.tags.parent; //check to see if it's a closable tag.
+        while (temp_parent) { //till we reach '' (the initial value);
+          if (tag + this.tags[tag + 'count'] === temp_parent) { //if this is it use it
+            break;
+          }
+          temp_parent = this.tags[temp_parent + 'parent']; //otherwise keep on climbing up the DOM Tree
+        }
+        if (temp_parent) { //if we caught something
+          this.indent_level = this.tags[tag + this.tags[tag + 'count']]; //set the indent_level accordingly
+          this.tags.parent = this.tags[temp_parent + 'parent']; //and set the current parent
+        }
+        delete this.tags[tag + this.tags[tag + 'count'] + 'parent']; //delete the closed tags parent reference...
+        delete this.tags[tag + this.tags[tag + 'count']]; //...and the tag itself
+        if (this.tags[tag + 'count'] == 1) {
+          delete this.tags[tag + 'count'];
+        }
+        else {
+          this.tags[tag + 'count']--;
+        }
+      }
+    }
+
+    this.get_tag = function () { //function to get a full tag and parse its type
+      var input_char = '';
+      var content = [];
+      var space = false;
+
+      do {
+        if (this.pos >= this.input.length) {
+          return content.length?content.join(''):['', 'TK_EOF'];
+        }
+
+        input_char = this.input.charAt(this.pos);
+        this.pos++;
+        this.line_char_count++;
+
+        if (this.Utils.in_array(input_char, this.Utils.whitespace)) { //don't want to insert unnecessary space
+          space = true;
+          this.line_char_count--;
+          continue;
+        }
+
+        if (input_char === "'" || input_char === '"') {
+          if (!content[1] || content[1] !== '!') { //if we're in a comment strings don't get treated specially
+            input_char += this.get_unformatted(input_char);
+            space = true;
+          }
+        }
+
+        if (input_char === '=') { //no space before =
+          space = false;
+        }
+
+        if (content.length && content[content.length-1] !== '=' && input_char !== '>'
+            && space) { //no space after = or before >
+          if (this.line_char_count >= this.max_char) {
+            this.print_newline(false, content);
+            this.line_char_count = 0;
+          }
+          else {
+            content.push(' ');
+            this.line_char_count++;
+          }
+          space = false;
+        }
+        content.push(input_char); //inserts character at-a-time (or string)
+      } while (input_char !== '>');
+
+      var tag_complete = content.join('');
+      var tag_index;
+      if (tag_complete.indexOf(' ') != -1) { //if there's whitespace, thats where the tag name ends
+        tag_index = tag_complete.indexOf(' ');
+      }
+      else { //otherwise go with the tag ending
+        tag_index = tag_complete.indexOf('>');
+      }
+      var tag_check = tag_complete.substring(1, tag_index).toLowerCase();
+      if (tag_complete.charAt(tag_complete.length-2) === '/' ||
+          this.Utils.in_array(tag_check, this.Utils.single_token)) { //if this tag name is a single tag type (either in the list or has a closing /)
+        this.tag_type = 'SINGLE';
+      }
+      else if (tag_check === 'script') { //for later script handling
+        this.record_tag(tag_check);
+        this.tag_type = 'SCRIPT';
+      }
+      else if (tag_check === 'style') { //for future style handling (for now it justs uses get_content)
+        this.record_tag(tag_check);
+        this.tag_type = 'STYLE';
+      }
+      else if (tag_check.charAt(0) === '!') { //peek for <!-- comment
+        if (tag_check.indexOf('[if') != -1) { //peek for <!--[if conditional comment
+          if (tag_complete.indexOf('!IE') != -1) { //this type needs a closing --> so...
+            var comment = this.get_unformatted('-->', tag_complete); //...delegate to get_unformatted
+            content.push(comment);
+          }
+          this.tag_type = 'START';
+        }
+        else if (tag_check.indexOf('[endif') != -1) {//peek for <!--[endif end conditional comment
+          this.tag_type = 'END';
+          this.unindent();
+        }
+        else if (tag_check.indexOf('[cdata[') != -1) { //if it's a <[cdata[ comment...
+          var comment = this.get_unformatted(']]>', tag_complete); //...delegate to get_unformatted function
+          content.push(comment);
+          this.tag_type = 'SINGLE'; //<![CDATA[ comments are treated like single tags
+        }
+        else {
+          var comment = this.get_unformatted('-->', tag_complete);
+          content.push(comment);
+          this.tag_type = 'SINGLE';
+        }
+      }
+      else {
+        if (tag_check.charAt(0) === '/') { //this tag is a double tag so check for tag-ending
+          this.retrieve_tag(tag_check.substring(1)); //remove it and all ancestors
+          this.tag_type = 'END';
+        }
+        else { //otherwise it's a start-tag
+          this.record_tag(tag_check); //push it on the tag stack
+          this.tag_type = 'START';
+        }
+        if (this.Utils.in_array(tag_check, this.Utils.extra_liners)) { //check if this double needs an extra line
+          this.print_newline(true, this.output);
+        }
+      }
+      return content.join(''); //returns fully formatted tag
+    }
+
+    this.get_unformatted = function (delimiter, orig_tag) { //function to return unformatted content in its entirety
+
+      if (orig_tag && orig_tag.indexOf(delimiter) != -1) {
+        return '';
+      }
+      var input_char = '';
+      var content = '';
+      var space = true;
+      do {
+
+
+        input_char = this.input.charAt(this.pos);
+        this.pos++
+
+        if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+          if (!space) {
+            this.line_char_count--;
+            continue;
+          }
+          if (input_char === '\n' || input_char === '\r') {
+            content += '\n';
+            for (var i=0; i<this.indent_level; i++) {
+              content += this.indent_string;
+            }
+            space = false; //...and make sure other indentation is erased
+            this.line_char_count = 0;
+            continue;
+          }
+        }
+        content += input_char;
+        this.line_char_count++;
+        space = true;
+
+
+      } while (content.indexOf(delimiter) == -1);
+      return content;
+    }
+
+    this.get_token = function () { //initial handler for token-retrieval
+      var token;
+
+      if (this.last_token === 'TK_TAG_SCRIPT') { //check if we need to format javascript
+        var temp_token = this.get_script();
+        if (typeof temp_token !== 'string') {
+          return temp_token;
+        }
+        token = js_beautify(temp_token,
+                {indent_size: this.indent_size, indent_char: this.indent_character, indent_level: this.indent_level}); //call the JS Beautifier
+        return [token, 'TK_CONTENT'];
+      }
+      if (this.current_mode === 'CONTENT') {
+        token = this.get_content();
+        if (typeof token !== 'string') {
+          return token;
+        }
+        else {
+          return [token, 'TK_CONTENT'];
+        }
+      }
+
+      if(this.current_mode === 'TAG') {
+        token = this.get_tag();
+        if (typeof token !== 'string') {
+          return token;
+        }
+        else {
+          var tag_name_type = 'TK_TAG_' + this.tag_type;
+          return [token, tag_name_type];
+        }
+      }
+    }
+
+    this.printer = function (js_source, indent_character, indent_size, max_char) { //handles input/output and some other printing functions
+
+      this.input = js_source || ''; //gets the input for the Parser
+      this.output = [];
+      this.indent_character = indent_character || ' ';
+      this.indent_string = '';
+      this.indent_size = indent_size || 2;
+      this.indent_level = 0;
+      this.max_char = max_char || 70; //maximum amount of characters per line
+      this.line_char_count = 0; //count to see if max_char was exceeded
+
+      for (var i=0; i<this.indent_size; i++) {
+        this.indent_string += this.indent_character;
+      }
+
+      this.print_newline = function (ignore, arr) {
+        this.line_char_count = 0;
+        if (!arr || !arr.length) {
+          return;
+        }
+        if (!ignore) { //we might want the extra line
+          while (this.Utils.in_array(arr[arr.length-1], this.Utils.whitespace)) {
+            arr.pop();
+          }
+        }
+        arr.push('\n');
+        for (var i=0; i<this.indent_level; i++) {
+          arr.push(this.indent_string);
+        }
+      }
+
+
+      this.print_token = function (text) {
+        this.output.push(text);
+      }
+
+      this.indent = function () {
+        this.indent_level++;
+      }
+
+      this.unindent = function () {
+        if (this.indent_level > 0) {
+          this.indent_level--;
+        }
+      }
+    }
+    return this;
+  }
+
+  /*_____________________--------------------_____________________*/
+
+
+
+  multi_parser = new Parser(); //wrapping functions Parser
+  multi_parser.printer(html_source, indent_character, indent_size); //initialize starting values
+
+
+
+  while (true) {
+      var t = multi_parser.get_token();
+      multi_parser.token_text = t[0];
+      multi_parser.token_type = t[1];
+
+    if (multi_parser.token_type === 'TK_EOF') {
+      break;
+    }
+
+
+    switch (multi_parser.token_type) {
+      case 'TK_TAG_START': case 'TK_TAG_SCRIPT': case 'TK_TAG_STYLE':
+        multi_parser.print_newline(false, multi_parser.output);
+        multi_parser.print_token(multi_parser.token_text);
+        multi_parser.indent();
+        multi_parser.current_mode = 'CONTENT';
+        break;
+      case 'TK_TAG_END':
+        multi_parser.print_newline(true, multi_parser.output);
+        multi_parser.print_token(multi_parser.token_text);
+        multi_parser.current_mode = 'CONTENT';
+        break;
+      case 'TK_TAG_SINGLE':
+        multi_parser.print_newline(false, multi_parser.output);
+        multi_parser.print_token(multi_parser.token_text);
+        multi_parser.current_mode = 'CONTENT';
+        break;
+      case 'TK_CONTENT':
+        if (multi_parser.token_text !== '') {
+          multi_parser.print_newline(false, multi_parser.output);
+          multi_parser.print_token(multi_parser.token_text);
+        }
+        multi_parser.current_mode = 'TAG';
+        break;
+    }
+    multi_parser.last_token = multi_parser.token_type;
+    multi_parser.last_text = multi_parser.token_text;
+  }
+  return multi_parser.output.join('');
+}

extras/beautify/beautify.js

+/*
+
+ JS Beautifier
+---------------
+
+
+  Written by Einar Lielmanis, <einars@gmail.com>
+      http://jsbeautifier.org/
+
+  Originally converted to javascript by Vital, <vital76@gmail.com>
+
+  You are free to use this in any way you want, in case you find this useful or working for you.
+
+  Usage:
+    js_beautify(js_source_text);
+    js_beautify(js_source_text, options);
+
+  The options are:
+    indent_size (default 4) — indentation size,
+    indent_char (default space) — character to indent with,
+    preserve_newlines (default true) — whether existing line breaks should be preserved,
+    indent_level (default 0)  — initial indentation level, you probably won't need this ever,
+
+    e.g
+
+    js_beautify(js_source_text, {indent_size: 1, indent_char: '\t'});
+
+
+*/
+
+
+
+function js_beautify(js_source_text, options)
+{
+
+    var input, output, token_text, last_type, last_text, last_word, current_mode, modes, indent_string;
+    var whitespace, wordchar, punct, parser_pos, line_starters, in_case;
+    var prefix, token_type, do_block_just_closed, var_line, var_line_tainted, if_line_flag;
+    var indent_level;
+
+
+    var options               = options || {};
+    var opt_indent_size       = options['indent_size'] || 4;
+    var opt_indent_char       = options['indent_char'] || ' ';
+    var opt_preserve_newlines =
+        typeof options['preserve_newlines'] === 'undefined' ? true : options['preserve_newlines'];
+    var opt_indent_level      = options['indent_level'] || 0; // starting indentation
+
+
+    function trim_output()
+    {
+        while (output.length && (output[output.length - 1] === ' ' || output[output.length - 1] === indent_string)) {
+            output.pop();
+        }
+    }
+
+    function print_newline(ignore_repeated)
+    {
+
+        ignore_repeated = typeof ignore_repeated === 'undefined' ? true: ignore_repeated;
+
+        if_line_flag = false;
+        trim_output();
+
+        if (!output.length) {
+            return; // no newline on start of file
+        }
+
+        if (output[output.length - 1] !== "\n" || !ignore_repeated) {
+            output.push("\n");
+        }
+        for (var i = 0; i < indent_level; i++) {
+            output.push(indent_string);
+        }
+    }
+
+
+
+    function print_space()
+    {
+        var last_output = output.length ? output[output.length - 1] : ' ';
+        if (last_output !== ' ' && last_output !== '\n' && last_output !== indent_string) { // prevent occassional duplicate space
+            output.push(' ');
+        }
+    }
+
+
+    function print_token()
+    {
+        output.push(token_text);
+    }
+
+    function indent()
+    {
+        indent_level++;
+    }
+
+
+    function unindent()
+    {
+        if (indent_level) {
+            indent_level--;
+        }
+    }
+
+
+    function remove_indent()
+    {
+        if (output.length && output[output.length - 1] === indent_string) {
+            output.pop();
+        }
+    }
+
+
+    function set_mode(mode)
+    {
+        modes.push(current_mode);
+        current_mode = mode;
+    }
+
+
+    function restore_mode()
+    {
+        do_block_just_closed = current_mode === 'DO_BLOCK';
+        current_mode = modes.pop();
+    }
+
+
+    function in_array(what, arr)
+    {
+        for (var i = 0; i < arr.length; i++)
+        {
+            if (arr[i] === what) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+
+    function get_next_token()
+    {
+        var n_newlines = 0;
+        var c = '';
+
+        do {
+            if (parser_pos >= input.length) {
+                return ['', 'TK_EOF'];
+            }
+            c = input.charAt(parser_pos);
+
+            parser_pos += 1;
+            if (c === "\n") {
+                n_newlines += 1;
+            }
+        }
+        while (in_array(c, whitespace));
+
+        var wanted_newline = false;
+
+        if (opt_preserve_newlines) {
+            if (n_newlines > 1) {
+                for (var i = 0; i < 2; i++) {
+                    print_newline(i === 0);
+                }
+            }
+            wanted_newline = (n_newlines === 1);
+        }
+
+
+        if (in_array(c, wordchar)) {
+            if (parser_pos < input.length) {
+                while (in_array(input.charAt(parser_pos), wordchar)) {
+                    c += input.charAt(parser_pos);
+                    parser_pos += 1;
+                    if (parser_pos === input.length) {
+                        break;
+                    }
+                }
+            }
+
+            // small and surprisingly unugly hack for 1E-10 representation
+            if (parser_pos !== input.length && c.match(/^[0-9]+[Ee]$/) && input.charAt(parser_pos) === '-') {
+                parser_pos += 1;
+
+                var t = get_next_token(parser_pos);
+                c += '-' + t[0];
+                return [c, 'TK_WORD'];
+            }
+
+            if (c === 'in') { // hack for 'in' operator
+                return [c, 'TK_OPERATOR'];
+            }
+            if (wanted_newline && last_type !== 'TK_OPERATOR' && !if_line_flag) {
+                print_newline();
+            }
+            return [c, 'TK_WORD'];
+        }
+
+        if (c === '(' || c === '[') {
+            return [c, 'TK_START_EXPR'];
+        }
+
+        if (c === ')' || c === ']') {
+            return [c, 'TK_END_EXPR'];
+        }
+
+        if (c === '{') {
+            return [c, 'TK_START_BLOCK'];
+        }
+
+        if (c === '}') {
+            return [c, 'TK_END_BLOCK'];
+        }
+
+        if (c === ';') {
+            return [c, 'TK_SEMICOLON'];
+        }
+
+        if (c === '/') {
+            var comment = '';
+            // peek for comment /* ... */
+            if (input.charAt(parser_pos) === '*') {
+                parser_pos += 1;
+                if (parser_pos < input.length) {
+                    while (! (input.charAt(parser_pos) === '*' && input.charAt(parser_pos + 1) && input.charAt(parser_pos + 1) === '/') && parser_pos < input.length) {
+                        comment += input.charAt(parser_pos);
+                        parser_pos += 1;
+                        if (parser_pos >= input.length) {
+                            break;
+                        }
+                    }
+                }
+                parser_pos += 2;
+                return ['/*' + comment + '*/', 'TK_BLOCK_COMMENT'];
+            }
+            // peek for comment // ...
+            if (input.charAt(parser_pos) === '/') {
+                comment = c;
+                while (input.charAt(parser_pos) !== "\x0d" && input.charAt(parser_pos) !== "\x0a") {
+                    comment += input.charAt(parser_pos);
+                    parser_pos += 1;
+                    if (parser_pos >= input.length) {
+                        break;
+                    }
+                }
+                parser_pos += 1;
+                if (wanted_newline) {
+                    print_newline();
+                }
+                return [comment, 'TK_COMMENT'];
+            }
+
+        }
+
+        if (c === "'" || // string
+        c === '"' || // string
+        (c === '/' &&
+        ((last_type === 'TK_WORD' && last_text === 'return') || (last_type === 'TK_START_EXPR' || last_type === 'TK_END_BLOCK' || last_type === 'TK_OPERATOR' || last_type === 'TK_EOF' || last_type === 'TK_SEMICOLON')))) { // regexp
+            var sep = c;
+            var esc = false;
+            var resulting_string = '';
+
+            if (parser_pos < input.length) {
+
+                while (esc || input.charAt(parser_pos) !== sep) {
+                    resulting_string += input.charAt(parser_pos);
+                    if (!esc) {
+                        esc = input.charAt(parser_pos) === '\\';
+                    } else {
+                        esc = false;
+                    }
+                    parser_pos += 1;
+                    if (parser_pos >= input.length) {
+                        break;
+                    }
+                }
+
+            }
+
+            parser_pos += 1;
+
+            resulting_string = sep + resulting_string + sep;
+
+            if (sep == '/') {
+                // regexps may have modifiers /regexp/MOD , so fetch those, too
+                while (parser_pos < input.length && in_array(input.charAt(parser_pos), wordchar)) {
+                    resulting_string += input.charAt(parser_pos);
+                    parser_pos += 1;
+                }
+            }
+            return [resulting_string, 'TK_STRING'];
+        }
+
+        if (in_array(c, punct)) {
+            while (parser_pos < input.length && in_array(c + input.charAt(parser_pos), punct)) {
+                c += input.charAt(parser_pos);
+                parser_pos += 1;
+                if (parser_pos >= input.length) {
+                    break;
+                }
+            }
+            return [c, 'TK_OPERATOR'];
+        }
+
+        return [c, 'TK_UNKNOWN'];
+    }
+
+
+    //----------------------------------
+
+    indent_string = '';
+    while (opt_indent_size--) {
+        indent_string += opt_indent_char;
+    }
+
+    indent_level = opt_indent_level;
+
+    input = js_source_text;
+
+    last_word = ''; // last 'TK_WORD' passed
+    last_type = 'TK_START_EXPR'; // last token type
+    last_text = ''; // last token text
+    output = [];
+
+    do_block_just_closed = false;
+    var_line = false;         // currently drawing var .... ;
+    var_line_tainted = false; // false: var a = 5; true: var a = 5, b = 6
+
+    whitespace = "\n\r\t ".split('');
+    wordchar = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$'.split('');
+    punct = '+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << >>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |= ::'.split(' ');
+
+    // words which should always start on new line.
+    line_starters = 'continue,try,throw,return,var,if,switch,case,default,for,while,break,function'.split(',');
+
+    // states showing if we are currently in expression (i.e. "if" case) - 'EXPRESSION', or in usual block (like, procedure), 'BLOCK'.
+    // some formatting depends on that.
+    current_mode = 'BLOCK';
+    modes = [current_mode];
+
+    parser_pos = 0;
+    in_case = false; // flag for parser that case/default has been processed, and next colon needs special attention
+    while (true) {
+        var t = get_next_token(parser_pos);
+        token_text = t[0];
+        token_type = t[1];
+        if (token_type === 'TK_EOF') {
+            break;
+        }
+
+        switch (token_type) {
+
+        case 'TK_START_EXPR':
+            var_line = false;
+            set_mode('EXPRESSION');
+            if (last_text === ';') {
+                print_newline();
+            } else if (last_type === 'TK_END_EXPR' || last_type === 'TK_START_EXPR') {
+                // do nothing on (( and )( and ][ and ]( ..
+            } else if (last_type !== 'TK_WORD' && last_type !== 'TK_OPERATOR') {
+                print_space();
+            } else if (in_array(last_word, line_starters) && last_word !== 'function') {
+                print_space();
+            }
+            print_token();
+            break;
+
+        case 'TK_END_EXPR':
+            print_token();
+            restore_mode();
+            break;
+
+        case 'TK_START_BLOCK':
+
+            if (last_word === 'do') {
+                set_mode('DO_BLOCK');
+            } else {
+                set_mode('BLOCK');
+            }
+            if (last_type !== 'TK_OPERATOR' && last_type !== 'TK_START_EXPR') {
+                if (last_type === 'TK_START_BLOCK') {
+                    print_newline();
+                } else {
+                    print_space();
+                }
+            }
+            print_token();
+            indent();
+            break;
+
+        case 'TK_END_BLOCK':
+            if (last_type === 'TK_START_BLOCK') {
+                // nothing
+                trim_output();
+                unindent();
+            } else {
+                unindent();
+                print_newline();
+            }
+            print_token();
+            restore_mode();
+            break;
+
+        case 'TK_WORD':
+
+            if (do_block_just_closed) {
+                print_space();
+                print_token();
+                print_space();
+                break;
+            }
+
+            if (token_text === 'case' || token_text === 'default') {
+                if (last_text === ':') {
+                    // switch cases following one another
+                    remove_indent();
+                } else {
+                    // case statement starts in the same line where switch
+                    unindent();
+                    print_newline();
+                    indent();
+                }
+                print_token();
+                in_case = true;
+                break;
+            }
+
+            prefix = 'NONE';
+            if (last_type === 'TK_END_BLOCK') {
+                if (!in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) {
+                    prefix = 'NEWLINE';
+                } else {
+                    prefix = 'SPACE';
+                    print_space();
+                }
+            } else if (last_type === 'TK_SEMICOLON' && (current_mode === 'BLOCK' || current_mode === 'DO_BLOCK')) {
+                prefix = 'NEWLINE';
+            } else if (last_type === 'TK_SEMICOLON' && current_mode === 'EXPRESSION') {
+                prefix = 'SPACE';
+            } else if (last_type === 'TK_STRING') {
+                prefix = 'NEWLINE';
+            } else if (last_type === 'TK_WORD') {
+                prefix = 'SPACE';
+            } else if (last_type === 'TK_START_BLOCK') {
+                prefix = 'NEWLINE';
+            } else if (last_type === 'TK_END_EXPR') {
+                print_space();
+                prefix = 'NEWLINE';
+            }
+
+            if (last_type !== 'TK_END_BLOCK' && in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) {
+                print_newline();
+            } else if (in_array(token_text, line_starters) || prefix === 'NEWLINE') {
+                if (last_text === 'else') {
+                    // no need to force newline on else break
+                    print_space();
+                } else if ((last_type === 'TK_START_EXPR' || last_text === '=') && token_text === 'function') {
+                    // no need to force newline on 'function': (function
+                    // DONOTHING
+                } else if (last_type === 'TK_WORD' && (last_text === 'return' || last_text === 'throw')) {
+                    // no newline between 'return nnn'
+                    print_space();
+                } else if (last_type !== 'TK_END_EXPR') {
+                    if ((last_type !== 'TK_START_EXPR' || token_text !== 'var') && last_text !== ':') {
+                        // no need to force newline on 'var': for (var x = 0...)
+                        if (token_text === 'if' && last_type === 'TK_WORD' && last_word === 'else') {
+                            // no newline for } else if {
+                            print_space();
+                        } else {
+                            print_newline();
+                        }
+                    }
+                } else {
+                    if (in_array(token_text, line_starters) && last_text !== ')') {
+                        print_newline();
+                    }
+                }
+            } else if (prefix === 'SPACE') {
+                print_space();
+            }
+            print_token();
+            last_word = token_text;
+
+            if (token_text === 'var') {
+                var_line = true;
+                var_line_tainted = false;
+            }
+
+            if (token_text === 'if' || token_text === 'else') {
+                if_line_flag = true;
+            }
+
+            break;
+
+        case 'TK_SEMICOLON':
+
+            print_token();
+            var_line = false;
+            break;
+
+        case 'TK_STRING':
+
+            if (last_type === 'TK_START_BLOCK' || last_type === 'TK_END_BLOCK' || last_type == 'TK_SEMICOLON') {
+                print_newline();
+            } else if (last_type === 'TK_WORD') {
+                print_space();
+            }
+            print_token();
+            break;
+
+        case 'TK_OPERATOR':
+
+            var start_delim = true;
+            var end_delim = true;
+            if (var_line && token_text !== ',') {
+                var_line_tainted = true;
+                if (token_text === ':') {
+                    var_line = false;
+                }
+            }
+            if (var_line && token_text === ',' && current_mode === 'EXPRESSION') {
+                // do not break on comma, for(var a = 1, b = 2)
+                var_line_tainted = false;
+            }
+
+            if (token_text === ':' && in_case) {
+                print_token(); // colon really asks for separate treatment
+                print_newline();
+                break;
+            }
+
+            if (token_text === '::') {
+                // no spaces around exotic namespacing syntax operator
+                print_token();
+                break;
+            }
+
+            in_case = false;
+
+            if (token_text === ',') {
+                if (var_line) {
+                    if (var_line_tainted) {
+                        print_token();
+                        print_newline();
+                        var_line_tainted = false;
+                    } else {
+                        print_token();
+                        print_space();
+                    }
+                } else if (last_type === 'TK_END_BLOCK') {
+                    print_token();
+                    print_newline();
+                } else {
+                    if (current_mode === 'BLOCK') {
+                        print_token();
+                        print_newline();
+                    } else {
+                        // EXPR od DO_BLOCK
+                        print_token();
+                        print_space();
+                    }
+                }
+                break;
+            } else if (token_text === '--' || token_text === '++') { // unary operators special case
+                if (last_text === ';') {
+                    // space for (;; ++i)
+                    start_delim = true;
+                    end_delim = false;
+                } else {
+                    start_delim = false;
+                    end_delim = false;
+                }
+            } else if (token_text === '!' && last_type === 'TK_START_EXPR') {
+                // special case handling: if (!a)
+                start_delim = false;
+                end_delim = false;
+            } else if (last_type === 'TK_OPERATOR') {
+                start_delim = false;
+                end_delim = false;
+            } else if (last_type === 'TK_END_EXPR') {
+                start_delim = true;
+                end_delim = true;
+            } else if (token_text === '.') {
+                // decimal digits or object.property
+                start_delim = false;
+                end_delim = false;
+
+            } else if (token_text === ':') {
+                // zz: xx
+                // can't differentiate ternary op, so for now it's a ? b: c; without space before colon
+                if (last_text.match(/^\d+$/)) {
+                    // a little help for ternary a ? 1 : 0;
+                    start_delim = true;
+                } else {
+                    start_delim = false;
+                }
+            }
+            if (start_delim) {
+                print_space();
+            }
+
+            print_token();
+
+            if (end_delim) {
+                print_space();
+            }
+            break;
+
+        case 'TK_BLOCK_COMMENT':
+
+            print_newline();
+            print_token();
+            print_newline();
+            break;
+
+        case 'TK_COMMENT':
+
+            // print_newline();
+            print_space();
+            print_token();
+            print_newline();
+            break;
+
+        case 'TK_UNKNOWN':
+            print_token();
+            break;
+        }
+
+        last_type = token_type;
+        last_text = token_text;
+    }
+
+    return output.join('');
+
+}

extras/jspec/images/bg.png

Added
New image

extras/jspec/images/hr.png

Added
New image

extras/jspec/images/sprites.bg.png

Added
New image

extras/jspec/images/sprites.png

Added
New image

extras/jspec/images/vr.png

Added
New image

extras/jspec/jspec.css

+body.jspec {
+  margin: 45px 0;
+  text-align: center;
+  font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+/*  background: #efefef url(images/bg.png) top left repeat-x;*/
+  background: #efefef;
+}
+#jspec {
+  margin: 0 auto;
+  padding-top: 25px;
+  width: 1008px;
+  background: url(images/vr.png) top left repeat-y;
+  text-align: left;
+}
+#jspec-top {
+  position: relative;
+  margin: 0 auto;
+  width: 1008px;
+  height: 40px;
+  background: url(images/sprites.bg.png) top left no-repeat;
+}
+#jspec-bottom {
+  margin: 0 auto;
+  width: 1008px;
+  height: 15px;
+  background: url(images/sprites.bg.png) bottom left no-repeat;
+}
+#jspec-title {
+  position: relative;
+  top: 35px;
+  left: 20px;
+  width: 160px;
+  font-size: 22px;
+  font-weight: normal;
+  background: url(images/sprites.png) 0 -126px no-repeat;
+}
+#jspec-title em {
+  font-size: 10px;
+  font-style: normal;
+  color: #BCC8D1;
+}
+#jspec-report * {
+	margin: 0;
+	padding: 0;
+	background: none;
+	border: none;
+}
+#jspec-report {
+  padding: 15px 40px;
+	font: 11px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+	color: #7B8D9B;
+}
+#jspec-report.has-failures {
+  padding-bottom: 30px;
+}
+#jspec-report .hidden {
+  display: none;
+}
+#jspec-report .heading {
+  margin-bottom: 15px;
+}
+#jspec-report .heading span {
+  padding-right: 10px;
+}
+#jspec-report .heading .passes em {
+  color: #0ea0eb;
+}
+#jspec-report .heading .failures em {
+  color: #FA1616;
+}
+#jspec-report table {
+  width: 100%;
+  font-size: 11px;
+  border-collapse: collapse;
+}
+#jspec-report td {
+  padding: 8px;
+  text-indent: 30px;
+  color: #7B8D9B;
+}
+#jspec-report tr td:first-child em {
+  font-style: normal;
+  font-weight: normal;
+  color: #7B8D9B;
+}
+#jspec-report tr:not(.description):hover {
+  text-shadow: 1px 1px 1px #fff;
+  background: #F2F5F7;
+}
+#jspec-report td + td {
+  padding-right: 0;
+  width: 15px;
+}
+#jspec-report td.pass {
+  background: url(images/sprites.png) 3px -7px no-repeat;
+}
+#jspec-report td.fail {
+  background: url(images/sprites.png) 3px -47px no-repeat;
+  font-weight: bold;
+  color: #FC0D0D;
+}
+#jspec-report td.requires-implementation {
+  background: url(images/sprites.png) 3px -87px no-repeat;
+}
+#jspec-report tr.description td {
+  margin-top: 25px;
+  padding-top: 25px;
+  font-size: 12px;
+  font-weight: bold;
+  text-indent: 0;
+  color: #1a1a1a;
+}
+#jspec-report tr.description:first-child td {
+  border-top: none;  
+}
+#jspec-report .assertion {
+  display: block;
+  float: left;
+  margin: 0 0 0 1px;
+  padding: 0;
+  width: 1px;
+  height: 5px;
+  background: #7B8D9B;
+}
+#jspec-report .assertion.failed {
+  background: red;
+}
+.jspec-sandbox {
+  display: none;
+}

extras/jspec/jspec.jquery.js

+
+// JSpec - jQuery - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
+
+(function($, $$){
+
+  // --- Dependencies
+
+  $$.requires('jQuery', 'when using jspec.jquery.js')
+  
+  // --- Async Support
+
+  $.ajaxSetup({ async : false })
+
+  // --- Helpers
+
+  $$.defaultContext.element = $
+  $$.defaultContext.elements = $
+  $$.defaultContext.defaultSandbox = $$.defaultContext.sandbox
+  $$.defaultContext.sandbox = function() { return $($$.defaultContext.defaultSandbox()) }
+
+  // --- Matchers
+
+  $$.addMatchers({
+    have_tag      : "jQuery(expected, actual).length == 1",
+    have_one      : "alias have_tag",
+    have_tags     : "jQuery(expected, actual).length > 1",
+    have_many     : "alias have_tags",
+    have_child    : "jQuery(actual).children(expected).length == 1",
+    have_children : "jQuery(actual).children(expected).length > 1",
+    have_class    : "jQuery(actual).hasClass(expected)",
+    have_text     : "jQuery(actual).text() == expected",
+    have_value    : "jQuery(actual).val() == expected",
+    be_visible    : "!jQuery(actual).is(':hidden')",
+    be_hidden     : "jQuery(actual).is(':hidden')",
+    be_enabled    : "!jQuery(actual).attr('disabled')",
+    
+    have_attr : { match : function(actual, attr, value) {
+        if (value) return $(actual).attr(attr) == value
+        else return $(actual).attr(attr)
+      }
+    }
+  })
+  
+  // --- be_BOOLATTR
+  
+  $$.each('disabled selected checked', function(attr){
+    $$.matchers['be_' + attr] = "jQuery(actual).attr('" + attr + "')"
+  })
+  
+  // --- have_ATTR
+  
+  $$.each('type id title alt href src rel rev name target', function(attr){
+    $$.matchers['have_' + attr] = { match : function(actual, value) {
+        return $$.matchers.have_attr.match(actual, attr, value)
+      }
+    }
+  })
+  
+  // --- be_a_TYPE_input (deprecated)
+  
+  $$.each('checkbox radio file password submit image text reset button', function(type){
+    console.warn("be_a_" + type + "_input is deprected; use have_type('" + type + "')");
+    JSpec.matchers['be_a_' + type + '_input'] = "jQuery(actual).get(0).type == '" + type + "'"
+  })
+  
+})(jQuery, JSpec)
+
+

extras/jspec/jspec.jquery.min.js

+
+(function($,$$){$$.requires('jQuery','when using jspec.jquery.js')
+$.ajaxSetup({async:false})
+$$.defaultContext.element=$
+$$.defaultContext.elements=$
+$$.defaultContext.defaultSandbox=$$.defaultContext.sandbox
+$$.defaultContext.sandbox=function(){return $($$.defaultContext.defaultSandbox())}
+$$.addMatchers({have_tag:"jQuery(expected, actual).length == 1",have_one:"alias have_tag",have_tags:"jQuery(expected, actual).length > 1",have_many:"alias have_tags",have_child:"jQuery(actual).children(expected).length == 1",have_children:"jQuery(actual).children(expected).length > 1",have_class:"jQuery(actual).hasClass(expected)",have_text:"jQuery(actual).text() == expected",have_value:"jQuery(actual).val() == expected",be_visible:"!jQuery(actual).is(':hidden')",be_hidden:"jQuery(actual).is(':hidden')",be_enabled:"!jQuery(actual).attr('disabled')",have_attr:{match:function(actual,attr,value){if(value)return $(actual).attr(attr)==value
+else return $(actual).attr(attr)}}})
+$$.each('disabled selected checked',function(attr){$$.matchers['be_'+attr]="jQuery(actual).attr('"+attr+"')"})
+$$.each('type id title alt href src rel rev name target',function(attr){$$.matchers['have_'+attr]={match:function(actual,value){return $$.matchers.have_attr.match(actual,attr,value)}}})
+$$.each('checkbox radio file password submit image text reset button',function(type){console.warn("be_a_"+type+"_input is deprected; use have_type('"+type+"')");JSpec.matchers['be_a_'+type+'_input']="jQuery(actual).get(0).type == '"+type+"'"})})(jQuery,JSpec)

extras/jspec/jspec.js

+
+// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
+
+(function(){
+
+  JSpec = {
+
+    version  : '1.0.4',
+    main     : this,
+    suites   : [],
+    matchers : {},
+    stats    : { specs : 0, assertions : 0, failures : 0, passes : 0 },
+    options  : { profile : false },
+
+    /**
+     * Default context in which bodies are evaluated.
+     * This allows specs and hooks to use the 'this' keyword in
+     * order to store variables, as well as allowing the context
+     * to provide helper methods or properties.
+     *
+     * Replace context simply by setting JSpec.context
+     * to your own like below:
+     *
+     * JSpec.context = { foo : 'bar' }
+     *
+     * Contexts can be changed within any body, this can be useful
+     * in order to provide specific helper methods to specific suites.
+     *
+     * To reset (usually in after hook) simply set to null like below:
+     *
+     * JSpec.context = null
+     */
+
+    defaultContext : {
+      sandbox : function(name) {
+        sandbox = document.createElement('div')
+        sandbox.setAttribute('class', 'jspec-sandbox')
+        document.body.appendChild(sandbox)
+        return sandbox
+      }
+    },
+
+    // --- Objects
+
+    /**
+     * Matcher.
+     *
+     * There are many ways to define a matcher within JSpec. The first being
+     * a string that is less than 4 characters long, which is considered a simple
+     * binary operation between two expressions. For example the matcher '==' simply
+     * evaluates to 'actual == expected'.
+     *
+     * The second way to create a matcher is with a larger string, which is evaluated,
+     * and then returned such as 'actual.match(expected)'.
+     *
+     * You may alias simply by starting a string with 'alias', such as 'be' : 'alias eql'.
+     *
+     * Finally an object may be used, and must contain a 'match' method, which is passed
+     * both the expected, and actual values. Optionally a 'message' method may be used to
+     * specify a custom message. Example:
+     *
+     * match : function(actual, expected) {
+     *   return typeof actual == expected
+     * }
+     *
+     * @param  {string} name
+     * @param  {hash, string} matcher
+     * @param  {object} actual
+     * @param  {array} expected
+     * @param  {bool} negate
+     * @return {Matcher}
+     * @api private
+     */
+
+    Matcher : function (name, matcher, actual, expected, negate) {
+      self = this
+      this.name = name
+      this.message = ''
+      this.passed = false
+      
+      // Define matchers from strings
+
+      if (typeof matcher == 'string') {
+        if (matcher.match(/^alias (\w+)/)) matcher = JSpec.matchers[matcher.match(/^alias (\w+)/)[1]]
+        if (matcher.length < 4) body = 'actual ' + matcher + ' expected'
+        else body = matcher
+        matcher = { match : function(actual, expected) { return eval(body) } }
+      }
+
+      // Generate matcher message
+
+      function generateMessage() {
+         // TODO: clone expected instead of unshifting in this.match()
+        expectedMessage = print.apply(this, expected.slice(1))
+        return 'expected ' + print(actual) + ' to ' + (negate ? ' not ' : '') + name.replace(/_/g, ' ') + ' ' + expectedMessage
+      }
+
+      // Set message to matcher callback invocation or auto-generated message
+
+      function setMessage() {
+        self.message = typeof matcher.message == 'function' ?
+          matcher.message(actual, expected, negate):
+          generateMessage()
+      }
+
+      // Pass the matcher
+
+      function pass() {
+        setMessage()
+        JSpec.stats.passes += 1
+        self.passed = true
+      }
+
+      // Fail the matcher
+
+      function fail() {
+        setMessage()
+        JSpec.stats.failures += 1
+      }
+
+      // Return result of match
+
+      this.match = function() {
+        expected.unshift(actual == null ? null : actual.valueOf())
+        return matcher.match.apply(JSpec, expected)
+      }
+
+      // Boolean match result
+
+      this.passes = function() {
+        this.result = this.match()
+        return negate? !this.result : this.result
+      }
+
+      // Performs match, and passes / fails the matcher
+
+      this.exec = function() {
+        this.passes() ? pass() : fail()
+        return this
+      }
+    },
+
+
+    formatters : {
+
+      /**
+       * Default formatter, outputting to the DOM.
+       *
+       * Options:
+       *   - reportToId    id of element to output reports to, defaults to 'jspec'
+       *   - failuresOnly  displays only suites with failing specs
+       *
+       * @api public
+       */
+
+      DOM : function(results, options) {
+        id = option('reportToId') || 'jspec'
+        report = document.getElementById(id)
+        classes = results.stats.failures ? 'has-failures' : ''
+        if (!report) error('requires the element #' + id + ' to output its reports')
+
+        markup =
+        '<div id="jspec-report" class="' + classes + '"><div class="heading">           \
+        <span class="passes">Passes: <em>' + results.stats.passes + '</em></span>       \
+        <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
+        </div><table class="suites">'
+          
+        function renderSuite(suite) {
+          failuresOnly = option('failuresOnly')
+          displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
+          if (displaySuite && suite.hasSpecs()) {
+            markup += '<tr class="description"><td colspan="2">' + suite.description + '</td></tr>'
+            each(suite.specs, function(i, spec){
+              markup += '<tr class="' + (i % 2 ? 'odd' : 'even') + '">'
+              if (spec.requiresImplementation() && !failuresOnly) {
+                markup += '<td class="requires-implementation" colspan="2">' + spec.description + '</td>'
+              }
+              else if (spec.passed() && !failuresOnly) {
+                markup += '<td class="pass">' + spec.description+ '</td><td>' + spec.assertionsGraph() + '</td>'
+              }
+              else if(!spec.passed()) {
+                markup += '<td class="fail">' + spec.description + ' <em>' + spec.failure().message + '</em>' + '</td><td>' + spec.assertionsGraph() + '</td>'
+              }
+              markup += '<tr class="body" style="display: none;"><td colspan="2">' + spec.body + '</td></tr>'
+            })
+            markup += '</tr>'
+          }
+        }  
+        
+        function renderSuites(suites) {
+          each(suites, function(suite){
+            renderSuite(suite)
+            if (suite.hasSuites()) renderSuites(suite.suites)
+          })
+        }
+        
+        renderSuites(results.suites)
+
+        markup += '</table></div>'
+
+        report.innerHTML = markup
+      },
+
+      /**
+       * Console formatter, tested with Firebug and Safari 4.
+       *
+       * @api public
+       */
+
+      Console : function(results, options) {
+        console.log('')
+        console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
+        
+        function renderSuite(suite) {
+          if (suite.ran) {
+            console.group(suite.description)
+            results.each(suite.specs, function(spec){
+              assertionCount = spec.assertions.length + ':'
+              if (spec.requiresImplementation())
+                console.warn(spec.description)
+              else if (spec.passed())
+                console.log(assertionCount + ' ' + spec.description)
+              else 
+                console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message)
+            })
+            console.groupEnd()
+          }          
+        }
+        
+        function renderSuites(suites) {
+          each(suites, function(suite){
+            renderSuite(suite)
+            if (suite.hasSuites()) renderSuites(suite.suites)
+          })
+        }
+        
+        renderSuites(results.suites)
+      }
+    },
+
+    /**
+     * Specification Suite block object.
+     *
+     * @param {string} description
+     * @param {function} body
+     * @api private
+     */
+
+    Suite : function(description, body) {
+      this.body = body, this.suites = [], this.specs = []
+      this.description = description, this.ran = false
+      this.hooks = { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] }
+
+      // Add a spec to the suite
+
+      this.addSpec = function(description, body) {
+        spec = new JSpec.Spec(description, body)
+        this.specs.push(spec)
+        spec.suite = this
+      }
+      
+      // Add a hook to the suite
+      
+      this.addHook = function(hook, body) {
+        this.hooks[hook].push(body)
+      }
+      
+      // Add a nested suite
+      
+      this.addSuite = function(description, body) {
+        suite = new JSpec.Suite(description, body)
+        suite.description = this.description + ' ' + suite.description
+        this.suites.push(suite)
+        suite.suite = this
+      }
+
+      // Invoke a hook in context to this suite
+
+      this.hook = function(hook) {
+        each(this.hooks[hook], function(body) {
+          JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + this.description + "': ")
+        })
+      }
+      
+      // Check if nested suites are present
+      
+      this.hasSuites = function() {
+        return this.suites.length  
+      }
+      
+      // Check if this suite has specs
+      
+      this.hasSpecs = function() {
+        return this.specs.length
+      }
+      
+      // Check if the entire suite passed
+
+      this.passed = function() {
+        var passed = true
+        each(this.specs, function(spec){
+          if (!spec.passed()) passed = false
+        })
+        return passed
+      }
+    },
+    
+    /**
+     * Specification block object.
+     *
+     * @param {string} description
+     * @param {function} body
+     * @api private
+     */
+
+    Spec : function(description, body) {
+      this.body = body, this.description = description, this.assertions = []
+
+      // Find first failing assertion
+
+      this.failure = function() {
+        return inject(this.assertions, null, function(failure, assertion){
+          return !assertion.passed && !failure ? assertion : failure
+        })
+      }
+      
+      // Find all failing assertions
+      
+      this.failures = function() {
+        return inject(this.assertions, [], function(failures, assertion){
+          if (!assertion.passed) failures.push(assertion)
+          return failures
+        })
+      }
+
+      // Weither or not the spec passed
+
+      this.passed = function() {
+        return !this.failure()
+      }
+
+      // Weither or not the spec requires implementation (no assertions)
+
+      this.requiresImplementation = function() {
+        return this.assertions.length == 0
+      }
+      
+      // Sprite based assertions graph
+      
+      this.assertionsGraph = function() {
+        return map(this.assertions, function(assertion){
+          return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
+        }).join('')
+      }
+    },
+
+    // --- Methods
+    
+    /**
+     * Get option value. This method first checks if
+     * the option key has been set via the query string,
+     * otherwise returning the options hash value.
+     *
+     * @param  {string} key
+     * @return {mixed}
+     * @api public
+     */
+     
+     option : function(key) {
+       if ((value = query(key)) !== null) return value
+       else return JSpec.options[key] || null
+     },
+
+    /**
+     * Generates a hash of the object passed.
+     *
+     * @param  {object} object
+     * @return {string}
+     * @api private
+     */
+
+    hash : function(object) {
+      serialize = function(prefix) {
+        return inject(object, prefix + ':', function(buffer, key, value){
+          return buffer += hash(value)
+        })
+      }
+      switch (object.constructor) {
+        case Array:  return serialize('a')
+        case Object: return serialize('o')
+        case RegExp: return 'r:' + object.toString()
+        case Number: return 'n:' + object.toString()
+        case String: return 's:' + object.toString()
+        default: return object.toString()
+      }
+    },
+
+    /**
+     * Return last element of an array.
+     *
+     * @param  {array} array
+     * @return {object}
+     * @api public
+     */
+
+    last : function(array) {
+      return array[array.length - 1]
+    },
+
+    /**
+     * Convert object(s) to a print-friend string.
+     *
+     * @param  {object, ...} object
+     * @return {string}
+     * @api public
+     */
+
+    print : function(object) {
+      if (arguments.length > 1) {
+        list = []
+        for (i = 0; i < arguments.length; i++) list.push(print(arguments[i]))
+        return list.join(', ')
+      }
+      if (object === undefined) return ''
+      if (object === null) return 'null'
+      if (object === true) return 'true'
+      if (object === false) return 'false'
+      if (object.jquery && object.selector.length > 0) return 'selector ' + print(object.selector) + ''
+      if (object.jquery) return escape(object.html())
+      if (object.nodeName) return escape(object.outerHTML)
+      switch (object.constructor) {
+        case String: return "'" + escape(object) + "'"
+        case Number: return object
+        case Array :
+          buff = '['
+          each(object, function(v){ buff += ', ' + print(v) })
+          return buff.replace('[,', '[') + ' ]'
+        case Object:
+          buff = '{'
+          each(object, function(k, v){ buff += ', ' + print(k) + ' : ' + print(v)})
+          return buff.replace('{,', '{') + ' }'
+        default: 
+          return escape(object.toString())
+      }
+    },
+
+    /**
+     * Escape HTML.
+     *
+     * @param  {string} html
+     * @return {string}
+     * @api public
+     */
+
+     escape : function(html) {
+       if (typeof html != 'string') return html
+       return html.
+         replace(/&/gmi, '&amp;').
+         replace(/"/gmi, '&quot;').
+         replace(/>/gmi, '&gt;').
+         replace(/</gmi, '&lt;')
+     },
+
+    /**
+     * Invoke a matcher.
+     *
+     * this.match('test', 'should', 'be_a', [String])
+     *
+     * @param  {object} actual
+     * @param  {bool, string} negate
+     * @param  {string} name
+     * @param  {array} expected
+     * @return {bool}
+     * @api private
+     */
+
+    match : function(actual, negate, name, expected) {
+      if (typeof negate == 'string') negate = negate == 'should' ? false : true
+      matcher = new this.Matcher(name, this.matchers[name], actual, expected, negate)
+      this.currentSpec.assertions.push(matcher.exec())
+      return matcher.result
+    },
+
+    /**
+     * Iterate an object, invoking the given callback.
+     *
+     * @param  {hash, array, string} object
+     * @param  {function} callback
+     * @return {JSpec}
+     * @api public
+     */
+
+    each : function(object, callback) {
+      if (typeof object == 'string') object = object.split(' ')
+      for (key in object) {
+        if (object.hasOwnProperty(key))
+          callback.length == 1 ?
+            callback.call(JSpec, object[key]):
+            callback.call(JSpec, key, object[key])
+      }
+      return JSpec
+    },
+
+    /**
+     * Iterate with memo.
+     *
+     * @param  {hash, array} object
+     * @param  {object} initial
+     * @param  {function} callback
+     * @return {object}
+     * @api public
+     */
+
+    inject : function(object, initial, callback) {
+      each(object, function(key, value){
+        initial = callback.length == 2 ?
+          callback.call(JSpec, initial, value):
+          callback.call(JSpec, initial, key, value) || initial
+      })
+      return initial
+    },
+    
+    /**
+     * Strim whitespace or chars.
+     *
+     * @param  {string} string
+     * @param  {string} chars
+     * @return {string}
+     * @api public
+     */
+
+     strip : function(string, chars) {
+       return string.
+         replace(new RegExp('['  + (chars || '\\s') + ']*$'), '').
+         replace(new RegExp('^[' + (chars || '\\s') + ']*'),  '')
+     },
+    
+    /**
+     * Map callback return values.
+     *
+     * @param  {hash, array} object
+     * @param  {function} callback
+     * @return {array}
+     * @api public
+     */
+
+    map : function(object, callback) {
+      return inject(object, [], function(memo, key, value){
+        memo.push(callback.length == 1 ?
+          callback.call(JSpec, value):
+          callback.call(JSpec, key, value))
+      })
+    },
+    
+    /**
+     * Returns true if the callback returns true at least once.
+     *
+     * @param  {hash, array} object
+     * @param  {function} callback
+     * @return {bool}
+     * @api public
+     */
+         
+    any : function(object, callback) {
+      return inject(object, false, function(state, key, value){
+        if (state) return true
+        return callback.length == 1 ?
+          callback.call(JSpec, value):
+          callback.call(JSpec, key, value)
+      })
+    },
+
+    /**
+     * Define matchers.
+     *
+     * @param  {hash} matchers
+     * @return {JSpec}
+     * @api public
+     */
+
+    addMatchers : function(matchers) {
+      each(matchers, function(name, body){ this.matchers[name] = body })
+      return this
+    },
+    
+    /**
+     * Add a root suite to JSpec.
+     *
+     * @param  {string} description
+     * @param  {body} function
+     * @return {JSpec}
+     * @api public
+     */
+    
+    addSuite : function(description, body) {
+      this.suites.push(new JSpec.Suite(description, body))
+      return this
+    },
+
+    /**
+     * Evaluate a JSpec capture body.
+     *
+     * @param  {function} body
+     * @param  {string} errorMessage (optional)
+     * @return {Type}
+     * @api private
+     */
+
+    evalBody : function(body, errorMessage) {
+      try { body.call(this.context || this.defaultContext) }
+      catch(e) { error(errorMessage, e) }
+    },
+
+    /**
+     * Pre-process a string of JSpec.
+     *
+     * @param  {string} input
+     * @return {string}
+     * @api private
+     */
+
+    preprocess : function(input) {
+      return input.
+        replace(/describe (.*?)$/m, 'JSpec.addSuite($1, function(){').
+        replace(/describe (.*?)$/gm, 'this.addSuite($1, function(){').
+        replace(/it (.*?)$/gm, 'this.addSpec($1, function(){').
+        replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'this.addHook("$1", function(){').
+        replace(/end(?= |\n|$)/gm, '});').
+        replace(/-{/g, 'function(){').
+        replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
+        replace(/([\s\(]+)\./gm, '$1this.').
+        replace(/\.should([_\.]not)?[_\.](\w+)(?: |$)(.*)$/gm, '.should$1_$2($3)').
+        replace(/(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, 'JSpec.match($1, "$2", "$3", [$4]);')
+    },
+
+    /**
+     * Create a range string which can be evaluated to a native array.
+     *
+     * @param  {int} start
+     * @param  {int} end
+     * @return {string}
+     * @api public
+     */
+
+    range : function(start, end) {
+      current = parseInt(start), end = parseInt(end), values = [current]
+      if (end > current) while (++current <= end) values.push(current)
+      else               while (--current >= end) values.push(current)
+      return '[' + values + ']'
+    },
+
+    /**
+     * Report on the results. 
+     *
+     * @return {JSpec}
+     * @api public
+     */
+
+    report : function() {
+      this.options.formatter ? 
+        new this.options.formatter(this, this.options):
+        new this.formatters.DOM(this, this.options)
+      return this
+    },
+
+    /**
+     * Run the spec suites.
+     *
+     * @return {JSpec}
+     * @api public
+     */
+
+    run : function() {
+      if (option('profile')) console.group('Profile')
+      each(this.suites, function(suite) { this.runSuite(suite) })
+      if (option('profile')) console.groupEnd()
+      return this
+    },
+
+    /**
+     * Run a suite.
+     *
+     * @param  {Suite} suite
+     * @return {JSpec}
+     * @api public
+     */
+
+    runSuite : function(suite) {
+      suite.body()
+      suite.ran = true
+      suite.hook('before')
+      each(suite.specs, function(spec) {
+        suite.hook('before_each')
+        this.runSpec(spec)
+        suite.hook('after_each')
+      })
+      suite.hook('after')
+      if (suite.hasSuites()) {
+        each(suite.suites, function(suite) {
+          this.runSuite(suite)
+        })
+      }
+      return this
+    },
+
+    /**
+     * Run a spec.
+     *
+     * @param  {Spec} spec
+     * @api public
+     */
+
+    runSpec : function(spec) {
+      this.currentSpec = spec
+      this.stats.specs++
+      if (option('profile')) console.time(spec.description)
+      this.evalBody(spec.body, "Error in spec '" + spec.description + "': ")
+      if (option('profile')) console.timeEnd(spec.description)
+      this.stats.assertions += spec.assertions.length
+    },
+
+    /**
+     * Require a dependency, with optional message.
+     *
+     * @param  {string} dependency
+     * @param  {string} message (optional)
+     * @api public
+     */
+
+    requires : function(dependency, message) {
+      try { eval(dependency) }
+      catch (e) { error('depends on ' + dependency + ' ' + (message || '')) }
+    },
+
+    /**
+     * Query against the current query strings keys
+     * or the queryString specified.
+     *
+     * @param  {string} key
+     * @param  {string} queryString
+     * @return {string, null}
+     * @api public
+     */
+
+    query : function(key, queryString) {
+      queryString = (queryString || window.location.search || '').substring(1)
+      return inject(queryString.split('&'), null, function(value, pair){
+        parts = pair.split('=')
+        return  parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
+      })
+    },
+
+    /**
+     * Throw a JSpec related error.
+     *
+     * @param {string} message
+     * @param {Exception} e
+     * @api public
+     */
+
+    error : function(message, e) {
+      throw 'jspec: ' + message + (e ? e.message : '') + ' near line ' + e.line
+    },
+
+    /**
+     * Load a files contents.
+     *
+     * @param  {string} file
+     * @return {string}
+     * @api public
+     */
+
+    load : function(file) {
+      if ('XMLHttpRequest' in this.main) {
+        request = new XMLHttpRequest
+        request.open('GET', file, false)
+        request.send(null)
+        if (request.readyState == 4) return request.responseText
+      }
+      else if ('load' in this.main) {
+        // TODO: workaround for IO issue / preprocessing
+        load(file)
+      }
+      else {
+        error('cannot load ' + file)
+      }