Maurycy / Caravel On the fly Preview

// ==UserScript==
// @name           Caravel On the fly Preview
// @namespace      Mauft.com
// @include        *forum.caravelgames.com/message.php*
// @include        *forum.caravelgames.com/pm.php*
// @description    Displays the preview of your post on the fly, as you are typing
// @version        1.0.0
// @copyright      2011, Maurycy Zarzycki
// ==/UserScript==

var PREVIEW_UPDATE_DELAY = 500;
var EMOTICONS = [
[":\\)","emoticons/smile1.gif"],
[";\\)","emoticons/wink.gif"],
[":D","emoticons/grin.gif"],
[":P","emoticons/tongue.gif"],
[":\\(","emoticons/sad.gif"],
[":~\\(","emoticons/cry.gif"],
[":\\|","emoticons/noexpression.gif"],
[":\\?","emoticons/confused.gif"],
[":\\-O","emoticons/ohmy.gif"],
[":glasses","emoticons/cool1.gif"],
["O:\\-","emoticons/angel.gif"],
["\\-\\_-","emoticons/sleep.gif"],
[":angry","emoticons/angry.gif"],
[":smile","emoticons/smile2.gif"],
[":lol","emoticons/laugh.gif"],
[":cool","emoticons/cool2.gif"],
[":fun","emoticons/fun.gif"],
[":thumbsup","emoticons/thumbsup.gif"],
[":thumbsdown","emoticons/thumbsdown.gif"],
[":blush","emoticons/blush.gif"],
[":weep","emoticons/weep.gif"],
[":unsure","emoticons/unsure.gif"],
[":closedeyes","emoticons/closedeyes.gif"],
[":yes","emoticons/yes.gif"],
[":no","emoticons/no.gif"],
[":huh","emoticons/huh.gif"],
[":w00t","emoticons/w00t.gif"],
[":look","emoticons/look.gif"],
[":rolleyes","emoticons/rolleyes.gif"],
[":kiss","emoticons/kiss.gif"],
[":shifty","emoticons/shifty.gif"],
[":blink","emoticons/blink.gif"],
[":smartass:","emoticons/smartass.gif"],
[":sick","emoticons/sick.gif"],
[":crazy","emoticons/crazy.gif"],
[":wacko","emoticons/wacko.gif"],
[":alien","emoticons/alien.gif"],
[":wizard","emoticons/wizard.gif"],
[":wavecry:","emoticons/wavecry.gif"],
[":wave:","emoticons/wave.gif"],
[":baby","emoticons/baby.gif"],
[":ras","emoticons/ras.gif"],
[":sly","emoticons/sly.gif"],
[":devil","emoticons/devil.gif"],
[":evilmad:","emoticons/evilmad.gif"],
[":evil:","emoticons/evil.gif"],
[":yucky","emoticons/yucky.gif"],
[":nugget","emoticons/nugget.gif"],
[":sneaky","emoticons/sneaky.gif"],
[":smart:","emoticons/smart.gif"],
[":shutup","emoticons/shutup.gif"],
[":yikes","emoticons/yikes.gif"],
[":flowers","emoticons/flowers.gif"],
[":wub","emoticons/wub.gif"],
[":santa","emoticons/santa.gif"],
[":indian","emoticons/indian.gif"],
[":guns","emoticons/guns.gif"],
[":crockett","emoticons/crockett.gif"],
[":zorro","emoticons/zorro.gif"],
[":snap","emoticons/snap.gif"],
[":beer","emoticons/beer.gif"],
[":drunk","emoticons/drunk.gif"],
[":sleeping","emoticons/sleeping.gif"],
[":mama","emoticons/mama.gif"],
[":pepsi","emoticons/pepsi.gif"],
[":medieval","emoticons/medieval.gif"],
[":rambo","emoticons/rambo.gif"],
[":ninja","emoticons/ninja.gif"],
[":hannibal","emoticons/hannibal.gif"],
[":party","emoticons/party.gif"],
[":snorkle","emoticons/snorkle.gif"],
[":evo","emoticons/evo.gif"],
[":king","emoticons/king.gif"],
[":mario","emoticons/mario.gif"],
[":pope","emoticons/pope.gif"],
[":fez","emoticons/fez.gif"],
[":cap","emoticons/cap.gif"],
[":cowboy","emoticons/cowboy.gif"],
[":pirate","emoticons/pirate.gif"],
[":rock","emoticons/rock.gif"],
[":cigar","emoticons/cigar.gif"],
[":icecream","emoticons/icecream.gif"],
[":oldtimer","emoticons/oldtimer.gif"],
[":wolverine","emoticons/wolverine.gif"],
[":strongbench","emoticons/strongbench.gif"],
[":weakbench","emoticons/weakbench.gif"],
[":bike","emoticons/bike.gif"],
[":music","emoticons/music.gif"],
[":book","emoticons/book.gif"],
[":fish","emoticons/fish.gif"],
[":whistle","emoticons/whistling.gif"],
[":hooray","emoticons/hooray.gif"],
[":yay","emoticons/yay.gif"],
[":cake","emoticons/cake.gif"],
[":hbd","emoticons/hbd.gif"],
[":hi","emoticons/hi.gif"],
[":offtopic","emoticons/offtopic.gif"],
[":punk","emoticons/punk.gif"],
[":bounce","emoticons/bounce.gif"],
[":group","emoticons/group.gif"],
[":console","emoticons/console.gif"],
[":eyes","emoticons/eyes.gif"],
[":twak","emoticons/twak.gif"],
[":onei","emoticons/onei.gif"],
[":afro","emoticons/afro.gif"],
[":2alien","emoticons/alien2.gif"],
[":1eye","emoticons/1eye.gif"],
[":band","emoticons/band.gif"],
[":bangin","emoticons/bangin.gif"],
[":banned","emoticons/banned.gif"],
[":batman","emoticons/batman.gif"],
[":blowup","emoticons/blowup.gif"],
[":1alien","emoticons/alien1.gif"],
[":tcaptain","emoticons/captain.gif"],
[":borg","emoticons/borg.gif"],
[":1disguise","emoticons/disguise.gif"],
[":2disguise","emoticons/disguise2.gif"],
[":eek","emoticons/eek.gif"],
[":excl","emoticons/excl.gif"],
[":drool","emoticons/drool.gif"],
[":fear","emoticons/fear.gif"],
[":donatello","emoticons/donatello.gif"],
[":dontgetit","emoticons/dontgetit.gif"],
[":fullmop","emoticons/fullmop.gif"],
[":gathering","emoticons/gathering.gif"],
[":geek","emoticons/geek.gif"],
[":frusty","emoticons/frusty.gif"],
[":glare","emoticons/glare.gif"],
[":gossip","emoticons/gossip.gif"],
[":holiday","emoticons/holiday.gif"],
[":jumpy","emoticons/jumpy.gif"],
[":lamo","emoticons/lamo.gif"],
[":moptop","emoticons/moptop.gif"],
[":nerd","emoticons/nerd.gif"],
[":Notworthy","emoticons/notworthy.gif"],
[":nuke","emoticons/nuke.gif"],
[":paperbag","emoticons/paperbag3.gif"],
[":pimp","emoticons/pimp.gif"],
[":robot","emoticons/robot.gif"],
[":ranting","emoticons/ranting.gif"],
[":scared","emoticons/scared.gif"],
[":2pirate","emoticons/pirate2.gif"],
[":pizza","emoticons/pizza.gif"],
[":shock","emoticons/shock.gif"],
[":pokey","emoticons/pokey.gif"],
[":shhocking","emoticons/shocking.gif"],
[":spamlaser","emoticons/spam_laser.gif"],
[":stretcher","emoticons/stretcher.gif"],
[":surprise","emoticons/surprise.gif"],
[":sweat","emoticons/sweat.gif"],
[":starwars","emoticons/starwars.gif"],
[":tabletalk","emoticons/tabletalk.gif"],
[":tomato","emoticons/tomato.gif"],
[":tongue","emoticons/tongue_ss.gif"],
[":trumpet","emoticons/trumpet.gif"],
[":tvhorror","emoticons/tv_horror.gif"],
[":tvhappy","emoticons/tv_happy.gif"],
[":sombrero","emoticons/sombrero2.gif"],
[":vampire","emoticons/vampire.gif"],
[":whip","emoticons/whip.gif"],
[":yawn","emoticons/yawn.gif"],
[":yinyang","emoticons/yinyang.gif"],
[":bomb","emoticons/bomb.gif"],
[":bond","emoticons/bond.gif"],
[":8bounce","emoticons/bounce8.gif"],
[":cold","emoticons/cold.gif"],
[":disgust","emoticons/disgust1.gif"],
[":2evil","emoticons/evil1.gif"],
[":frankie","emoticons/frankie.gif"],
[":greedy","emoticons/greedy.gif"],
[":ill","emoticons/ill.gif"],
[":lmao","emoticons/lmao.gif"],
[":matrix2","emoticons/matrix_2.gif"],
[":protest","emoticons/protest.gif"],
[":rain","emoticons/rain.gif"],
[":slap","emoticons/slap.gif"],
[":smurf","emoticons/smurf.gif"],
[":throw","emoticons/throw.gif"],
[":brklnk","emoticons/x.gif"],
[":doh","emoticons/doh.gif"],
[":toohard","emoticons/toohard.gif"],
[":selftwak","emoticons/selftwak.gif"],
[":teamtwak","emoticons/teamtwak.gif"],
[":tease","emoticons/tease8le.gif"],
[":stud","emoticons/stud.gif"],
[":yahoo:","emoticons/yahoo.gif"],
[":cheers:","emoticons/cheers.gif"],
[":hmmm:","emoticons/hmmm.gif"],
[":swordfight:","emoticons/swordfight.gif"],
[":tumbleweed:","emoticons/tumbleweed.gif"],
[":facepalm:","emoticons/facepalm.gif"]];

// -----------------------------------------------------------------------
// Copyright (c) 2008, Stone Steps Inc. 
// All rights reserved
// http://www.stonesteps.ca/legal/bsd-license/
//
// This is a BBCode parser written in JavaScript. The parser is intended
// to demonstrate how to parse text containing BBCode tags in one pass 
// using regular expressions.
//
// The parser may be used as a backend component in ASP or in the browser, 
// after the text containing BBCode tags has been served to the client. 
//
// Following BBCode expressions are recognized:
//
// [b]bold[/b]
// [i]italic[/i]
// [u]underlined[/u]
// [s]strike-through[/s]
// [samp]sample[/samp]
//
// [color=red]red[/color]
// [color=#FF0000]red[/color]
// [size=1.2]1.2em[/size]
//
// [url]http://blogs.stonesteps.ca/showpost.asp?pid=33[/url]
// [url=http://blogs.stonesteps.ca/showpost.asp?pid=33][b]BBCode[/b] Parser[/url]
//
// [q=http://blogs.stonesteps.ca/showpost.asp?pid=33]inline quote[/q]
// [q]inline quote[/q]
// [blockquote=http://blogs.stonesteps.ca/showpost.asp?pid=33]block quote[/blockquote]
// [blockquote]block quote[/blockquote]
//
// [pre]formatted 
//     text[/pre]
// [code]if(a == b) 
//   print("done");[/code]
//
// text containing [noparse] [brackets][/noparse]
//
// -----------------------------------------------------------------------
var opentags;           // open tag stack
var crlf2br = true;     // convert CRLF to <br>?
var noparse = false;    // ignore BBCode tags?
var urlstart = -1;      // beginning of the URL if zero or greater (ignored if -1)

// aceptable BBcode tags, optionally prefixed with a slash
var tagname_re = /^\/?(?:b|i|u|code|color|secret|SECRET|size|url|s|img|quote|lb|rb)$/;

// color names or hex color
var color_re = /^(red|green|blue|yellow|orange)$/i;

// numbers
var number_re = /^(\-2|\-1|\+1|\+2)/i;

// reserved, unreserved, escaped and alpha-numeric [RFC2396]
var uri_re = /^[-;\/\?:@&=\+\$,_\.!~\*'\(\)%0-9a-z]{1,512}$/i;

// main regular expression: CRLF, [tag=option], [tag] or [/tag]
var postfmt_re = /([\r\n])|(?:\[([a-z]{1,16})(?:=([^\x00-\x1F"'\(\)<>\[\]]{1,256}))?\])|(?:\[\/([a-z]{1,16})\])/ig;

var secret_count = 100;

// stack frame object
function taginfo_t(bbtag, etag)
{
   this.bbtag = bbtag;
   this.etag = etag;
}

// check if it's a valid BBCode tag
function isValidTag(str)
{
   if(!str || !str.length)
      return false;

   return tagname_re.test(str);
}

function getSecretContentOpen(){
  
}

//
// m1 - CR or LF
// m2 - the tag of the [tag=option] expression
// m3 - the option of the [tag=option] expression
// m4 - the end tag of the [/tag] expression
//
function textToHtmlCB(mstr, m1, m2, m3, m4, offset, string)
{
   var i;
   //
   // CR LF sequences
   //
   if(m1 && m1.length) {
      if(!crlf2br)
         return mstr;

      switch (m1) {
         case '\r':
            return "";
         case '\n':
            return "<br>";
      }
   }

   //
   // handle start tags
   //
   if(isValidTag(m2)) {
      // if in the noparse state, just echo the tag
      if(noparse)
         return "[" + m2 + "]";

      // ignore any tags if there's an open option-less [url] tag
      if(opentags.length && opentags[opentags.length-1].bbtag == "url" && urlstart >= 0)
         return "[" + m2 + "]";

      switch (m2) {
         case "code":
            opentags.push(new taginfo_t(m2, "</pre>"));
            crlf2br = false;
            return "<pre>";

         case "color":
            if(!m3)
              return "[" + m2 + "]";
            else if (!color_re.test(m3))
              return "[" + m2 + "=" + m3 + "]"; 
              
            opentags.push(new taginfo_t(m2, "</span>"));
            return "<span style=\"color: " + m3 + "\">";
            
         case "secret":
            opentags.push(new taginfo_t(m2, "</div><br>"));
            
            i = secret_count;
            secret_count += 1;
  
            return ("<div onclick=\"document.getElementById('secret"+i+"').style.display='block';"+
              "document.getElementById('secretclick"+i+"').style.display='none';\""+
              "id='secretclick"+i+"' class='secretclick1'>"+
              "Click here to view the secret text</div>"+
              "<div class='secret1' style='display: none;' id='secret"+i+"'>"+
              "<span onclick=\"document.getElementById('secretclick"+i+"').style.display='block';"+
              "document.getElementById('secret"+i+"').style.display='none';\" style='border: none; padding-right: 4px' class='secretclick'>×"+
              "</span>");
              
         case "SECRET":
            opentags.push(new taginfo_t(m2, '</font></td></tr><tr><td><font size="2">(Highlight the secret text above.)</font></td></tr></tbody></table>')); 
            return '<table><tbody><tr><td bgcolor="white"><font color="white">';
            
         case "size":
            if(!m3)
              return "[" + m2 + "]";
            else if (!number_re.test(m3))
              return "[" + m2 + "=" + m3 + "]"; 
            
            switch(m3){
              case("-2"): m3 = "60%"; break;
              case("-1"): m3 = "80%"; break;
              case("+1"): m3 = "120%"; break;
              case("+2"): m3 = "135%"; break;
            }
            opentags.push(new taginfo_t(m2, "</span>"));
            return "<span style=\"font-size: " + m3 + "\">";

         case "s":
            opentags.push(new taginfo_t(m2, "</span>"));
            return "<span style=\"text-decoration: line-through\">";

         case "noparse":
            noparse = true;
            return "";

         case "url":
            opentags.push(new taginfo_t(m2, "</a>"));
            
            // check if there's a valid option
            if(m3 && uri_re.test(m3)) {
               // if there is, output a complete start anchor tag
               urlstart = -1;
               return "<a href=\"" + m3 + "\">";
            }

            // otherwise, remember the URL offset 
            urlstart = mstr.length + offset;

            // and treat the text following [url] as a URL
            return "<a href=\"";

         case "quote":
            opentags.push(new taginfo_t(m2, "<hr></blockquote>"));
            if (!m3){
              return '<blockquote><font size="1">quote:</font><hr>';
            } else {
              return '<blockquote><font size="1">quote:</font><hr><b>'+m3+' wrote:</b>';
            }
            
         case "img":
            opentags.push(new taginfo_t(m2, '">'));
            return '<img border="0" src="';
            
          case "lb":
            return "[";
            
          case "rb":
            return "]";

         default:
            // [samp], [b], [i] and [u] don't need special processing
            opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
            return "<" + m2 + ">";
            
      }
   }

   //
   // process end tags
   //
   if(isValidTag(m4)) {
      if(noparse) {
         // if it's the closing noparse tag, flip the noparse state
         if(m4 == "noparse")  {
            noparse = false;
            return "";
         }
         
         // otherwise just output the original text
         return "[/" + m4 + "]";
      }
      
      // highlight mismatched end tags
      if(!opentags.length || opentags[opentags.length-1].bbtag != m4)
         return "[/" + m4 + "]";

      if(m4 == "url") {
         // if there was no option, use the content of the [url] tag
         if(urlstart > 0)
            return "\">" + string.substr(urlstart, offset-urlstart) + opentags.pop().etag;
         
         // otherwise just close the tag
         return opentags.pop().etag;
      }
      else if(m4 == "code" || m4 == "pre")
         crlf2br = true;

      // other tags require no special processing, just output the end tag
      return opentags.pop().etag;
   }

   return mstr;
}

//
// post must be HTML-encoded
//
function parseBBCode(post)
{
   var result, endtags, tag, i, l;
   
   secret_count = 100;

   // convert CRLF to <br> by default
   crlf2br = true;

   // create a new array for open tags
   if(opentags == null || opentags.length)
      opentags = new Array(0);

   // run the text through main regular expression matcher
   result = post.replace(postfmt_re, textToHtmlCB);

   // reset noparse, if it was unbalanced
   if(noparse)
      noparse = false;
   
   // if there are any unbalanced tags, make sure to close them
   if(opentags.length) {
      endtags = new String();
      
      // if there's an open [url] at the top, close it
      if(opentags[opentags.length-1].bbtag == "url") {
         opentags.pop();
         endtags += "\">" + post.substr(urlstart, post.length-urlstart) + "</a>";
      }
      
      // close remaining open tags
      while(opentags.length)
         endtags += opentags.pop().etag;
   }

    i = 0;
    l = EMOTICONS.length;
    for(i; i < l; i++){
      result = result.replace(new RegExp(EMOTICONS[i][0], 'g'),
        "<img align='absmiddle' src='"+EMOTICONS[i][1]+"' border='0' title='"+EMOTICONS[i][0]+"' alt='"+EMOTICONS[i][0]+"'>");
    }

   return endtags ? result + endtags : result;
}

function init() {
  // quit if this function has already been called
  if (arguments.callee.done) return;

  // flag this function so we don't do the same thing twice
  arguments.callee.done = true;

  // kill the timer
  if (_timer) clearInterval(_timer);

  initParser();
};

/* for Mozilla/Opera9 */
if (document.addEventListener) {
  document.addEventListener("DOMContentLoaded", init, false);
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
  document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
  var script = document.getElementById("__ie_onload");
  script.onreadystatechange = function() {
    if (this.readyState == "complete") {
      init(); // call the onload handler
    }
  };
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
  var _timer = setInterval(function() {
    if (/loaded|complete/.test(document.readyState)) {
      init(); // call the onload handler
    }
  }, 10);
}

/* for other browsers */
window.onload = init;



// :::::::::::::::::::::::::::::::
// :: ACTUAL CODE
// :::::::::::::::::::::::::::::::

var box, container;
function initParser(){  
  box = getBox();//document.getElementById("message");
  if (!box)
    return;
    
  container = getBoxContainer(box);
  if (!container)
    return;
  
  var throttleFunction = throttle(doParse, PREVIEW_UPDATE_DELAY);
  
  if(typeof box.addEventListener == 'function') {
    box.addEventListener("keyup", throttleFunction);
    box.addEventListener("focus", throttleFunction);
    box.addEventListener("blur", throttleFunction);
    box.addEventListener("change", throttleFunction);
  } else {
    box.attachEvent("keyup", throttleFunction);
    box.attachEvent("focus", throttleFunction);
    box.attachEvent("blur", throttleFunction);
  }
  
  doParse();
}

function doParse(){
  container.innerHTML = parseBBCode(box.value);
}

function getBox(){
  var box = document.getElementById("message");
  if (box)
    return box;
    
  box = document.getElementsByTagName("textarea");
  var i = 0;
  var l = box.length;
  
  for(;i < l; i++){
    if (box[i].getAttribute('name') == 'message')
      return box[i];
  }
}

function getBoxContainer(item){
  var bigTr;
  var msgTd;
  var msgTable;
  var itemName;
  var newTable;
  
  var html = document.createElement('table');
  
  html.innerHTML = getNewTableHTML();
  html = html.firstChild.firstChild;
  
  while (item.parentNode){
    item = item.parentNode;
    
    itemName = item.nodeName.toLowerCase();
    
    if (itemName == "table" && !msgTable){
      msgTable = item;
    }else if (itemName == "td" && msgTable){
      msgTd = item;
    } else if (itemName == "tr" && msgTable){
      bigTr = item;
      break;
    }
  }
  
  if (!bigTr)
    return null;
  
  bigTr.parentNode.insertBefore(html, bigTr.nextSibling);
  
  return document.getElementById('previewMagic');
}

function getNewTableHTML(){
  return ('<tbody><tr valign="top" class="message1">' +
  	 '<td width="130" valign="top" align="left"><b>Preview:</b></td>' +
  		'<td id="previewMagic"valign="top">' +
  		'</td>' +
  	'</tr></tbody>');
}

function getNewTableHTML_(){
  return ('<tbody><tr valign="top" class="message1">' +
  	 '<td width="130" valign="top" align="left"><b>Preview:</b></td>' +
  		'<td width="100%" valign="top" nowrap="">' +
  			'<table width="100%"><tbody><tr>' +
  				'<td id="previewMagic" align="left" rowspan="2"></td>' +
  			'</tr>' +
  			'</tbody></table>' +
  		'</td>' +
  	'</tr></tbody>');
}

function throttle(f, delay){
    var timer = null;
    return function(){
        var context = this, args = arguments;
        clearTimeout(timer);
        timer = window.setTimeout(function(){
            f.apply(context, args);
        },
        delay || 500);
    };
}