/*
 * jQuery plugin
 */
(function($) {
  var counter = 0;

  $.fn.wmd = function(_options) {
    this.each(function() {
      var defaults = {'preview': true};
      var options = $.extend({}, _options || {}, defaults);

      if (!options.button_bar) {
        options.button_bar = "wmd-button-bar-" + counter;
        var button_bar = $('<div>');
        button_bar.attr({
          'class': 'wmd-button-bar',
          'id': options.button_bar
        });
        button_bar.insertBefore(this);
      }

      if (typeof(options.preview) == "boolean" && options.preview) {
        options.preview = "wmd-preview-" + counter;
        var preview = $('<div>');
        preview.attr({
          'class': 'wmd-preview',
          'id': options.preview
        });
        preview.insertAfter(this);
      }

      if (typeof(options.output) == "boolean" && options.output) {
        options.output = "wmd-output-" + counter;
        var output = $('<div>');
        output.attr({
          'class': 'wmd-output',
          'id': options.output
        });
        output.insertAfter(this);
      }

      this.id = this.id || "wmd-input-" + counter;
      options.input = this;

      WMD.init(options);
      counter++;
    });
  };
})(jQuery);


/*
 * WMD
 */
var WMD = (function() {
  var doc = document;
  var buttons = {};
  var browser = {};
  var options = {
    version: 2.0,
    output_format: "markdown",
    has_image_button: true
  };
  var panels = {};
  var preview_poll_interval = 500; // miliseconds
  var paste_poll_interval = 100; // miliseconds
  var re = RegExp;
  var showdown = window.top.Attacklab && window.top.Attacklab.showdown;
  var nativeUndo = true;

  return {
    init: function(options) {
      this.merge_options(options);
      this.setup_browsers();
      this.base();
    },

    merge_options: function(obj) {
      for(var key in obj) {
        options[key] = obj[key];
      }
    },

    setup_browsers: function() {
      var platform = navigator.platform.toLowerCase();
      var agent = navigator.userAgent.toLowerCase();

      browser.isWindows = /win/.test(platform);
      browser.isIE = /msie/.test(agent);
      browser.isIE_5or6 = /msie 6/.test(agent) || /msie 5/.test(agent);
      browser.isIE_7plus = browser.isIE && !browser.isIE_5or6;
      browser.isOpera = /opera/.test(agent);
      browser.isKonqueror = /konqueror/.test(agent);
    },

    panel_collection: function() {
      // A collection of the important regions on the page.
      // Cached so we don't have to keep traversing the DOM.
      panels.button_bar = doc.getElementById(options.button_bar || "wmd-button-bar");
      panels.preview = doc.getElementById(options.preview || "wmd-preview");
      panels.output = doc.getElementById(options.output || "wmd-output");
      panels.input = options.input;
    },

    base: function() {
      // A few handy aliases for readability.
      var wmd  = this;

      // help button attributes
      var help = {
        'href': options.help_url || "http://daringfireball.net/projects/markdown/",
        'title': options.help_title || "Formatting help",
        'target': options.help_target || "_blank"
      };

      // image button attributes
      var image = {
        'href': '/weblogs/upload/?popup=true',
        'target': help.target,
        'title': 'Upload an image'
      };

      /*
       * Internet explorer has problems with CSS sprite buttons that use HTML
       * lists.  When you click on the background image "button", IE will
       * select the non-existent link text and discard the selection in the
       * textarea.  The solution to this is to cache the textarea selection
       * on the button's mousedown event and set a flag.  In the part of the
       * code where we need to grab the selection, we check for the flag
       * and, if it's set, use the cached area instead of querying the
       * textarea.
       *
       * This ONLY affects Internet Explorer (tested on versions 6, 7
       * and 8) and ONLY on button clicks.  Keyboard shortcuts work
       * normally since the focus never leaves the textarea.
       */
      wmd.ieCachedRange = null;     // cached textarea selection
      wmd.ieRetardedClick = false;  // flag

      /*
       * Watches the input textarea, polling at an interval and runs
       * a callback function if anything has changed.
       */
      wmd.inputPoller = function(callback, interval) {
        var pollerObj = this;
        var inputArea = panels.input;

        // Stored start, end and text.  Used to see if there are changes to the input.
        var lastStart;
        var lastEnd;
        var markdown;

        var killHandle; // Used to cancel monitoring on destruction.

        // Checks to see if anything has changed in the textarea.
        // If so, it runs the callback.
        this.tick = function() {
          if (!Util.visible(inputArea)) {
            return;
          }

          // Update the selection start and end, text.
          if (inputArea.selectionStart || inputArea.selectionStart === 0) {
            var start = inputArea.selectionStart;
            var end = inputArea.selectionEnd;
            if (start != lastStart || end != lastEnd) {
              lastStart = start;
              lastEnd = end;

              if (markdown != inputArea.value) {
                markdown = inputArea.value;
                return true;
              }
            }
          }
          return false;
        };

        var doTickCallback = function() {
          if (!Util.visible(inputArea)) {
            return;
          }

          // If anything has changed, call the function.
          if (pollerObj.tick()) {
            callback();
          }
        };

        // Set how often we poll the textarea for changes.
        var assignInterval = function() {
          // previewPollInterval is set at the top of the namespace.
          killHandle = window.top.setInterval(doTickCallback, interval);
        };

        this.destroy = function() {
          window.top.clearInterval(killHandle);
        };

        assignInterval();
      };

      /*
       * Handles pushing and popping TextareaStates for undo/redo commands.
       * I should rename the stack variables to list.
       */
      wmd.undoManager = function(callback) {
        var undoObj = this;
        var undoStack = [];   // A stack of undo states
        var stackPtr = 0;     // The index of the current state
        var mode = "none";
        var lastState;        // The last state
        var poller;
        var timer;            // The setTimeout handle for cancelling the timer
        var inputStateObj;

        // Set the mode for later logic steps.
        var setMode = function(newMode, noSave) {
          if (mode != newMode) {
            mode = newMode;
            if (!noSave) {
              saveState();
            }
          }

          if (!browser.isIE || mode != "moving") {
            timer = window.top.setTimeout(refreshState, 1);
          }
          else {
            inputStateObj = null;
          }
        };

        var refreshState = function() {
          inputStateObj = new wmd.TextareaState();
          poller.tick();
          timer = undefined;
        };

        this.setCommandMode = function() {
          mode = "command";
          saveState();
          timer = window.top.setTimeout(refreshState, 0);
        };

        this.canUndo = function() {
          return stackPtr > 1;
        };

        this.canRedo = function() {
          if (undoStack[stackPtr + 1]) {
            return true;
          }
          return false;
        };

        // Removes the last state and restores it.
        this.undo = function() {
          if (undoObj.canUndo()) {
            if (lastState) {
              // What about setting state -1 to null or checking for undefined?
              lastState.restore();
              lastState = null;
            }
            else {
              undoStack[stackPtr] = new wmd.TextareaState();
              undoStack[--stackPtr].restore();
              if (callback) {
                callback();
              }
            }
          }
          mode = "none";
          panels.input.focus();
          refreshState();
        };

        // Redo an action.
        this.redo = function() {
          if (undoObj.canRedo()) {
            undoStack[++stackPtr].restore();
            if (callback) {
              callback();
            }
          }
          mode = "none";
          panels.input.focus();
          refreshState();
        };

        // Push the input area state to the stack.
        var saveState = function() {
          var currState = inputStateObj || new wmd.TextareaState();
          if (!currState) {
            return false;
          }
          if (mode == "moving") {
            if (!lastState) {
              lastState = currState;
            }
            return;
          }
          if (lastState) {
            if (undoStack[stackPtr - 1].text != lastState.text) {
              undoStack[stackPtr++] = lastState;
            }
            lastState = null;
          }
          undoStack[stackPtr++] = currState;
          undoStack[stackPtr + 1] = null;
          if (callback) {
            callback();
          }
        };

        var handleCtrlYZ = function(event) {
          var handled = false;
          if (event.ctrlKey || event.metaKey) {
            // IE and Opera do not support charCode.
            var keyCode = event.charCode || event.keyCode;
            var keyCodeChar = String.fromCharCode(keyCode);
            switch (keyCodeChar) {
              case "y":
                undoObj.redo();
                handled = true;
                break;
              case "z":
                if (!event.shiftKey) {
                  undoObj.undo();
                }
                else {
                  undoObj.redo();
                }
                handled = true;
                break;
            }
          }
          if (handled) {
            if (event.preventDefault) {
              event.preventDefault();
            }
            if (window.top.event) {
              window.top.event.returnValue = false;
            }
            return;
          }
        };

        // Set the mode depending on what is going on in the input area.
        var handleModeChange = function(event) {
          if (!event.ctrlKey && !event.metaKey) {
            var keyCode = event.keyCode;
            if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
              // 33 - 40: page up/dn and arrow keys
              // 63232 - 63235: page up/dn and arrow keys on safari
              setMode("moving");
            }
            else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
              // 8: backspace
              // 46: delete
              // 127: delete
              setMode("deleting");
            }
            else if (keyCode == 13) {
              // 13: Enter
              setMode("newlines");
            }
            else if (keyCode == 27) {
              // 27: escape
              setMode("escape");
            }
            else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
              // 16-20 are shift, etc.
              // 91: left window key
              // I think this might be a little messed up since there are
              // a lot of nonprinting keys above 20.
              setMode("typing");
            }
          }
        };

        var setEventHandlers = function() {
          Util.addEvent(panels.input, "keypress", function(event) {
            // keyCode 89: y
            // keyCode 90: z
            if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) {
              event.preventDefault();
            }
          });

          var handlePaste = function() {
            if (browser.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) {
              if (timer == undefined) {
                mode = "paste";
                saveState();
                refreshState();
              }
            }
          };

          // pastePollInterval is specified at the beginning of this namespace.
          poller = new wmd.inputPoller(handlePaste, paste_poll_interval);

          Util.addEvent(panels.input, "keydown", handleCtrlYZ);
          Util.addEvent(panels.input, "keydown", handleModeChange);

          Util.addEvent(panels.input, "mousedown", function() {
            setMode("moving");
          });
          panels.input.onpaste = handlePaste;
          panels.input.ondrop = handlePaste;
        };

        var init = function() {
          setEventHandlers();
          refreshState();
          saveState();
        };

        this.destroy = function() {
          if (poller) {
            poller.destroy();
          }
        };

        init();
      };

      /*
       * I think my understanding of how the buttons and callbacks are stored in
       * the array is incomplete.
       */
      wmd.editor = function(previewRefreshCallback) {
        if (!previewRefreshCallback) {
          previewRefreshCallback = function(){};
        }

        var inputBox = panels.input;
        var offsetHeight = 0;
        var editObj = this;
        var mainDiv;
        var mainSpan;
        var div;              // This name is pretty ambiguous.  I should rename this.
        var creationHandle;   // Used to cancel recurring events from setInterval.
        var undoMgr;          // The undo manager

        // Perform the button's action.
        var doClick = function(button) {
          inputBox.focus();
          if (button.textOp) {
            if (undoMgr) {
              undoMgr.setCommandMode();
            }
            var state = new wmd.TextareaState();
            if (!state) { return; }
            var chunks = state.getChunks();

            /*
             * Some commands launch a "modal" prompt dialog.  Javascript
             * can't really make a modal dialog box and the WMD code
             * will continue to execute while the dialog is displayed.
             * This prevents the dialog pattern I'm used to and means
             * I can't do something like this:
             *
             * var link = CreateLinkDialog();
             * makeMarkdownLink(link);
             *
             * Instead of this straightforward method of handling a
             * dialog I have to pass any code which would execute
             * after the dialog is dismissed (e.g. link creation)
             * in a function parameter.
             *
             * Yes this is awkward and I think it sucks, but there's
             * no real workaround.  Only the image and link code
             * create dialogs and require the function pointers.
             */
            var fixupInputArea = function() {
              inputBox.focus();

              if (chunks) {
                state.setChunks(chunks);
              }

              state.restore();
              previewRefreshCallback();
            };

            var default_text = true;
            var noCleanup = button.textOp(chunks, fixupInputArea, default_text);

            if(!noCleanup) {
              fixupInputArea();
            }
          }
          if (button.execute) {
            button.execute(editObj);
          }
        };

        var setUndoRedoButtonStates = function() {
          if(undoMgr){
            setupButton(buttons["wmd-undo-button"], undoMgr.canUndo());
            setupButton(buttons["wmd-redo-button"], undoMgr.canRedo());
          }
        };

        var setupButton = function(button) {
          var normalYShift = "0px";
          var disabledYShift = "-20px";
          var highlightYShift = "-40px";

          button.css('background-position', button.XShift + " " + normalYShift);

          // IE tries to select the background image "button" text (it's
          // implemented in a list item) so we have to cache the selection
          // on mousedown.
          if(browser.isIE) {
            button.bind('mousedown', function(e) {
              wmd.ieRetardedClick = true;
              wmd.ieCachedRange = document.selection.createRange();
            });
          }

          if (!button.external_link) {
            button.bind('click', function(e) {
              if (this.onmouseout) {
                button.trigger('mouseout');
              }
              doClick(button);
              return false;
            });
          }
          else {
            if (button.window_size) {
              button.bind('click', function(e) {
                e.preventDefault();
                var features = "height="+button.window_size.height+",width="+button.window_size.width+",scrollTo,resizable=1,scrollbars=1,location=0";
                window.open(image.href+'?popup=true', 'Popup', features);
              });
            }
          }
        }

        var makeSpritedButtonRow = function() {
          var button_bar = $('#'+options.button_bar || '.wmd-button-bar');
          var normalYShift = '0px';
          var disabledYShift = '-20px';
          var highlightYShift = '-40px';
          var button_row = $('<ul>');
          var xoffset = 0;

          button_row.attr('class', 'wmd-button-row');
          button_row.appendTo(button_bar);

          function createButton(name, classname, title, func) {
            var button = $('<li>');
            buttons[classname] = button;

            button.attr('class', 'wmd-button ' + classname);
            button.text(name);
            button.XShift = xoffset + "px";
            xoffset -= 20;

            button.attr('title',  title);
            button.textOp = func;

            return button;
          }

          function addButton(name, classname, title, func) {
            var button = createButton(name, classname, title, func);
            setupButton(button);
            button.appendTo(button_row);
            return button;
          }

          function addSpacer() {
            var spacer = $('<li>');
            spacer.attr('class', 'wmd-spacer');
            spacer.appendTo(button_row);
          }

          var bold_button = addButton("Bold", "wmd-bold-button", "Strong <strong> Ctrl+B", function(chunk, post_processing, default_text) { Command.bold_italic(chunk, 2, 'Strong'); });
          var italic_button = addButton("Italic", "wmd-italic-button", "Emphasis <em> Ctrl+I", function(chunk, post_processing, default_text) { Command.bold_italic(chunk, 1, 'Emphasis'); });
          addSpacer();
          var link_button = addButton("Link", "wmd-link-button", "Hyperlink <a> Ctrl+L", function(chunk, post_processing, default_text) { return Command.dialog(chunk, post_processing, false); });
          var quote_button = addButton("Quote", "wmd-quote-button", "Blockquote <blockquote> Ctrl+Q", function(chunk, post_processing, default_text) { Command.blockquote(chunk, default_text); });
        //  var code_button = addButton("Code", "wmd-code-button", "Code Sample <pre><code> Ctrl+K", function(chunk, post_processing, default_text) { Command.code(chunk, default_text); });

          // Image button
          if (options.has_image_button) {
            var image_button = createButton("Image", "wmd-image-button");
            image_button.external_link = true;
            image_button.window_size = {'width': 500, 'height': 200}
            setupButton(image_button);
            image_button.appendTo(button_row);
            var image_anchor = $('<a>');
            image_anchor.attr(image);
            image_anchor.text('Image');
            image_button.html(image_anchor);
          }

          addSpacer();
          var numbered_list = addButton("Numbered list", "wmd-olist-button", "Numbered List <ol> Ctrl+O", function(chunk, post_processing, default_text) { Command.list(chunk, true, default_text); });
          var bulleted_list = addButton("Bulleted list", "wmd-ulist-button", "Bulleted List <ul> Ctrl+U", function(chunk, post_processing, default_text) { Command.list(chunk, false, default_text); });

          // Undo / Redo
          if (!nativeUndo) {
            addSpacer();
            var undo_button = addButton("Undo", "wmd-undo-button", "Undo - Ctrl+Z");
            undo_button.execute = function(manager){
              manager.undo();
            };

            var redo_button = addButton("Redo", "wmd-redo-button", "Redo - Ctrl+Y");
            if (browser.isWindows) {
              redo_button.title = "Redo - Ctrl+Y";
            }
            else {
              // mac and other non-Windows platforms
              redo_button.title = "Redo - Ctrl+Shift+Z";
            }
            redo_button.execute = function(manager){
              manager.redo();
            };
          }

          // Help button
          addSpacer();
          var help_button = addButton("Help", "wmd-help-button", "", function(chunk, post_processing, default_text) { return Command.dialog(chunk, post_processing, true); });

          setUndoRedoButtonStates();
        }

        var setupEditor = function() {
          if (/\?noundo/.test(doc.location.href)) {
            nativeUndo = true;
          }
          if (!nativeUndo) {
            undoMgr = new wmd.undoManager(function() {
              previewRefreshCallback();
              setUndoRedoButtonStates();
            });
          }

          makeSpritedButtonRow();

          var keyEvent = "keydown";

          if (browser.isOpera) {
            keyEvent = "keypress";
          }

          Util.addEvent(inputBox, keyEvent, function(key) {
            // Check to see if we have a button key and, if so execute the callback.
            if (key.ctrlKey || key.metaKey) {

              var keyCode = key.charCode || key.keyCode;
              var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();

              switch(keyCodeStr) {
                case "b":
                  doClick(buttons["wmd-bold-button"]);
                  break;
                case "i":
                  doClick(buttons["wmd-italic-button"]);
                  break;
                case "l":
                  doClick(buttons["wmd-link-button"]);
                  break;
                case "q":
                  doClick(buttons["wmd-quote-button"]);
                  break;
                case "k":
                  doClick(buttons["wmd-code-button"]);
                  break;
                case "g":
                  doClick(buttons["wmd-image-button"]);
                  break;
                case "o":
                  doClick(buttons["wmd-olist-button"]);
                  break;
                case "u":
                  doClick(buttons["wmd-ulist-button"]);
                  break;
                case "h":
                  doClick(buttons["wmd-heading-button"]);
                  break;
                case "r":
                  doClick(buttons["wmd-hr-button"]);
                  break;
                case "y":
                  doClick(buttons["wmd-redo-button"]);
                  break;
                case "z":
                  if(key.shiftKey) {
                    doClick(buttons["wmd-redo-button"]);
                  }
                  else {
                    doClick(buttons["wmd-undo-button"]);
                  }
                  break;
                default:
                  return;
              }

              if (key.preventDefault) {
                key.preventDefault();
              }

              if (window.top.event) {
                window.top.event.returnValue = false;
              }
            }
          });

          /*
           * Auto-continue lists, code blocks and block quotes when the enter
           * key is pressed.
           */
          Util.addEvent(inputBox, "keyup", function(key){
            if (!key.shiftKey && !key.ctrlKey && !key.metaKey) {
              var keyCode = key.charCode || key.keyCode;
              // Key code 13 is Enter
              if (keyCode === 13) {
                fakeButton = {};
                fakeButton.textOp = Command.auto_indent;
                doClick(fakeButton);
              }
            }
          });

          // Disable ESC clearing the input textarea on IE
          if (browser.isIE) {
            Util.addEvent(inputBox, "keydown", function(key) {
              var code = key.keyCode;
              // Key code 27 is ESC
              if (code === 27) {
                return false;
              }
            });
          }

          if (inputBox.form) {
            var submitCallback = inputBox.form.onsubmit;
            inputBox.form.onsubmit = function() {
              convertToHtml();
              if (submitCallback) {
                return submitCallback.apply(this, arguments);
              }
            };
          }
        };

        /*
         * Convert the contents of the input textarea to HTML in the 
         * output/preview panels.
         */
        var convertToHtml = function() {
          if (showdown) {
            var markdownConverter = new showdown.converter();
          }
          var text = inputBox.value;
          var callback = function() {
            inputBox.value = text;
          };

          if (!/markdown/.test(options.output_format.toLowerCase())) {
            if (markdownConverter) {
              inputBox.value = markdownConverter.makeHtml(text);
              window.top.setTimeout(callback, 0);
            }
          }
          return true;
        };

        this.undo = function(){
          if (undoMgr) {
            undoMgr.undo();
          }
        };
        this.redo = function(){
          if (undoMgr) {
            undoMgr.redo();
          }
        };

        /*
         * This is pretty useless. The setupEditor function contents should just
         * be copied here.
         */ 
        var init = function() {
          setupEditor();
        };

        this.destroy = function() {
          if (undoMgr) {
            undoMgr.destroy();
          }
          if (div.parentNode) {
            div.parentNode.removeChild(div);
          }
          if (inputBox) {
            inputBox.style.marginTop = "";
          }
          window.top.clearInterval(creationHandle);
        };

        init();
      };

      /*
       * TextareaState
       *
       * The input textarea state/contents. This is used to implement undo/redo by 
       * the undo manager.
       */
      wmd.TextareaState = function() {
        // Aliases
        var stateObj = this;
        var inputArea = panels.input;

        this.init = function() {
          if (!Util.visible(inputArea)) {
            return;
          }
          this.setInputAreaSelectionStartEnd();
          this.scrollTop = inputArea.scrollTop;
          if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
            this.text = inputArea.value;
          }
        }

        // Sets the selected text in the input box after we've performed an
        // operation.
        this.setInputAreaSelection = function() {
          if (!Util.visible(inputArea)) {
            return;
          }
          if (inputArea.selectionStart !== undefined && !browser.isOpera) {
            inputArea.focus();
            inputArea.selectionStart = stateObj.start;
            inputArea.selectionEnd = stateObj.end;
            inputArea.scrollTop = stateObj.scrollTop;
          }
          else if (doc.selection) {
            if (doc.activeElement && doc.activeElement !== inputArea) {
              return;
            }
            inputArea.focus();
            var range = inputArea.createTextRange();
            range.moveStart("character", -inputArea.value.length);
            range.moveEnd("character", -inputArea.value.length);
            range.moveEnd("character", stateObj.end);
            range.moveStart("character", stateObj.start);
            range.select();
          }
        };

        this.setInputAreaSelectionStartEnd = function() {
          if (inputArea.selectionStart || inputArea.selectionStart === 0) {
            stateObj.start = inputArea.selectionStart;
            stateObj.end = inputArea.selectionEnd;
          }
          else if (doc.selection) {
            stateObj.text = Util.fixEolChars(inputArea.value);
            // IE loses the selection in the textarea when buttons are
            // clicked.  On IE we cache the selection and set a flag
            // which we check for here.
            var range;
            if(wmd.ieRetardedClick && wmd.ieCachedRange) {
              range = wmd.ieCachedRange;
              wmd.ieRetardedClick = false;
            }
            else {
              range = doc.selection.createRange();
            }
            var fixedRange = Util.fixEolChars(range.text);
            var marker = "\x07";
            var markedRange = marker + fixedRange + marker;
            range.text = markedRange;
            var inputText = Util.fixEolChars(inputArea.value);
            range.moveStart("character", -markedRange.length);
            range.text = fixedRange;
            stateObj.start = inputText.indexOf(marker);
            stateObj.end = inputText.lastIndexOf(marker) - marker.length;
            var len = stateObj.text.length - Util.fixEolChars(inputArea.value).length;
            if (len) {
              range.moveStart("character", -fixedRange.length);
              while (len--) {
                fixedRange += "\n";
                stateObj.end += 1;
              }
              range.text = fixedRange;
            }
            this.setInputAreaSelection();
          }
        };

        // Restore this state into the input area.
        this.restore = function() {
          if (stateObj.text != undefined && stateObj.text != inputArea.value) {
            inputArea.value = stateObj.text;
          }
          this.setInputAreaSelection();
          inputArea.scrollTop = stateObj.scrollTop;
        };

        // Gets a collection of HTML chunks from the inptut textarea.
        this.getChunks = function() {
          var chunk = new wmd.Chunks();
          chunk.before = Util.fixEolChars(stateObj.text.substring(0, stateObj.start));
          chunk.startTag = "";
          chunk.selection = Util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
          chunk.endTag = "";
          chunk.after = Util.fixEolChars(stateObj.text.substring(stateObj.end));
          chunk.scrollTop = stateObj.scrollTop;
          return chunk;
        };

        // Sets the TextareaState properties given a chunk of markdown.
        this.setChunks = function(chunk) {
          chunk.before = chunk.before + chunk.startTag;
          chunk.after = chunk.endTag + chunk.after;

          if (browser.isOpera) {
            chunk.before = chunk.before.replace(/\n/g, "\r\n");
            chunk.selection = chunk.selection.replace(/\n/g, "\r\n");
            chunk.after = chunk.after.replace(/\n/g, "\r\n");
          }

          this.start = chunk.before.length;
          this.end = chunk.before.length + chunk.selection.length;
          this.text = chunk.before + chunk.selection + chunk.after;
          this.scrollTop = chunk.scrollTop;
        };

        this.init();
      };

      // before: contains all the text in the input box BEFORE the selection.
      // after: contains all the text in the input box AFTER the selection.
      wmd.Chunks = function() {};

      // startRegex: a regular expression to find the start tag
      // endRegex: a regular expresssion to find the end tag
      wmd.Chunks.prototype.findTags = function(startRegex, endRegex) {
        var chunkObj = this;
        var regex;

        if (startRegex) {
          regex = Util.extendRegExp(startRegex, "", "$");
          this.before = this.before.replace(regex,
            function(match){
              chunkObj.startTag = chunkObj.startTag + match;
              return "";
            });

          regex = Util.extendRegExp(startRegex, "^", "");

          this.selection = this.selection.replace(regex,
            function(match) {
              chunkObj.startTag = chunkObj.startTag + match;
              return "";
            });
        }

        if (endRegex) {
          regex = Util.extendRegExp(endRegex, "", "$");
          this.selection = this.selection.replace(regex,
            function(match) {
              chunkObj.endTag = match + chunkObj.endTag;
              return "";
            });

          regex = Util.extendRegExp(endRegex, "^", "");

          this.after = this.after.replace(regex,
            function(match){
              chunkObj.endTag = match + chunkObj.endTag;
              return "";
            });
        }
      };

      // If remove is false, the whitespace is transferred to the before/after 
      // regions. If remove is true, the whitespace disappears.
      wmd.Chunks.prototype.trimWhitespace = function(remove) {
        this.selection = this.selection.replace(/^(\s*)/, "");

        if (!remove) {
          this.before += re.$1;
        }

        this.selection = this.selection.replace(/(\s*)$/, "");

        if (!remove) {
          this.after = re.$1 + this.after;
        }
      };

      wmd.Chunks.prototype.addBlankLines = function(nLinesBefore, nLinesAfter, findExtraNewlines) {
        if (nLinesBefore === undefined) {
          nLinesBefore = 1;
        }

        if (nLinesAfter === undefined) {
          nLinesAfter = 1;
        }

        nLinesBefore++;
        nLinesAfter++;

        var regexText;
        var replacementText;

        this.selection = this.selection.replace(/(^\n*)/, "");
        this.startTag = this.startTag + re.$1;
        this.selection = this.selection.replace(/(\n*$)/, "");
        this.endTag = this.endTag + re.$1;
        this.startTag = this.startTag.replace(/(^\n*)/, "");
        this.before = this.before + re.$1;
        this.endTag = this.endTag.replace(/(\n*$)/, "");
        this.after = this.after + re.$1;

        if (this.before) {
          regexText = replacementText = "";
          while (nLinesBefore--) {
            regexText += "\\n?";
            replacementText += "\n";
          }
          if (findExtraNewlines) {
            regexText = "\\n*";
          }
          this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
        }

        if (this.after) {
          regexText = replacementText = "";

          while (nLinesAfter--) {
            regexText += "\\n?";
            replacementText += "\n";
          }
          if (findExtraNewlines) {
            regexText = "\\n*";
          }

          this.after = this.after.replace(new re(regexText, ""), replacementText);
        }
      };

      wmd.previewManager = function() {
        var managerObj = this;
        var converter;
        var poller;
        var timeout;
        var elapsedTime;
        var oldInputText;
        var htmlOut;
        var maxDelay = 3000;
        var startType = "delayed"; // The other legal value is "manual"

        // Adds event listeners to elements and creates the input poller.
        var setupEvents = function(inputElem, listener) {
          Util.addEvent(inputElem, "input", listener);
          inputElem.onpaste = listener;
          inputElem.ondrop = listener;

          Util.addEvent(inputElem, "keypress", listener);
          Util.addEvent(inputElem, "keydown", listener);
          // previewPollInterval is set at the top of this file.
          poller = new wmd.inputPoller(listener, preview_poll_interval);
        };

        var getDocScrollTop = function() {
          var result = 0;
          if (window.top.innerHeight) {
            result = window.top.pageYOffset;
          }
          else {
            if (doc.documentElement && doc.documentElement.scrollTop) {
              result = doc.documentElement.scrollTop;
            }
            else {
              if (document.body) {
                result = document.body.scrollTop;
              }
            }
          }
          return result;
        };

        var makePreviewHtml = function() {
          // If there are no registered preview and output panels
          // there is nothing to do.
          if (!panels.preview && !panels.output) {
            return;
          }

          var text = panels.input.value;
          if (text && text == oldInputText) {
            return; // Input text hasn't changed.
          }
          else {
            oldInputText = text;
          }

          var prevTime = new Date().getTime();

          if (typeof(options.preview_preprocess) == 'function') {
              text = options.preview_preprocess(text, function(text) {
                pushPreviewHtml(convertMarkup(text));
              });
          }

          text = convertMarkup(text);

          // Calculate the processing time of the HTML creation.
          // It's used as the delay time in the event listener.
          var currTime = new Date().getTime();
          elapsedTime = currTime - prevTime;

          pushPreviewHtml(text);
          htmlOut = text;
        };

        var convertMarkup = function(text) {
          if (!converter && showdown) {
            converter = new showdown.converter();
          }

          if (converter) {
            text = converter.makeHtml(text);
          }

          return text;
        };

        // setTimeout is already used.  Used as an event listener.
        var applyTimeout = function() {
          if (timeout) {
            window.top.clearTimeout(timeout);
            timeout = undefined;
          }

          if (startType !== "manual") {
            var delay = 0;
            if (startType === "delayed") {
              delay = elapsedTime;
            }
            if (delay > maxDelay) {
              delay = maxDelay;
            }
            timeout = window.top.setTimeout(makePreviewHtml, delay);
          }
        };

        var getScaleFactor = function(panel) {
          if (panel.scrollHeight <= panel.clientHeight) {
            return 1;
          }
          return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
        };

        var setPanelScrollTops = function() {
          if (panels.preview) {
            panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);
            ;
          }

          if (panels.output) {
            panels.output.scrollTop = (panels.output.scrollHeight - panels.output.clientHeight) * getScaleFactor(panels.output);
            ;
          }
        };

        this.refresh = function(requiresRefresh) {
          if (requiresRefresh) {
            oldInputText = "";
            makePreviewHtml();
          }
          else {
            applyTimeout();
          }
        };

        this.processingTime = function() {
          return elapsedTime;
        };

        // The output HTML
        this.output = function() {
          return htmlOut;
        };

        // The mode can be "manual" or "delayed"
        this.setUpdateMode = function(mode) {
          startType = mode;
          managerObj.refresh();
        };

        var isFirstTimeFilled = true;

        var pushPreviewHtml = function(text) {
          var emptyTop = Position.top(panels.input) - getDocScrollTop();

          // Send the encoded HTML to the output textarea/div.
          if (panels.output) {
            // The value property is only defined if the output is a textarea.
            if (panels.output.value !== undefined) {
              panels.output.value = text;
              panels.output.readOnly = true;
            }
            // Otherwise we are just replacing the text in a div.
            // Send the HTML wrapped in <pre><code>
            else {
              var newText = text.replace(/&/g, "&amp;");
              newText = newText.replace(/</g, "&lt;");
              var output = $(panels.output);
              output.html("<pre><code>" + newText + "</code></pre>");
              //panels.output.innerHTML = "<pre><code>" + newText + "</code></pre>";
            }
          }

          if (panels.preview) {
            var panel = $(panels.preview);
            panel.html(text);
            //panels.preview.innerHTML = text;
          }

          setPanelScrollTops();

          if (isFirstTimeFilled) {
            isFirstTimeFilled = false;
            return;
          }

          var fullTop = Position.top(panels.input) - getDocScrollTop();

          if (browser.isIE) {
            window.top.setTimeout(function(){
              window.top.scrollBy(0, fullTop - emptyTop);
            }, 0);
          }
          else {
            window.top.scrollBy(0, fullTop - emptyTop);
          }
        };

        var init = function() {
          setupEvents(panels.input, applyTimeout);
          makePreviewHtml();
          if (panels.preview) {
            panels.preview.scrollTop = 0;
          }
          if (panels.output) {
            panels.output.scrollTop = 0;
          }
        };

        this.destroy = function() {
          if (poller) {
            poller.destroy();
          }
        };

        init();
      };

      this.panel_collection();
      var preview_manager = new wmd.previewManager();
      var preview_refresh_callback = preview_manager.refresh;
      var edit = new wmd.editor(preview_refresh_callback);
      preview_manager.refresh(true);
    }
  }
})();


var Util = (function() {
  return {
    visible: function(element) {
      return element.offsetWidth > 0 || element.offsetHeight > 0;
    },

    addEvent: function(element, event, listener) {
      if (element.attachEvent) {
        // IE only.  The "on" is mandatory.
        element.attachEvent("on" + event, listener);
      }
      else {
        element.addEventListener(event, listener, false);
      }
    },

    removeEvent: function(element, event, listener) {
      if (element.detachEvent) {
        // IE only.  The "on" is mandatory.
        element.detachEvent("on" + event, listener);
      }
      else {
        element.removeEventListener(event, listener, false);
      }
    },

    fixEolChars: function(text) {
      text = text.replace(/\r\n/g, "\n");
      text = text.replace(/\r/g, "\n");
      return text;
    },

    /*
     * Extends a regular expression.  
     * Returns a new RegExp using pre + regex + post as the expression.
     * Used in a few functions where we have a base expression and we want 
     * to pre- or append some conditions to it (e.g. adding "$" to the end).
     * The flags are unchanged.
     *
     *   @regex: is a RegExp, pre and post are strings.
     *   @pre
     *   @post
     */
    extendRegExp: function(regex, pre, post) {
      if (pre === null || pre === undefined) {
        pre = "";
      }
      if(post === null || post === undefined) {
        post = "";
      }

      var pattern = regex.toString();
      var flags = "";

      // Replace the flags with empty space and store them.
      // Technically, this can match incorrect flags like "gmm".
      var result = pattern.match(/\/([gim]*)$/);

      if (result === null) {
        flags = result[0];
      }
      else {
        flags = "";
      }

      // Remove the flags and slash delimiters from the regular expression.
      pattern = pattern.replace(/(^\/|\/[gim]*$)/g, "");
      pattern = pre + pattern + post;

      return new RegExp(pattern, flags);
    },

    /*
     * Prompt
     * This simulates a modal dialog box and asks for the URL when you
     * click the hyperlink or image buttons.
     *
     *   @text:                 The html for the input box.
     *   @default_input_text:   The default value that appears in the input box.
     *   @markdown_link:        The function which is executed when the prompt is dismissed, either via OK or Cancel
     */
    prompt: function(text, default_input_text, markdown_link) {
      var dialog;       // The dialog box.
      var background;   // The background beind the dialog box.
      var input;        // The text box where you enter the hyperlink.

      if (default_input_text === undefined) {
        default_input_text = "";
      }

      // Used as a keydown event handler. Esc dismisses the prompt.
      // Key code 27 is ESC.
      var checkEscape = function(key){
        var code = (key.charCode || key.keyCode);
        if (code === 27) {
          close(true);
        }
      };

      // Dismisses the hyperlink input box.
      var close = function(is_cancel){
        if (input) {
          Util.removeEvent(document.body, 'keydown', checkEscape);
          var text = input.val();

          if (is_cancel) {
            text = null;
          }
          else {
            // Fixes common pasting errors.
            text = text.replace('http://http://', 'http://');
            text = text.replace('http://https://', 'https://');
            text = text.replace('http://ftp://', 'ftp://');

            if (text.indexOf('http://') === -1 && text.indexOf('ftp://') === -1 && text.indexOf('https://') === -1) {
              text = 'http://' + text;
            }
          }
          markdown_link(text);
        }
        
        dialog.remove();
        background.remove();
        return false;
      };

      // Creates the background behind the hyperlink text entry box.
      // Most of this has been moved to CSS but the div creation and
      // browser-specific hacks remain here.
      var createBackground = function() {
        var pageSize = Position.page_size();
        background = $('<div>');
        background.attr({
          'class': 'wmd-prompt-background'
        });
        background.css({
          'position': 'absolute',
          'top': '0',
          'z-index': '1000',
          'height': pageSize[1] + 'px',
          'opacity': '0.5',
          'left': '0',
          'width': '100%'
        });

        var document_body = $(document.body);
        background.appendTo(document_body);
      };

      // Create the text input box form/window.
      var createDialog = function() {
        // The main dialog box.
        dialog = $('<div>');
        dialog.attr('class', 'wmd-prompt-dialog');
        dialog.css({
          'padding': '10px',
          'width': '400px',
          'position': 'fixed',
          'z-index': '1001'
        });

        // The dialog text.
        var question = $('<div>');
        question.html(text);
        question.css({'padding': '5px'});
        question.appendTo(dialog);

        if (default_input_text && markdown_link) {
          // The web form container for the text box and buttons.
          var form = $('<form>');
          form.bind('submit', function(e) { return close(false); });
          form.css({
            'padding': '0',
            'margin': '0',
            'float': 'left',
            'width': '100%',
            'text-align': 'center',
            'position': 'relative'
          });
          form.appendTo(dialog);

          // The input text box
          input = $('<input>');
          input.attr({
            'type': 'text',
            'value': default_input_text
          });
          input.css({
            'display': 'block',
            'width': '80%',
            'margin': '0 auto'
          })
          input.appendTo(form);

          // The ok button
          var okButton = $('<button>');
          okButton.bind('click', function(e) { return close(false); })
          okButton.html('OK');
          okButton.css({'margin': '10px 5px'});
          okButton.appendTo(form);

          // The cancel button
          var cancelButton = $('<button>');
          cancelButton.bind('click', function(e) { return close(true); });
          cancelButton.html('Cancel');
          cancelButton.css({'margin': '10px 5px'});
          cancelButton.appendTo(form);
        }
        else {
          var cancelButton = $('<button>');
          cancelButton.bind('click', function(e) { return close(true); });
          cancelButton.html('Close');
          cancelButton.css({'margin': '10px 5px'});
          cancelButton.appendTo(dialog);
        }

        Util.addEvent(document.body, "keydown", checkEscape);

        dialog.css({
          'top': '50%',
          'left': '50%',
          'display': 'block'
        });

        var document_body = $(document.body);
        dialog.appendTo(document_body);

        // This has to be done AFTER adding the dialog to the form if you
        // want it to be centered.
        dialog.css({
          'margin-top': -(Position.height(dialog.get(0)) / 2) + 'px',
          'margin-left': -(Position.width(dialog.get(0)) / 2) + 'px'
        });
      }

      createBackground();

      // Why is this in a zero-length timeout?
      // Is it working around a browser bug?
      window.top.setTimeout(function() {
        createDialog();
        if (input) {
          var text_length = default_input_text.length;
          if (input.selectionStart !== undefined) {
            input.selectionStart = 0;
            input.selectionEnd = text_length;
          }
          else if (input.createTextRange) {
            var range = input.createTextRange();
            range.collapse(false);
            range.moveStart("character", -text_length);
            range.moveEnd("character", text_length);
            range.select();
          }
          input.focus();
        }
      }, 0);
    },

    makeAPI: function(wmd) {
      wmd.wmd = {};
      wmd.wmd.editor = wmd.editor;
      wmd.wmd.previewManager = wmd.previewManager;
    }
  };
})();


var Command = (function() {
  var prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
  var line_length = 40;
  var link_dialog_text = '<h4>Enter the web address</h4><p>You can also add a title, which will be displayed as a tool tip.</p><h4>Example:</h4><p>http://google.com/ \"Optional title\"</p>';
  var help_dialog_text = '<h4>Formatting help</h4><ul><li>*italic*</li><li>**bold**</li><li>An [example](http://url.com/ "Title")</li><li>![alt text](/path/img.jpg "Title")</li><li>* Unordered list item</li><li>1. Ordered list item</li></ul>';

  function add_lines(chunk, length) {
    remove_lines(chunk);
    var regex = new RegExp("(.{1," + length + "})( +|$\\n?)", "gm");
    chunk.selection = chunk.selection.replace(regex, function(line, marked) {
      if (new RegExp("^" + prefixes, "").test(line)) {
        return line;
      }
      return marked + "\n";
    });
    chunk.selection = chunk.selection.replace(/\s+$/, "");
  }
  
  function remove_lines(chunk) {
    var regex = new RegExp("([^\\n])\\n(?!(\\n|" + prefixes + "))", "g");
    chunk.selection = chunk.selection.replace(regex, "$1 $2");
  }

  function add_link(chunk, link_def) {
    var refNumber = 0; // The current reference number
    var defsToAdd = {};
    // Start with a clean slate by removing all previous link definitions.
    chunk.before = strip_link(chunk.before, defsToAdd);
    chunk.selection = strip_link(chunk.selection, defsToAdd);
    chunk.after = strip_link(chunk.after, defsToAdd);

    var defs = "";
    var regex = /(\[(?:\[[^\]]*\]|[^\[\]])*\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;

    var addDefNumber = function(def){
      refNumber++;
      def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, "  [" + refNumber + "]:");
      defs += "\n" + def;
    };

    var getLink = function(wholeMatch, link, id, end) {
      if (defsToAdd[id]) {
        addDefNumber(defsToAdd[id]);
        return link + refNumber + end;
      }
      return wholeMatch;
    };

    chunk.before = chunk.before.replace(regex, getLink);
    if (link_def) {
      addDefNumber(link_def);
    }
    else {
      chunk.selection = chunk.selection.replace(regex, getLink);
    }

    var refOut = refNumber;
    chunk.after = chunk.after.replace(regex, getLink);

    if (chunk.after) {
      chunk.after = chunk.after.replace(/\n*$/, "");
    }
    if (!chunk.after) {
      chunk.selection = chunk.selection.replace(/\n*$/, "");
    }

    chunk.after += "\n\n" + defs;
    return refOut;
  }

  function strip_link(text, defsToAdd) {
    text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, function(totalMatch, id, link, newlines, title) {
      defsToAdd[id] = totalMatch.replace(/\s*$/, "");
      if (newlines) {
        // Strip the title and return that separately.
        defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
        return newlines + title;
      }
      return "";
    });
    return text;
  }

  return {
    auto_indent: function(chunk, post_processing, default_text) {
      // Moves the cursor to the next line and continues lists, quotes and code.
      chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
      chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
      chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");

      var default_text = false;

      if(/(\n|^)[ ]{0,3}([*+-])[ \t]+.*\n$/.test(chunk.before)){
        if (this.list) {
          this.list(chunk, line_length, false, true);
        }
      }
      if(/(\n|^)[ ]{0,3}(\d+[.])[ \t]+.*\n$/.test(chunk.before)){
        if(this.list){
          this.list(chunk, line_length, true, true);
        }
      }
      if(/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)){
        if(this.blockquote){
          this.blockquote(chunk, default_text);
        }
      }
      if(/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)){
        if (this.code) {
          this.code(chunk, default_text);
        }
      }
    },

    blockquote: function(chunk, default_text) {
      chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
        function(totalMatch, newlinesBefore, text, newlinesAfter){
          chunk.before += newlinesBefore;
          chunk.after = newlinesAfter + chunk.after;
          return text;
        });

      chunk.before = chunk.before.replace(/(>[ \t]*)$/,
        function(totalMatch, blankLine){
          chunk.selection = blankLine + chunk.selection;
          return "";
        });

      var defaultText = default_text ? "Blockquote" : "";
      chunk.selection = chunk.selection.replace(/^(\s|>)+$/ ,"");
      chunk.selection = chunk.selection || defaultText;

      if (chunk.before) {
        chunk.before = chunk.before.replace(/\n?$/,"\n");
      }
      if (chunk.after) {
        chunk.after = chunk.after.replace(/^\n?/,"\n");
      }

      chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
        function(totalMatch){
          chunk.startTag = totalMatch;
          return "";
        });

      chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
        function(totalMatch){
          chunk.endTag = totalMatch;
          return "";
        });

      var replaceBlanksInTags = function(useBracket){
        var replacement = useBracket ? "> " : "";

        if (chunk.startTag) {
          chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
            function(totalMatch, markdown){
              return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
            });
        }
        if (chunk.endTag) {
          chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
            function(totalMatch, markdown){
              return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
            });
        }
      };

      if(/^(?![ ]{0,3}>)/m.test(chunk.selection)){
        add_lines(chunk, line_length - 2);
        chunk.selection = chunk.selection.replace(/^/gm, "> ");
        replaceBlanksInTags(true);
        chunk.addBlankLines();
      }
      else{
        chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
        remove_lines(chunk);
        replaceBlanksInTags(false);

        if(!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag){
          chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
        }

        if(!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag){
          chunk.endTag=chunk.endTag.replace(/^\n{0,2}/, "\n\n");
        }
      }

      if(!/\n/.test(chunk.selection)){
        chunk.selection = chunk.selection.replace(/^(> *)/,
        function(wholeMatch, blanks){
          chunk.startTag += blanks;
          return "";
        });
      }
    },

    dialog: function(chunk, post_processing, isHelp) {
      chunk.trimWhitespace();
      chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
      if (chunk.endTag.length > 1) {
        chunk.startTag = chunk.startTag.replace(/!?\[/, "");
        chunk.endTag = "";
        add_link(chunk, null);
      }
      else {
        if (/\n\n/.test(chunk.selection)) {
          add_link(chunk, null);
          return;
        }
        // The function to be executed when you enter a link and press OK or Cancel.
        // Marks up the link and adds the ref.
        var markdown_link = function(link) {
          if (link !== null) {
            chunk.startTag = chunk.endTag = "";
            var linkDef = " [999]: " + link;
            var num = add_link(chunk, linkDef);
            chunk.startTag = "[";
            chunk.endTag = "][" + num + "]";
            if (!chunk.selection) {
              chunk.selection = "link text";
            }
          }
          post_processing();
        };

        if (isHelp) {
          Util.prompt(help_dialog_text);
        }
        else {
          Util.prompt(link_dialog_text, 'http://', markdown_link);
        }
        return true;
      }
    },

    list: function(chunk, is_numbered, default_text) {
      // These are identical except at the very beginning and end.
      // Should probably use the regex extension function to make this clearer.
      var re_previous_items = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
      var re_next_items = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;

      var bullet = "-";   // The default bullet is a dash
      var num = 1;        // The number in a numbered list.

      // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
      var getItemPrefix = function() {
        var prefix;
        if(is_numbered){
          prefix = " " + num + ". ";
          num++;
        }
        else{
          prefix = " " + bullet + " ";
        }
        return prefix;
      };

      // Fixes the prefixes of the other list items.
      var getPrefixedItem = function(itemText) {
        // The numbering flag is unset when called by autoindent.
        if(is_numbered === undefined){
          is_numbered = /^\s*\d/.test(itemText);
        }

        // Renumber/bullet the list element.
        itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
          function( _ ) { return getItemPrefix(); });

        return itemText;
      };

      chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);

      if(chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)){
        chunk.before += chunk.startTag;
        chunk.startTag = "";
      }

      if (chunk.startTag) {
        var hasDigits = /\d+[.]/.test(chunk.startTag);
        chunk.startTag = "";
        chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
        remove_lines(chunk);
        chunk.addBlankLines();

        if(hasDigits){
          // Have to renumber the bullet points if this is a numbered list.
          chunk.after = chunk.after.replace(re_next_items, getPrefixedItem);
        }
        if(is_numbered == hasDigits){
          return;
        }
      }

      var nLinesBefore = 1;

      chunk.before = chunk.before.replace(re_previous_items,
        function(itemText){
          if(/^\s*([*+-])/.test(itemText)){
            bullet = RegExp.$1;
          }
          nLinesBefore = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
          return getPrefixedItem(itemText);
        });

      if(!chunk.selection){
        chunk.selection = default_text ? "List item" : " ";
      }

      var prefix = getItemPrefix();
      var nLinesAfter = 1;

      chunk.after = chunk.after.replace(re_next_items,
        function(itemText){
          nLinesAfter = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
          return getPrefixedItem(itemText);
        });

      chunk.trimWhitespace(true);
      chunk.addBlankLines(nLinesBefore, nLinesAfter, true);
      chunk.startTag = prefix;
      var spaces = prefix.replace(/./g, " ");
      add_lines(chunk, line_length - spaces.length);
      chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
    },

    heading: function(chunk) {
      // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
      chunk.selection = chunk.selection.replace(/\s+/g, " ");
      chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");

      // If we clicked the button with no selected text, we just
      // make a level 2 hash header around some default text.
      if(!chunk.selection) {
        chunk.startTag = "## ";
        chunk.selection = "Heading";
        chunk.endTag = " ##";
        return;
      }

      var headerLevel = 0; // The existing header level of the selected text.

      // Remove any existing hash heading markdown and save the header level.
      chunk.findTags(/#+[ ]*/, /[ ]*#+/);
      if(/#+/.test(chunk.startTag)){
        headerLevel = re.lastMatch.length;
      }
      chunk.startTag = chunk.endTag = "";

      // Try to get the current header level by looking for - and = in the line
      // below the selection.
      chunk.findTags(null, /\s?(-+|=+)/);
      if(/=+/.test(chunk.endTag)){
        headerLevel = 1;
      }
      if(/-+/.test(chunk.endTag)){
        headerLevel = 2;
      }

      // Skip to the next line so we can create the header markdown.
      chunk.startTag = chunk.endTag = "";
      chunk.addBlankLines(1, 1);
      // We make a level 2 header if there is no current header.
      // If there is a header level, we substract one from the header level.
      // If it's already a level 1 header, it's removed.
      var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;

      if(headerLevelToCreate > 0) {
        // The button only creates level 1 and 2 underline headers.
        // Why not have it iterate over hash header levels?  Wouldn't that be easier and cleaner?
        var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
        var len = chunk.selection.length;
        if(len > line_length){
          len = line_length;
        }
        chunk.endTag = "\n";
        while(len--){
          chunk.endTag += headerChar;
        }
      }
    },

    code: function(chunk, default_text) {
      var hasTextBefore = /\S[ ]*$/.test(chunk.before);
      var hasTextAfter = /^[ ]*\S/.test(chunk.after);

      // Use 'four space' markdown if the selection is on its own
      // line or is multiline.
      if((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)){
        chunk.before = chunk.before.replace(/[ ]{4}$/,
          function(totalMatch){
            chunk.selection = totalMatch + chunk.selection;
            return "";
          });

        var nLinesBefore = 1;
        var nLinesAfter = 1;

        if(/\n(\t|[ ]{4,}).*\n$/.test(chunk.before) || chunk.after === ""){
          nLinesBefore = 0;
        }
        if(/^\n(\t|[ ]{4,})/.test(chunk.after)){
          nLinesAfter = 0; // This needs to happen on line 1
        }

        chunk.addBlankLines(nLinesBefore, nLinesAfter);

        if(!chunk.selection){
          chunk.startTag = "    ";
          chunk.selection = default_text ? "enter code here" : "";
        }
        else {
          if(/^[ ]{0,3}\S/m.test(chunk.selection)){
            chunk.selection = chunk.selection.replace(/^/gm, "    ");
          }
          else{
            chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
          }
        }
      }
      else {
        // Use backticks (`) to delimit the code block.
        chunk.trimWhitespace();
        chunk.findTags(/`/, /`/);

        if(!chunk.startTag && !chunk.endTag){
          chunk.startTag = chunk.endTag="`";
          if(!chunk.selection){
            chunk.selection = default_text ? "enter code here" : "";
          }
        }
        else if(chunk.endTag && !chunk.startTag){
          chunk.before += chunk.endTag;
          chunk.endTag = "";
        }
        else{
          chunk.startTag = chunk.endTag="";
        }
      }
    },

    bold_italic: function(chunk, num_stars, default_text) {
      // Get rid of whitespace and fixup newlines.
      chunk.trimWhitespace();
      chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");

      // Look for stars before and after.  Is the chunk already marked up?
      chunk.before.search(/(\**$)/);
      var stars_before = RegExp.$1;

      chunk.after.search(/(^\**)/);
      var stars_after = RegExp.$1;
      var previous_stars = Math.min(stars_before.length, stars_after.length);

      // Remove stars if we have to since the button acts as a toggle.
      if ((previous_stars >= num_stars) && (previous_stars != 2 || num_stars != 1)) {
        chunk.before = chunk.before.replace(RegExp("[*]{" + num_stars + "}$", ""), "");
        chunk.after = chunk.after.replace(RegExp("^[*]{" + num_stars + "}", ""), "");
      }
      else if (!chunk.selection && stars_after) {
        // It's not really clear why this code is necessary.  It just moves
        // some arbitrary stuff around.
        chunk.after = chunk.after.replace(/^([*_]*)/, "");
        chunk.before = chunk.before.replace(/(\s?)$/, "");
        var whitespace = RegExp.$1;
        chunk.before = chunk.before + stars_after + whitespace;
      }
      else {
        // In most cases, if you don't have any selected text and click the button
        // you'll get a selected, marked up region with the default text inserted.
        if (!chunk.selection && !stars_after) {
          chunk.selection = default_text;
        }

        // Add the true markup.
        var markup = num_stars <= 1 ? "*" : "**"; // shouldn't the test be = ?
        chunk.before = chunk.before + markup;
        chunk.after = markup + chunk.after;
      }
      return;
    },

    horizontal_rule: function(chunk) {
      chunk.startTag = "----------\n";
      chunk.selection = "";
      chunk.addBlankLines(2, 1, true);
    }
  }
})();


var Position = (function() {
  return {
    top: function(element, is_inner) {
      var result = element.offsetTop;
      if (!is_inner) {
        while (element = element.offsetParent) {
          result += element.offsetTop;
        }
      }
      return result;
    },

    height: function(element) {
      return element.offsetHeight || element.scrollHeight;
    },

    width: function(element) {
      return element.offsetWidth || element.scrollWidth;
    },

    page_size: function() {
      var scroll_width;
      var scroll_height;
      var inner_width;
      var inner_height;

      // It's not very clear which blocks work with which browsers.
      if (document.body.scrollHeight > document.body.offsetHeight){
        scroll_width = document.body.scrollWidth;
        scroll_height = document.body.scrollHeight;
      }
      else {
        scroll_width = document.body.offsetWidth;
        scroll_height = document.body.offsetHeight;
      }

      if (document.documentElement && document.documentElement.clientHeight){
        // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
        inner_width = document.documentElement.clientWidth;
        inner_height = document.documentElement.clientHeight;
      }
      else if (document.body) {
        // Other versions of IE
        inner_width = document.body.clientWidth;
        inner_height = document.body.clientHeight;
      }

      var max_width = Math.max(scroll_width, inner_width);
      var max_height = Math.max(scroll_height, inner_height);

      return [max_width, max_height, inner_width, inner_height];
    }
  }
})();

