/* Copyright © 2004,2005 Charles C. Fu <URL:http://www.web-i18n.net/> */

var listeners;
var spanTemplate = document.createElement("span");
var divTemplate = document.createElement("div");
var rubyTemplate = document.createElement("ruby");
var rbTemplate = document.createElement("rb");
var rtTemplate = document.createElement("rt");
var rpTemplate = document.createElement("rp");
var rbcTemplate = document.createElement("rbc");
var rtcTemplate = document.createElement("rtc");
var iframeTemplate = document.createElement("iframe");
var pleasedo = String(document.location).match(/(?:%7[cC]|\|)pleasedo=/);
var isTest = false;
/*
isTest = String(document.location).match(/fjunk/)
  != null;
*/

/* Get value from self or first child or first child's first child or .... */
function descendNodeValue(node) {
  if (node.nodeValue)
    return node.nodeValue;
  if (node.hasChildNodes())
    return descendNodeValue(node.firstChild);
  return null;
}

function setLang(node, lang) {
  node.lang = lang;
  /* Workaround IE's lack of lang selector support. */
  appendClass(node, lang);
}

/* Append to class attribute. */
function appendClass(node, moreClass) {
  if (! node)
    return;
  /* IE requires node.className.  Changes made using
   * node.setAttribute("class", ...) are recorded but do not trigger CSS
   * selectors. */
  var oldClass = node.className;
  node.className = (node.className
		    ? node.className + " " + moreClass
		    : moreClass);
}

/* For some reason, Firefox's document.getElementsByTagName is
 * sometimes temporarily undefined when called from an event handler.
 * So, we cache the method's value and issue a call to the cached
 * method in case the temporary undef is encountered. */
var getElementsByTagName = document.getElementsByTagName;
function nodeGetElementsByTagName(node, tagName) {
  if (node.getElementsByTagName)
    return node.getElementsByTagName(tagName);
  return getElementsByTagName.apply(node, [tagName]);
}

function replaceElements(node, descendentOfMatchedSequence, replacementTmpl,
			 outerName) {
  var outers = nodeGetElementsByTagName(node, outerName);
  for (var i = outers.length - 1; i >= 0; --i)
    replaceElement(outers[i], descendentOfMatchedSequence, replacementTmpl);
}

function replaceChildren(parent, replacementTmpl) {
  for (var child = parent.firstChild, nextSib; child; child = nextSib) {
    nextSib = child.nextSibling;
    replaceElement(child, nop, replacementTmpl);
  }
}

/* Replace <outer>...</outer> where descendentOfMatchedSequence is valid with
 * <replacement>...</replacement>. */
function replaceElement(outer, descendentOfMatchedSequence, replacementTmpl) {
  var inner = descendentOfMatchedSequence(outer);
  if (! inner)
    return;
  var replacement = replacementTmpl.cloneNode(true);
  var frag = document.createDocumentFragment();
  var parsingAttribs = true;
  var rubyBaseDone = false;
  /* With IE, mismatched start and end tags can result in
   * element.hasChildNodes() true yet element.firstChild == null. */
  for (var child = inner.firstChild, nextSib; child; child = nextSib) {
    nextSib = child.nextSibling;
    var attribute = parsingAttribs && descendentOfAttributeSequence(child);
    if (attribute) {
      /* Attribute value missing.  Node incomplete?  Return so we can
       * try again later in case being called from mutation listener.
       * Shouldn't occur since no longer do replacements mid-post. */
      if (! nextSib)
	return;
      var value = trim(descendNodeValue(nextSib));
      nextSib = nextSib.nextSibling;
      switch (trim(descendNodeValue(attribute))) {
      case "xml:lang":
	setLang(replacement, value);
	break;
      case "class":		// safe coreattrs
	appendClass(replacement, value);
	break;
      case "title":
	replacement.title = value;
	break;
      case "dir":		// safe i18n
	replacement.dir = value;
	break;
      case "rbspan":
	replacement.rbspan = value;
	break;
      case "height":
	replacement.style.height = value;
	break;
      case "width":
	replacement.style.width = value;
	break;
      case "background":
	replacement.style.background = value;
	break;
      case "src":
	replacement = iframeTemplate.cloneNode(true);
	replacement.src = value;
	break;
      default:
	frag.appendChild
	  (document.createTextNode
	   ("Attribute must be xml:lang, class, background, title, or dir!"));
      }
    } else {
      parsingAttribs = false;
      if (descendentOfMatchedSequence == descendentOfRubySequence) {
	/* Ruby element content value missing.  Node incomplete?
	 * Return so we can try again later in case being called from
	 * mutation listener. Shouldn't occur since no longer do
	 * replacements mid-post. */
	if (! nextSib)
	  return;
	switch (child.tagName.toLowerCase()) {
	case "b":
	  frag.appendChild(child);
	  replaceElement(child, nop, rbTemplate);
	  if (nextSib == inner.lastChild) {
	    frag.appendChild(nextSib);
	    replaceElement(nextSib, nop, rtTemplate);
	  } else {
	    var rt = nextSib.nextSibling;
	    var rpClose = rt.nextSibling;
	    frag.appendChild(nextSib);
	    replaceElement(nextSib, nop, rpTemplate);
	    frag.appendChild(rt);
	    replaceElement(rt, nop, rpTemplate);
	    frag.appendChild(rpClose);
	    replaceElement(rpClose, nop, rpTemplate);
	  }
	  break;
	case "u":
	  var rtc2 = nextSib.nextSibling;
	  frag.appendChild(child);
	  replaceChildren(replaceElement(child, nop, rbcTemplate), rbTemplate);
	  frag.appendChild(nextSib);
	  replaceChildren(replaceElement(nextSib, nop, rtcTemplate),
			  rtTemplate);
	  if (rtc2) {
	    frag.appendChild(rtc2);
	    rtc2 = replaceElement(rtc2, nop, rtcTemplate);
	    appendClass(rtc2, "hideFromIE");
	    replaceChildren(rtc2, rtTemplate);
	  }
	  break;
	default:
	  alert("Illegal element inside [ruby]");
	  return;
	}
	break;
      } else
	frag.appendChild(child);
    }
  }

  if (frag.firstChild != null)
    replacement.appendChild(frag);
  replacement.replacedElement = true; 
  outer.parentNode.replaceChild(replacement, outer);
  return replacement;
}

/* I I I I */
function descendentOfAttributeSequence(node) {
  try {
    if (node.tagName.toLowerCase() == "i"
	&& node.firstChild.tagName.toLowerCase() == "i"
	&& node.firstChild.firstChild.tagName.toLowerCase() == "i"
	&& node.firstChild.firstChild.firstChild.tagName.toLowerCase() == "i")
      return node.firstChild.firstChild.firstChild;
    return null;
  } catch (e) {
    return null;
  }
}

/* (U) U U U */
function descendentOfSpanSequence(node) {
  try {
    if (! node.replacedElement
	&& node.firstChild.tagName.toLowerCase() == "u"
	&& ! node.firstChild.replacedElement
	&& node.firstChild.firstChild.tagName.toLowerCase() == "u"
	&& ! node.firstChild.firstChild.replacedElement
	&& node.firstChild.firstChild.firstChild.tagName.toLowerCase() == "u"
	&& ! node.firstChild.firstChild.firstChild.replacedElement)
      return node.firstChild.firstChild.firstChild;
    return null;
  } catch (e) {
    return null;
  }
}

/* (B) B B B */
function descendentOfBlockSequence(node) {
  try {
    if (! node.replacedElement) {
      var firstChild = node.firstChild;
      if (firstChild.tagName.toLowerCase() == "b"
	  && ! firstChild.replacedElement) {
	firstChild = firstChild.firstChild;
	if (firstChild.tagName.toLowerCase() == "b"
	    && ! firstChild.replacedElement) {
	  firstChild = firstChild.firstChild;
	  if (firstChild.tagName.toLowerCase() == "b"
	      && ! firstChild.replacedElement)
	    return firstChild;
	}
      }
    }
  } catch (e) { }
  return null;
}

/* (U) U B U */
function descendentOfRubySequence(node) {
  try {
    if (! node.replacedElement) {
      var firstChild = node.firstChild;
      if (firstChild.tagName.toLowerCase() == "u"
	  && ! firstChild.replacedElement) {
	firstChild = firstChild.firstChild;
	if (firstChild.tagName.toLowerCase() == "b"
	    && ! firstChild.replacedElement) {
	  firstChild = firstChild.firstChild;
	  if (firstChild.tagName.toLowerCase() == "u"
	      && ! firstChild.replacedElement)
	    return firstChild;
	}
      }
    }
  } catch (e) { }
  return null;
}

function nop(node) {
  return node;
}

/* Try to add text surrounded by prepend/append in a number of ways. */
function addElement(prepend, append) {
  saveTextarea();
  var a = document.forms.msgform.elements.body; // reply textarea
  if (a.selectionStart != null) // Moz
    insertSelectionMoz(a, prepend, append);
  else if (document.selection) // IE/Win32
    insertSelectionIEWin32(a, prepend, append);
  else {
    if (a.getSelection)
      a.value += prepend + getSelection() + append;
    else
      a.value += prepend + append;
    a.focus();
  }
}

function replaceSelection(replaceFun) {
  saveTextarea();
  var a = document.forms.msgform.elements.body; // reply textarea
  if (a.selectionStart != null) // Moz
    replaceSelectionMoz(a, replaceFun);
  else if (document.selection) // IE/Win32
    replaceSelectionIEWin32(a, replaceFun);
}

/* For Mozilla family browsers, replace selected textarea text with
 * selected text surrounded by prepend/append.  If no other text is
 * selected, just surround selected textarea text. */
function insertSelectionMoz(a, prepend, append) {
  sel = window.savedSelection;
  if (sel == "")
    return surroundMozTextareaSelection(a, prepend, append);

  /* Replaces selected textarea text with (prepend+[non-textarea] selected
   * text+append).  Leaves inserted selected text selected. */
  var selLength = sel.length;
  var start = a.selectionStart;
  var end = a.selectionEnd;
  var t = a.value;              // textarea text

  var scrollTop = a.scrollTop;
  var scrollLeft = a.scrollLeft;
  a.value = t.substring(0, start) + prepend + sel + append + t.substring(end);
  a.scrollTop = scrollTop;
  a.scrollLeft = scrollLeft;

  a.selectionStart = start + prepend.length;
  a.selectionEnd = start + prepend.length + selLength;
  a.focus();
}

function replaceSelectionMoz(a, replaceFun) {
  sel = window.savedSelection;
  if (sel == "")
    return replaceMozTextareaSelection(a, replaceFun);

  /* Replaces selected textarea text with (prepend+[non-textarea] selected
   * text+append).  Leaves inserted selected text selected. */
  var selLength = sel.length;
  var start = a.selectionStart;
  var end = a.selectionEnd;
  var t = a.value;              // textarea text

  var scrollTop = a.scrollTop;
  var scrollLeft = a.scrollLeft;
  a.value = t.substring(0, start) + replaceFun(sel) + t.substring(end);
  a.scrollTop = scrollTop;
  a.scrollLeft = scrollLeft;

  a.selectionStart = start;
  a.selectionEnd = start + selLength;
  a.focus();
}

/* Replaces selected textarea text with (prepend+selected
 * text+append).  That is, it surrounds the selected text with
 * prepend/append.  Leaves surrounded text selected. */
function surroundMozTextareaSelection(a, prepend, append) {
  var start = a.selectionStart;
  var end = a.selectionEnd;
  var t = a.value;              // textarea text

  var scrollTop = a.scrollTop;
  var scrollLeft = a.scrollLeft;
  a.value = (t.substring(0, start)
             + prepend + t.substring(start, end) + append
             + t.substring(end));
  a.scrollTop = scrollTop;
  a.scrollLeft = scrollLeft;

  if (a.setSelectionRange)
    a.setSelectionRange(start + prepend.length, end + prepend.length);
  else {
    a.selectionStart = start + prepend.length;
    a.selectionEnd = end + prepend.length;
  }
  a.focus();
}

function replaceMozTextareaSelection(a, replaceFun) {
  var start = a.selectionStart;
  var end = a.selectionEnd;
  var t = a.value;              // textarea text

  var scrollTop = a.scrollTop;
  var scrollLeft = a.scrollLeft;
  var replacement = replaceFun(t.substring(start, end));
  a.value = (t.substring(0, start)
	     + replacement
             + t.substring(end));
  a.scrollTop = scrollTop;
  a.scrollLeft = scrollLeft;

  if (a.setSelectionRange)
    a.setSelectionRange(start, start + replacement.length);
  else {
    a.selectionStart = start;
    a.selectionEnd = start + replacement.length;
  }
  a.focus();
}

/* Stores IE caret position. */
function storeIECaret(el) {
  var el = document.forms.msgform.elements.body;
  if (el.createTextRange)
    el.caretPos = document.selection.createRange().duplicate();
}

/* Replaces selected textarea text (text at caret) with
 * (prepend+selected text+append).  Leaves inserted selected text
 * selected. */
function insertSelectionIEWin32(a, prepend, append) {
  if (! a.caretPos) {
    /* IE6/Win32 initially places cursor at the start of textareas. */
    a.caretPos = a.createTextRange();
    a.caretPos.collapse();
  }
  var insertText = document.savedSelection;
  /* Can't calc on insertText.length since that counts CR/LF as 2 but
   * caretPos counts it as 1. */
  a.caretPos.text = prepend;
  var afterPrepend = a.caretPos.duplicate();
  a.caretPos.text = insertText + append;
  a.caretPos.move("character", - append.length);
  a.caretPos.setEndPoint("StartToStart", afterPrepend);
  a.caretPos.select();
}

function replaceSelectionIEWin32(a, replaceFun) {
  if (! a.caretPos) {
    /* IE6/Win32 initially places cursor at the start of textareas. */
    a.caretPos = a.createTextRange();
    a.caretPos.collapse();
  }
  var insertText = document.savedSelection;
  /* Can't calc on insertText.length since that counts CR/LF as 2 but
   * caretPos counts it as 1. */
  //  a.caretPos.text = '';
  //  var strart = a.caretPos.duplicate();
  a.caretPos.text = replaceFun(insertText);
  //  a.caretPos.setEndPoint("StartToStart", start);
  a.caretPos.select();
}

function selectionLengthIE(sel) {
  var r = sel.duplicate();
  r.collapse();
  var len = 0;
  while (! r.isEqual(sel)) {
    r.moveEnd("character", 1);
    ++len;
  }
  return len;
}

function saveTextarea(undoOrRedo) {
  var a = document.forms.msgform.elements.body; // reply textarea
  var state = new Object();
  state.value = a.value;
  if (window.getSelection)
    window.savedSelection = window.getSelection().toString();
  if (document.selection)
    document.savedSelection = document.selection.createRange().text;
  if (a.selectionStart) {
    state.selectionStart = a.selectionStart;
    state.selectionEnd = a.selectionEnd;
  }
  if (a.caretPos) {
    state.offsetLeft = a.caretPos.offsetLeft;
    state.offsetTop = a.caretPos.offsetTop;
    state.length = selectionLengthIE(a.caretPos);
  }
  a.focus();

  if (! undoOrRedo)
    undoOrRedo = "undo";
  var btn = document.getElementById(undoOrRedo);
  if (btn) {
    btn.saved.push(state);
    btn.disabled = false;
  }
}

function restoreTextarea(state) {
  var a = document.forms.msgform.elements.body; // reply textarea
  a.value = state.value;
  if (state.selectionStart) {
    a.selectionStart = state.selectionStart;
    a.selectionEnd = state.selectionEnd;
  }
  a.focus();

  if (state.offsetLeft) {
    a.caretPos.moveToPoint(state.offsetLeft, state.offsetTop);
    a.caretPos.moveEnd("character", state.length);
    a.caretPos.select();
  }
}

function undoQCode() {
  var a = document.forms.msgform.elements.body; // reply textarea
  var undo = document.getElementById("undo");
  saveTextarea("redo");
  restoreTextarea(undo.saved.pop());
  if (! undo.saved.length)
    undo.disabled = true;
}

function redoQCode() {
  var a = document.forms.msgform.elements.body; // reply textarea
  saveTextarea("undo");
  var redo = document.getElementById("redo");
  restoreTextarea(redo.saved.pop());
  if (! redo.saved.length)
    redo.disabled = true;
}

/* Locate posting buttons. */
function firstQCodeButton() {
  var inputs = nodeGetElementsByTagName(document.body, "input");
  if (! inputs)
    return null;
  for (var i = 0; i < inputs.length; ++i)
    if (inputs[i].className == "qcode_bar_but")
      return inputs[i];
  return null;
}

function setQCodeButton(id, value, lang) {
  var node = document.getElementById(id);
  if (!node)
    return;
  if (lang && lang != "")
    setLang(node, lang);
  if (node.tagName.toLowerCase() == 'option')
    node.text = value;
  else
    node.value = value;
}

/* Move hidden children from hidden form to posting form. */
function movePostFormAdditions() {
  if (! pleasedo || document.postFormAdditionsMoved)
    return;
  var input = firstQCodeButton();
  if (! input)
    return;

  var formAddition = document.createElement("div");
  formAddition.innerHTML =
    "<hr />"
    + "<form method='post' action='http://imageshack.us/index.php'"
    +      " enctype='multipart/form-data' target='_blank'>"
    +   "<div>"
    +     "<input type='submit' value='↑ Upload Image' />"
    +     "<input type='hidden' name='MAX_FILE_SIZE' value='1048576' />"
    +     "<input type='file' name='fileupload' class='file' />"
    +     "<a id='rules'"
    +       " href='http://reg.imageshack.us/content.php?page=rules'>rules</a>"
    + 	   "<table>"
    +        "<tr>"
    +          "<td class='instructions'>"
    + 	     	 "To post an image from your computer:"
    + 	     	 "<ol>"
    + 	     	   "<li>Click <strong>Browse</strong></li>"
    + 	     	   "<li>Choose an image</li>"
    + 	     	   "<li>Click <strong>Upload Image</strong></li>"
    + 	     	   "<li>Copy text to left of"
    +        	     " <strong>Hotlink for forums (1)</strong>"
    +        	     "<ul><li><strong>Thumbnail for forums (1)</strong>"
    +        	     " is good for images &gt;200kB</li></ul></li>"
    + 	     	   "<li>Paste it in your post below</li>"
    + 	     	 "</ol>"
    +          "</td>"
    +          "<td class='instructions'>"
    + 	     	 "To use CJK's thumbnails instead of ImageShack's:"
    + 	     	 "<ol>"
    + 	     	   "<li>Follow steps 1–5 to the left</li>"
    + 	     	   "<li>"
    +        	     "Firefox/Mozilla/IE users,"
    +                " highlight the [IMG]…[/IMG] text."
    +        	   "</li>"
    + 	     	   "<li>Press the <strong>span</strong> button.</li>"
    + 	     	   "<li>Select <em>thumbnail</em> from the"
    +        	       " <strong>class</strong> list</li>"
    + 	     	   "<li>Move mouse over thumbnail to see full size</li>"
    + 	     	 "</ol>"
    +          "</td>"
    +        "</tr>"
    +      "</table>"
    +   "</div>"
    + "</form>"
    + "<hr class='clear'/>"
    + "<div class='smileys'>"
    +   "<img src='http://CJKDramas.com/img/smileys.png'"
    +      " alt='' onclick='addSmiley(event)' />"
    + "</div>"
    + "<div xml:lang='en-US' lang='en-US'>"
    +   "<input type='button' class='SUR' value='Save'"
    +     " onclick='saveTextarea()' />"
    +   "<input type='button' class='SUR' value='Undo' id='undo'"
    +         " onclick='undoQCode()' disabled='disabled' />"
    +   "<input type='button' class='SUR' value='Redo' id='redo'"
    +         " onclick='redoQCode()' disabled='disabled' />"
    +   "<br />"
    +   "<input type='button' value='b' title='bold'"
    +         " style='font-weight:bold'"
    +         " onclick='addElement(\"[b]\", \"[/b]\")' />"
    +   "<input type='button' value='i' title='italic'"
    +         " style='font-style:italic'"
    +         " onclick='addElement(\"[i]\", \"[/i]\")' />"
    +   "<input type='button' value='u' title='underline'"
    +         " style='text-decoration:underline'"
    +         " onclick='addElement(\"[u]\", \"[/u]\")' />"
    +   "<select onclick='addElement(options[selectedIndex].value.charAt(0),options[selectedIndex].value.charAt(1))'>"
    +     "<option selected='selected'>“”</option>"
    +     "<option>‘’</option>"
    +     "<option>「」</option>"
    +   "</select>"
    +   "<select onclick='addElement(options[selectedIndex].value,\"\")'>"
    +     "<option selected='selected' value='Ω'>Special Chars: Ω</option>"
    +     "<option lang='en-US' xml:lang='en-US' value='…'>"
    +       "Ellipsis: …</option>"
    +     "<option value=' '>Non-breaking SP</option>"
    +     "<optgroup label='Dashes'>"
    +       "<option value='—'>Em Dash: —</option>"
    +       "<option value='–'>En Dash: –</option>"
    +       "<option value='−'>Minus: −</option>"
    +     "</optgroup>"
    +     "<optgroup label='Precomposed'>"
    +       "<option value='ā'>ā</option>"
    +       "<option value='ī'>ī</option>"
    +       "<option value='ū'>ū</option>"
    +       "<option value='ē'>ē</option>"
    +       "<option value='ō'>ō</option>"
    +     "</optgroup>"
    +   "</select>"
    +   "<button onclick='replaceSelection(toPinyinWithToneMarks)'"
    +    " title='tone numbers to tone marks'>"
    +     "<ruby>"
    +       "<rb xml:lang='zh-CN' lang='zh-CN' class='zh-CN'>"
    +         "符号和拼音"
    +       "</rb>"
    +       "<rt>tone marks</rt>"
    +     "</ruby>"
    +   "</button>"
    +   "<br />"
    +   "<button onclick='addElement(\"[span][class=reveal/][img]http://CJKDramas.com/img/spoiler.png[/img][/span][span][class=spoiler/]\",\"[/span]\")'"
    +          " title='Change [span] to [p] or [div] if desired'>"
    +     "<img src='http://CJKDramas.com/img/spoiler.png'"
    +       " alt='spoiler revealer' /></button>"
    +   "<input type='button' value='div' title='block of content'"
    +         " onclick='addElement(\"[div]\",\"[/div]\")' />"
    +   "<input type='button' value='span' title='inline content'"
    +         " onclick='addElement(\"[span]\",\"[/span]\")' />"
    +   "<input type='button' value='¶' title='paragraph' id='qcode-p'"
    +         " onclick='addElement(\"[p]\",\"[/p]\")' />"
    +   "<button onclick='addElement(\"[list]\n[*]\",\"\n[/list]\")'"
    +         "  title='bulleted list'>"
    +     "<img src='http://CJKDramas.com/img/list-disc.png'"
    +       " alt='bulleted list' /></button>"
    +   "<br />"
    + "<div class='grp'>"
    +   "<div>These attributes must be within a [div], [span], or [p].</div>"
    +   "<select onclick='addElement(\"[xml:lang=\"+options[selectedIndex].value+\"/]\",\"\")'>"
    +     "<option value='' selected='selected'>Language</option>"
    +     "<option value='en-US'>American English</option>"
    +     "<option title='Japanese'"
    +       " value='ja-JP' xml:lang='ja-JP' lang='ja-JP' class='ja-JP'>"
    +       "日本語</option>"
    +     "<option title='Chinese (Taiwan, international)'"
    +       " value='zh-TW' xml:lang='zh-TW' lang='zh-TW' class='zh-TW'>"
    +       "中文(繁)</option>"
    +     "<option title='Korean'"
    +       " value='ko-KR' xml:lang='ko-KR' lang='ko-KR' class='ko-KR'>"
    +       "한국어</option>"
    +     "<option value='en'>English (generic)</option>"
    +     "<option title='Chinese (China)'"
    +       " value='zh-CN' xml:lang='zh-CN' lang='zh-CN' class='zh-CN'>"
    +       "中文(简)</option>"
    +     "<option title='Chinese (Hong Kong)'"
    +            " value='zh-HK' xml:lang='zh-HK' lang='zh-HK' class='zh-HK'>"
    +       "中文(香港)</option>"
    +   "</select>"
    +   "<select onclick='addElement(\"[class=\"+options[selectedIndex].value+\"/]\",\"\")'>"
    +     "<optgroup label='in div/span/p'>"
    +       "<option value='' selected='selected'>class</option>"
    +       "<option title='Make this text larger'"
    +              " value='larger'>larger</option>"
    +       "<option title='Make this text smaller'"
    +              " value='smaller'>smaller</option>"
    +       "<option title='Overline text'"
    +              " value='overline'>overline</option>"
    +       "<option title='Line-through text'"
    +              " value='line-through'>line-through</option>"
    +       "<option title='Blink text'"
    +              " value='blink'>blink</option>"
    +       "<option value='hide-br'>hide breaks</option>"
    +       "<option title='Float this item towards the left margin'"
    +              " value='float-left'>float left</option>"
    +       "<option title='Float this item towards the right margin'"
    +              " value='float-right'>float right</option>"
    +       "<option title='Indictes phonetically spelled foreign language text'"
    +              " value='romanized'>romanized</option>"
    +       "<option title='Hides this text'"
    +              " value='spoiler'>spoiler</option>"
    +     "</optgroup>"
    +     "<optgroup label='in div/p'>"
    +       "<option value='blockquote'>blockquote</option>"
    +       "<option value='center'>center</option>"
    +       "<option value='justify'>justify</option>"
    +     "</optgroup>"
    +     "<optgroup label='in div'>"
    +       "<option value='ul'>unordered list</option>"
    +     "</optgroup>"
    +     "<optgroup label='in span'>"
    +       "<option value='li'>list item</option>"
    +     "</optgroup>"
    + "<optgroup label='in span in link'>"
    + "<option value='thumbnail'>thumbnail</option>"
    + "</optgroup>"
    +   "</select>"
    +   "<input type='button' value='rtl'"
    +     " title='right-to-left default directionality'"
    +     " onclick='addElement(\"[dir=rtl/]\", \"\")' />"
    +   "<input type='button' value='title'"
    +     " title='tooltip'"
    +     " onclick='addElement(\"[title=\", \"/]\")' />"
    +   "<input type='button' value='background'"
    +     " title='background of enclosing [div], [p], [quote], …, or [span]'"
    +     " onclick='addElement(\"[background=\", \"/]\")' />"
    + "</div>"
    + "<div class='grp'>"
    +   "<div>Use inside a [div] (block) or [p] (paragraph).</div>"
    +   "<button onclick='addElement(\"[class=justify/]\",\"\")'"
    +     " title='Justify text flush left and right'>"
    +     "<img src='http://CJKDramas.com/img/justify.png' alt='justify' />"
    +   "</button>"
    +   "<button onclick='addElement(\"[class=center/]\",\"\")'"
    +     " title='Center text'>"
    +     "<img src='http://CJKDramas.com/img/center.png' alt='center' />"
    +   "</button>"
    +   "<button onclick='addElement(\"[class=float-left/]\",\"\")'"
    +     " title='Float item to the left, wrapping text around it'>"
    +     "<img src='http://CJKDramas.com/img/float-left.png'"
    +       " alt='float-left' /></button>"
    +   "<button onclick='addElement(\"[class=float-right/]\",\"\")'"
    +     " title='Float item to the right, wrapping text around it'>"
    +     "<img src='http://CJKDramas.com/img/float-right.png'"
    +       " alt='float-right' /></button>"
    + "</div>"
    + "</div>";

  input.parentNode.appendChild(formAddition);

  document.postFormAdditionsMoved = true;
}

function removeTmpForm() {
  var tmpForm = document.forms["tmp-form"];
  if (tmpForm)
    tmpForm.parentNode.removeChild(tmpForm);
}

function unconvertQCode(textarea) {
  var val = textarea.value;
  val =
    val.replace(/\n?\[\/b\]\[\/B\]\[\/b\]\[\/B\]\[\/b\]\[\/B\]\[\/B\]\[\/B\]/g,
		"\n[/list]\n");
  val = val.replace(/\[u\]\[u\]\[u\]\[u\]/g, "[span]");
  val = val.replace(/\[\/u\]\[\/u\]\[\/u\]\[\/u\]/g, "[/span]");
  val = val.replace(/\[u\]\[u\]\[b\]\[u\]/g, "[ruby]");
  val = val.replace(/\[\/u\]\[\/b\]\[\/u\]\[\/u\]/g, "[/ruby]");
  val = val.replace(/\s*\[b\]\[b\]\[b\]\[b\]/g, "\n[div]");
  val = val.replace(/\s*\[quote\]/g, "\n\n[quote]");
  val = val.replace(/^\s*\[div\]/, "[div]");
  val = val.replace(/\[\/b\]\[\/b\]\[\/b\]\[\/b\]\s*/g, "[/div]\n");
  val = val.replace(/\[\/quote\]\s*/g, "[/quote]\n\n");
  val = val.replace(/\[\/div\]\s*$/, "[/div]\n");
  val = val.replace(/\[i\]\[i\]\[i\]\[i\] ?/g, "[");
  val =
    val.replace(/\[\/i\]\[\/i\]\[\/i\]\[\/i\]\[i\] ?([^\[]*[^\[ ]+) ?\[\/i\]/g,
		"=$1/]");
  val = val.replace(/\n?\[\/div\]\s*\[div\]\[class=li\/\]/g, "\n[*]");
  val = val.replace(/\[div\]\[class=ul\/\]\s*\[div\]\[class=li\/\]/g,
		    "[list]\n[*]");
  val = val.replace(/^[\r\n]*/, "");
  textarea.value = val;
}

var replaceQuotes = true;

/* Convert our custom QCodes into Runboard's QCodes. */
function convertQCode(event) {
  var msg = document.forms.msgform;
  var val = msg.body.value;
  val = val.replace(/\[p\](?:[\r\n]|\r\n)?/g, "[div][class=p/]");
  if (replaceQuotes) {
    val = val.replace(/\[quote\](?:[\r\n]|\r\n)?/g,
		      "[div][class=blockquote/]");
    val = val.replace(/\[center\](?:[\r\n]|\r\n)?/g, "[div][class=center/]");
  } else
    val = val.replace(/\s*\[(quote|center)\](?:[\r\n]|\r\n)?/g, "[$1]");
  val = val.replace(/\[list\]\s*\[\*\](?:[\r\n]|\r\n)?/g,
		    "[div][class=ul/][div][class=li/]");
  val = val.replace(/\[list\](?:[\r\n]|\r\n)?/g, "[div][class=ul/]");

  val = val.replace(/\s*\[\*\](?:[\r\n]|\r\n)?/g, "[/div][div][class=li/]");
  val = val.replace(/(?:[\r\n]|\r\n)?\[\/list\]\s*/g,
		    "[/b][/B][/b][/B][/b][/B][/B][/B]");

  val = val.replace(/\[span\](?:[\r\n]|\r\n)?/g, "[u][u][u][u]");
  val = val.replace(/(?:[\r\n]|\r\n)?\[\/span\]/g, "[/u][/u][/u][/u]");
  val = val.replace(/\[ruby\](?:[\r\n]|\r\n)?/g, "[u][u][b][u]");
  val = val.replace(/(?:[\r\n]|\r\n)?\[\/ruby\]/g, "[/u][/b][/u][/u]");
  val = val.replace(/\[(rb|rt|rp)\](?:[\r\n]|\r\n)?/g, "[b]");
  val = val.replace(/(?:[\r\n]|\r\n)?\[\/(rb|rt|rp)\]/g, "[/b]");
  val = val.replace(/\[(rbc|rtc)\](?:[\r\n]|\r\n)?/g, "[u]");
  val = val.replace(/(?:[\r\n]|\r\n)?\[\/(rbc|rtc)\]/g, "[/u]");
  val = val.replace(/\s*\[div\](?:[\r\n]|\r\n)?/g, "[b][b][b][b]");
  val = val.replace(/(?:[\r\n]|\r\n)?\[\/(div|p)\]\s*/g, "[/b][/b][/b][/b]");
  if (replaceQuotes)
    val = val.replace(/(?:[\r\n]|\r\n)?\[\/(?:quote|center)\]\s*/g,
		      "[/b][/b][/b][/b]");
  else
    val = val.replace(/(?:[\r\n]|\r\n)?\[\/(quote|center)\]\s*/g, "[/$1]");

  val = val.replace(/\[(class|xml:lang|dir|height|width)=([^\/]*)\/\]/g,
    "[i][i][i][i] $1[/i][/i][/i][/i][i] $2 [/i]");
  val = val.replace(/\[(background|title)=(.*?)\/\]/g,
    "[i][i][i][i] $1[/i][/i][/i][/i][i] $2 [/i]");
  /* Execute this replacement separately because it relies on
     non-greedy matches, which all browsers might not support. */
  val = val.replace(/\[src=(.*?)\/\]/g,
		    "[i][i][i][i] src[/i][/i][/i][/i][i] $1 [/i]");

  val = val.replace(/\s*\[code\]/g, "[code]");
  val = val.replace(/\[\/code\]\s*/g, "[/code]");
  msg.body.value = val;
  return true;
}

/* Workaround Runboard bugs that alter content when previewing. */
function escapeRunboardPreview(event) {
  var msg = document.forms.msgform;
  var val = msg.body.value;
  val = val.replace(/&#(91|x51);|&lsqb;/g, "&AMP;#$1;");
  val = val.replace(/&#(160|x[Aa]0);|&nbsp;| /g, "&AMP;nbsp;");
  val = val.replace(/&([^-A-Za-z0-9.#])/g, "&AMP;$1");
  val = val.replace(/&amp;/g, "&AMP;amp;");

  val = val.replace(/&AMP;/g, "&");
  msg.body.value = val;
  return true;
}

function trim(s) {
  return s ? s.replace(/^ | $/, "") : null;
}

function toPinyinWithToneMarks(str) {
  var tm_map = [[/a([oi]?|n?g?)1/g, '\u0101$1'],
		[/a([oi]?|n?g?)2/g, 'á$1'],
		[/a([oi]?|n?g?)3/g, '\u01ce$1'],
		[/a([oi]?|n?g?)4/g, 'à$1'],

		[/e([ir]?|n?g?)1/g, '\u0113$1'],
		[/e([ir]?|n?g?)2/g, 'é$1'],
		[/e([ir]?|n?g?)3/g, '\u011b$1'],
		[/e([ir]?|n?g?)4/g, 'è$1'],

		[/i(n?g?)1/g, '\u012b$1'],
		[/i(n?g?)2/g, 'í$1'],
		[/i(n?g?)3/g, '\u01d0$1'],
		[/i(n?g?)4/g, 'ì$1'],

		[/o(u?|n?g?)1/g, '\u014d$1'],
		[/o(u?|n?g?)2/g, 'ó$1'],
		[/o(u?|n?g?)3/g, '\u01d2$1'],
		[/o(u?|n?g?)4/g, 'ò$1'],

		[/u(n?g?)1/g, '\u016b$1'],
		[/u(n?g?)2/g, 'ú$1'],
		[/u(n?g?)3/g, '\u01d4$1'],
		[/u(n?g?)4/g, 'ù$1'],

		[/([ln])yu/g, '$1ü'],
		[/ü1/g, '\u01d6'],
		[/ü2/g, '\u01d8'],
		[/ü3/g, '\u01da'],
		[/ü4/g, '\u01dc']];
  var match = str.match(/^\[span\]\[xml:lang=([-a-zA-Z]*)\/\](?:\[class=romanized\/\])?([\s\S]*)\[\/span\]$/);
  var lang = 'zh-CN';
  if (match) {
    lang = match[1];
    str = match[2];
  }

  for (var i in tm_map)
    str = str.replace(tm_map[i][0], tm_map[i][1]);

  return "[span][xml:lang=" + lang + "/][class=romanized/]" + str + "[/span]";
}

function installPostHandlers(msg) {
  if (! msg)
    return;

  unconvertQCode(msg.body);

  var postHandler = new Object();
  postHandler.handleEvent = convertQCode;
  if (msg.addEventListener)
    msg.addEventListener('submit', postHandler, true);
  else
    msg.onsubmit = postHandler.handleEvent;

  /*
  var clickHandler = new Object();
  clickHandler.handleEvent = escapeRunboardPreview;
  if (msg.preview.addEventListener)
    msg.preview.addEventListener('click', clickHandler, true);
  else
    msg.preview.onclick = clickHandler.handleEvent;
    */

  msg.body.onkeyup = storeIECaret;
  msg.body.onclick = storeIECaret;
  msg.body.onselect = storeIECaret;

  if (document.getElementById("undo"))
    document.getElementById("undo").saved = new Array(0);
  if (document.getElementById("redo"))
    document.getElementById("redo").saved = new Array(0);
}

/* Remove temporary CSS style block used to hide custom QCode during
 * page loading. */
function disableTemporaryCSS() {
  document.getElementById("tmp-CSS").disabled = true;
}

var didLoad = false;
function load() {
  if (didLoad)
    return;
  didLoad = true;

  for (var i in listeners)
    document.removeEventListener('DOMNodeInserted', listeners[i], false);
  var outerTable = document.getElementById("outer-table");
  outerTable.className = "tmp-class-1";
  replaceElements(document, descendentOfSpanSequence, spanTemplate, "u");
  replaceElements(document, descendentOfBlockSequence, divTemplate, "b");
  replaceElements(document, descendentOfRubySequence, rubyTemplate, "u");
  outerTable.className = "tmp-class-2";
  for (var i in listeners)
    document.addEventListener('DOMNodeInserted', listeners[i], false);
  disableTemporaryCSS();
}

function enhanceForm() {
  movePostFormAdditions();
  removeTmpForm();
  installPostHandlers(document.forms.msgform);
}

/* Since this listener is called thousands of times, it is VITAL that it do
 * the bare minimum.  Every extra if statement is significant because of
 * the immense number of times it will be run. event.target.className
 * itself, below, takes about 0.5ms each time. */
function nodeInsertedListener(event) {
  var className = event.target.className;
  if (false // className == "ak_msg_post_signature_block"  // Firefox 1.5 bug
      || className == "postlistdate") {
    for (var i in listeners)
      document.removeEventListener('DOMNodeInserted', listeners[i], false);
    replaceElements(document, descendentOfSpanSequence, spanTemplate, "u");
    replaceElements(document, descendentOfBlockSequence, divTemplate, "b");
    replaceElements(document, descendentOfRubySequence, rubyTemplate, "u");
    for (var i in listeners)
      document.addEventListener('DOMNodeInserted', listeners[i], false);
    movePostFormAdditions();
  }
}

function integer_divide(dividend, divisor) {
  return (dividend - (dividend % divisor)) / divisor;
}


/* standardizeEvent written by Christian Schmidt, with preventDefault
 * fixed by Charles C. Fu */
function standardizeEvent(e) {
  if (! e.stopPropagation)
    e.stopPropagation = new Function("this.cancelBubble = true");
  if (! e.preventDefault)
    e.preventDefault = new Function("this.returnValue = true");
  if (typeof e.layerX == "undefined" && typeof e.offsetX == "number") {
    e.layerX = e.offsetX;
    e.layerY = e.offsetY;
  }
  if (! e.target && e.srcElement) {
    e.target = e.srcElement;
    if (e.type == "onmouseout")
      e.relatedTarget = e.toElement;
    else if (e.type == "onmouseover")
      e.relatedTarget = e.fromElement;
  }
}



if (false && document.addEventListener) {
  listeners = [nodeInsertedListener];
  document.addEventListener('DOMNodeInserted', nodeInsertedListener, false);
} else {
  listeners = [];
}
