]> git.openstreetmap.org Git - osqa.git/blob - forum/skins/default/media/js/wmd/wmd.js
70271d4d08aa6c3703f6e4956cc08fd5b2c4463f
[osqa.git] / forum / skins / default / media / js / wmd / wmd.js
1 var Attacklab = Attacklab || {};
2
3 Attacklab.wmdBase = function(){
4
5         // A few handy aliases for readability.
6         var wmd  = top.Attacklab;
7         var doc  = top.document;
8         var re   = top.RegExp;
9         var nav  = top.navigator;
10         
11         // Some namespaces.
12         wmd.Util = {};
13         wmd.Position = {};
14         wmd.Command = {};
15         wmd.Global = {};
16         
17         var util = wmd.Util;
18         var position = wmd.Position;
19         var command = wmd.Command;
20         var global = wmd.Global;
21         
22         
23         // Used to work around some browser bugs where we can't use feature testing.
24         global.isIE = /msie/.test(nav.userAgent.toLowerCase());
25         global.isIE_5or6 = /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase());
26         global.isIE_7plus = global.isIE && !global.isIE_5or6;
27         global.isOpera = /opera/.test(nav.userAgent.toLowerCase());
28         global.isKonqueror = /konqueror/.test(nav.userAgent.toLowerCase());
29         
30         var toolbar_strong_label = $.i18n._('bold') + " <strong> Ctrl-B";
31     var toolbar_emphasis_label = $.i18n._('italic') + " <em> Ctrl-I";
32     var toolbar_hyperlink_label = $.i18n._('link') + " <a> Ctrl-L";
33     var toolbar_blockquote_label = $.i18n._('quote') + " <blockquote> Ctrl-.";
34     var toolbar_code_label = $.i18n._('preformatted text') + " <pre><code> Ctrl-K";
35     var toolbar_image_label = $.i18n._('image') + " <img> Ctrl-G";
36     var toolbar_numbered_label = $.i18n._('numbered list') + " <ol> Ctrl-O";
37     var toolbar_bulleted_label = $.i18n._('bulleted list') + " <ul> Ctrl-U";
38     var toolbar_heading_label = $.i18n._('heading') + " <h1>/<h2> Ctrl-H";
39     var toolbar_horizontal_label = $.i18n._('horizontal bar') + " <hr> Ctrl-R";
40     var toolbar_undo_label = $.i18n._('undo') + " Ctrl-Z";
41     var toolbar_redo_label = $.i18n._('redo') + " Ctrl-Y";
42     
43         // -------------------------------------------------------------------
44         //  YOUR CHANGES GO HERE
45         //
46         // I've tried to localize the things you are likely to change to 
47         // this area.
48         // -------------------------------------------------------------------
49         
50         // The text that appears on the upper part of the dialog box when
51         // entering links.
52         var imageDialogText = "<p style='margin-top: 0px'>" + $.i18n._('enter image url') + '</p>';
53         var linkDialogText = "<p style='margin-top: 0px'>" + $.i18n._('enter url') + '</p>';
54         var uploadImageHTML ="<div>" + $.i18n._('upload image') + "</div>" + 
55         "<input type=\"file\" name=\"file-upload\" id=\"file-upload\" size=\"26\" "+
56         "onchange=\"return ajaxFileUpload($('#image-url'));\"/><br>" +
57         "<img id=\"loading\" src=\"" + mediaUrl("media/images/indicator.gif") + "\" style=\"display:none;\"/>";
58     
59         // The default text that appears in the dialog input box when entering
60         // links.
61         var imageDefaultText = "http://";
62         var linkDefaultText = "http://";
63         
64         // The location of your button images relative to the base directory.
65         var imageDirectory = "images/";
66         
67         // Some intervals in ms.  These can be adjusted to reduce the control's load.
68         var previewPollInterval = 500;
69         var pastePollInterval = 100;
70         
71         // The link and title for the help button
72         var helpLink = "http://wmd-editor.com/";
73         var helpHoverTitle = "WMD website";
74         var helpTarget = "_blank";
75         
76         // -------------------------------------------------------------------
77         //  END OF YOUR CHANGES
78         // -------------------------------------------------------------------
79         
80         // A collection of the important regions on the page.
81         // Cached so we don't have to keep traversing the DOM.
82         wmd.PanelCollection = function(){
83                 this.buttonBar = doc.getElementById("wmd-button-bar");
84                 this.preview = doc.getElementById("previewer");
85                 this.output = doc.getElementById("wmd-output");
86                 this.input = doc.getElementById("editor");
87         };
88         
89         // This PanelCollection object can't be filled until after the page
90         // has loaded.
91         wmd.panels = undefined;
92         
93         // Internet explorer has problems with CSS sprite buttons that use HTML
94         // lists.  When you click on the background image "button", IE will 
95         // select the non-existent link text and discard the selection in the
96         // textarea.  The solution to this is to cache the textarea selection
97         // on the button's mousedown event and set a flag.  In the part of the
98         // code where we need to grab the selection, we check for the flag
99         // and, if it's set, use the cached area instead of querying the
100         // textarea.
101         //
102         // This ONLY affects Internet Explorer (tested on versions 6, 7
103         // and 8) and ONLY on button clicks.  Keyboard shortcuts work
104         // normally since the focus never leaves the textarea.
105         wmd.ieCachedRange = null;               // cached textarea selection
106         wmd.ieRetardedClick = false;    // flag
107         
108         // Returns true if the DOM element is visible, false if it's hidden.
109         // Checks if display is anything other than none.
110         util.isVisible = function (elem) {
111         
112             if (window.getComputedStyle) {
113                 // Most browsers
114                         return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none";
115                 }
116                 else if (elem.currentStyle) {
117                     // IE
118                         return elem.currentStyle.display !== "none";
119                 }
120         };
121         
122         
123         // Adds a listener callback to a DOM element which is fired on a specified
124         // event.
125         util.addEvent = function(elem, event, listener){
126                 if (elem.attachEvent) {
127                         // IE only.  The "on" is mandatory.
128                         elem.attachEvent("on" + event, listener);
129                 }
130                 else {
131                         // Other browsers.
132                         elem.addEventListener(event, listener, false);
133                 }
134         };
135
136         
137         // Removes a listener callback from a DOM element which is fired on a specified
138         // event.
139         util.removeEvent = function(elem, event, listener){
140                 if (elem.detachEvent) {
141                         // IE only.  The "on" is mandatory.
142                         elem.detachEvent("on" + event, listener);
143                 }
144                 else {
145                         // Other browsers.
146                         elem.removeEventListener(event, listener, false);
147                 }
148         };
149
150         // Converts \r\n and \r to \n.
151         util.fixEolChars = function(text){
152                 text = text.replace(/\r\n/g, "\n");
153                 text = text.replace(/\r/g, "\n");
154                 return text;
155         };
156
157         // Extends a regular expression.  Returns a new RegExp
158         // using pre + regex + post as the expression.
159         // Used in a few functions where we have a base
160         // expression and we want to pre- or append some
161         // conditions to it (e.g. adding "$" to the end).
162         // The flags are unchanged.
163         //
164         // regex is a RegExp, pre and post are strings.
165         util.extendRegExp = function(regex, pre, post){
166                 
167                 if (pre === null || pre === undefined)
168                 {
169                         pre = "";
170                 }
171                 if(post === null || post === undefined)
172                 {
173                         post = "";
174                 }
175                 
176                 var pattern = regex.toString();
177                 var flags;
178                 
179                 // Replace the flags with empty space and store them.
180                 pattern = pattern.replace(/\/([gim]*)$/, "");
181                 flags = re.$1;
182                 
183                 // Remove the slash delimiters on the regular expression.
184                 pattern = pattern.replace(/(^\/|\/$)/g, "");
185                 pattern = pre + pattern + post;
186                 
187                 return new re(pattern, flags);
188         };
189
190         
191         // Sets the image for a button passed to the WMD editor.
192         // Returns a new element with the image attached.
193         // Adds several style properties to the image.
194         util.createImage = function(img){
195                 
196                 var imgPath = imageDirectory + img;
197                 
198                 var elem = doc.createElement("img");
199                 elem.className = "wmd-button";
200                 elem.src = imgPath;
201
202                 return elem;
203         };
204         
205
206         // This simulates a modal dialog box and asks for the URL when you
207         // click the hyperlink or image buttons.
208         //
209         // text: The html for the input box.
210         // defaultInputText: The default value that appears in the input box.
211         // makeLinkMarkdown: The function which is executed when the prompt is dismissed, either via OK or Cancel
212         util.prompt = function(text, defaultInputText, makeLinkMarkdown){
213         
214                 // These variables need to be declared at this level since they are used
215                 // in multiple functions.
216                 var dialog;                     // The dialog box.
217                 var background;         // The background beind the dialog box.
218                 var input;                      // The text box where you enter the hyperlink.
219         var type = 0;
220         // The dialog box type(0: Link, 1: Image)
221         if(arguments.length == 4){
222             type = arguments[3];
223         }
224
225                 if (defaultInputText === undefined) {
226                         defaultInputText = "";
227                 }
228                 
229                 // Used as a keydown event handler. Esc dismisses the prompt.
230                 // Key code 27 is ESC.
231                 var checkEscape = function(key){
232                         var code = (key.charCode || key.keyCode);
233                         if (code === 27) {
234                                 close(true);
235                         }
236                 };
237                 
238                 // Dismisses the hyperlink input box.
239                 // isCancel is true if we don't care about the input text.
240                 // isCancel is false if we are going to keep the text.
241                 var close = function(isCancel){
242                         util.removeEvent(doc.body, "keydown", checkEscape);
243                         var text = input.value;
244
245                         if (isCancel){
246                                 text = null;
247                         }
248                         else{
249                                 // Fixes common pasting errors.
250                                 text = text.replace('http://http://', 'http://');
251                                 text = text.replace('http://https://', 'https://');
252                                 text = text.replace('http://ftp://', 'ftp://');
253                                 
254                                 if (text.indexOf('http://') === -1 && text.indexOf('ftp://') === -1) {
255                                         text = 'http://' + text;
256                                 }
257                         }
258                         
259                         dialog.parentNode.removeChild(dialog);
260                         background.parentNode.removeChild(background);
261                         makeLinkMarkdown(text);
262                         return false;
263                 };
264                 
265                 // Creates the background behind the hyperlink text entry box.
266                 // Most of this has been moved to CSS but the div creation and
267                 // browser-specific hacks remain here.
268                 var createBackground = function(){
269                 
270                         background = doc.createElement("div");
271                         background.className = "wmd-prompt-background";
272                         style = background.style;
273                         style.position = "absolute";
274                         style.top = "0";
275                         
276                         style.zIndex = "1000";
277                         
278                         // Some versions of Konqueror don't support transparent colors
279                         // so we make the whole window transparent.
280                         //
281                         // Is this necessary on modern konqueror browsers?
282                         if (global.isKonqueror){
283                                 style.backgroundColor = "transparent";
284                         }
285                         else if (global.isIE){
286                                 style.filter = "alpha(opacity=50)";
287                         }
288                         else {
289                                 style.opacity = "0.5";
290                         }
291                         
292                         var pageSize = position.getPageSize();
293                         style.height = pageSize[1] + "px";
294                         
295                         if(global.isIE){
296                                 style.left = doc.documentElement.scrollLeft;
297                                 style.width = doc.documentElement.clientWidth;
298                         }
299                         else {
300                                 style.left = "0";
301                                 style.width = "100%";
302                         }
303                         
304                         doc.body.appendChild(background);
305                 };
306                 
307                 // Create the text input box form/window.
308                 var createDialog = function(){
309                 
310                         // The main dialog box.
311                         dialog = doc.createElement("div");
312                         dialog.className = "wmd-prompt-dialog";
313                         dialog.style.padding = "10px;";
314                         dialog.style.position = "fixed";
315                         dialog.style.width = "400px";
316                         dialog.style.zIndex = "1001";
317                         
318                         // The dialog text.
319                         var question = doc.createElement("div");
320                         question.innerHTML = text;
321                         question.style.padding = "5px";
322                         dialog.appendChild(question);
323                         
324                         // The web form container for the text box and buttons.
325                         var form = doc.createElement("form");
326                         form.onsubmit = function(){ return close(false); };
327                         style = form.style;
328                         style.padding = "0";
329                         style.margin = "0";
330                         style.cssFloat = "left";
331                         style.width = "100%";
332                         style.textAlign = "center";
333                         style.position = "relative";
334                         dialog.appendChild(form);
335                         
336                         // The input text box
337                         input = doc.createElement("input");
338             if(type == 1){
339                 input.id = "image-url";
340             }
341                         input.type = "text";
342                         input.value = defaultInputText;
343                         style = input.style;
344                         style.display = "block";
345                         style.width = "80%";
346                         style.marginLeft = style.marginRight = "auto";
347                         form.appendChild(input);
348                         
349             // The upload file input
350             if(type == 1){
351                 var upload = doc.createElement("div");
352                 upload.innerHTML = uploadImageHTML;
353                 upload.style.padding = "5px";
354                 form.appendChild(upload);   
355             }
356             
357                         // The ok button
358                         var okButton = doc.createElement("input");
359                         okButton.type = "button";
360                         okButton.onclick = function(){ return close(false); };
361                         okButton.value = "OK";
362                         style = okButton.style;
363                         style.margin = "10px";
364                         style.display = "inline";
365                         style.width = "7em";
366
367                         
368                         // The cancel button
369                         var cancelButton = doc.createElement("input");
370                         cancelButton.type = "button";
371                         cancelButton.onclick = function(){ return close(true); };
372                         cancelButton.value = "Cancel";
373                         style = cancelButton.style;
374                         style.margin = "10px";
375                         style.display = "inline";
376                         style.width = "7em";
377
378                         // The order of these buttons is different on macs.
379                         if (/mac/.test(nav.platform.toLowerCase())) {
380                                 form.appendChild(cancelButton);
381                                 form.appendChild(okButton);
382                         }
383                         else {
384                                 form.appendChild(okButton);
385                                 form.appendChild(cancelButton);
386                         }
387
388                         util.addEvent(doc.body, "keydown", checkEscape);
389                         dialog.style.top = "50%";
390                         dialog.style.left = "50%";
391                         dialog.style.display = "block";
392                         if(global.isIE_5or6){
393                                 dialog.style.position = "absolute";
394                                 dialog.style.top = doc.documentElement.scrollTop + 200 + "px";
395                                 dialog.style.left = "50%";
396                         }
397                         doc.body.appendChild(dialog);
398                         
399                         // This has to be done AFTER adding the dialog to the form if you
400                         // want it to be centered.
401                         dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";
402                         dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";
403                         
404                 };
405                 
406                 createBackground();
407                 
408                 // Why is this in a zero-length timeout?
409                 // Is it working around a browser bug?
410                 top.setTimeout(function(){
411                 
412                         createDialog();
413
414                         var defTextLen = defaultInputText.length;
415                         if (input.selectionStart !== undefined) {
416                                 input.selectionStart = 0;
417                                 input.selectionEnd = defTextLen;
418                         }
419                         else if (input.createTextRange) {
420                                 var range = input.createTextRange();
421                                 range.collapse(false);
422                                 range.moveStart("character", -defTextLen);
423                                 range.moveEnd("character", defTextLen);
424                                 range.select();
425                         }
426                         
427                         input.focus();
428                 }, 0);
429         };
430         
431         
432         // UNFINISHED
433         // The assignment in the while loop makes jslint cranky.
434         // I'll change it to a better loop later.
435         position.getTop = function(elem, isInner){
436                 var result = elem.offsetTop;
437                 if (!isInner) {
438         while (elem.offsetParent) {
439             elem = elem.offsetParent;
440             result += elem.offsetTop;
441         }
442                 }
443                 return result;
444         };
445         
446         position.getHeight = function (elem) {
447                 return elem.offsetHeight || elem.scrollHeight;
448         };
449
450         position.getWidth = function (elem) {
451                 return elem.offsetWidth || elem.scrollWidth;
452         };
453
454         position.getPageSize = function(){
455                 
456                 var scrollWidth, scrollHeight;
457                 var innerWidth, innerHeight;
458                 
459                 // It's not very clear which blocks work with which browsers.
460                 if(self.innerHeight && self.scrollMaxY){
461                         scrollWidth = doc.body.scrollWidth;
462                         scrollHeight = self.innerHeight + self.scrollMaxY;
463                 }
464                 else if(doc.body.scrollHeight > doc.body.offsetHeight){
465                         scrollWidth = doc.body.scrollWidth;
466                         scrollHeight = doc.body.scrollHeight;
467                 }
468                 else{
469                         scrollWidth = doc.body.offsetWidth;
470                         scrollHeight = doc.body.offsetHeight;
471                 }
472                 
473                 if(self.innerHeight){
474                         // Non-IE browser
475                         innerWidth = self.innerWidth;
476                         innerHeight = self.innerHeight;
477                 }
478                 else if(doc.documentElement && doc.documentElement.clientHeight){
479                         // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
480                         innerWidth = doc.documentElement.clientWidth;
481                         innerHeight = doc.documentElement.clientHeight;
482                 }
483                 else if(doc.body){
484                         // Other versions of IE
485                         innerWidth = doc.body.clientWidth;
486                         innerHeight = doc.body.clientHeight;
487                 }
488                 
489         var maxWidth = Math.max(scrollWidth, innerWidth);
490         var maxHeight = Math.max(scrollHeight, innerHeight);
491         return [maxWidth, maxHeight, innerWidth, innerHeight];
492         };
493         
494         // Watches the input textarea, polling at an interval and runs
495         // a callback function if anything has changed.
496         wmd.inputPoller = function(callback, interval){
497         
498                 var pollerObj = this;
499                 var inputArea = wmd.panels.input;
500                 
501                 // Stored start, end and text.  Used to see if there are changes to the input.
502                 var lastStart;
503                 var lastEnd;
504                 var markdown;
505                 
506                 var killHandle; // Used to cancel monitoring on destruction.
507                 // Checks to see if anything has changed in the textarea.
508                 // If so, it runs the callback.
509                 this.tick = function(){
510                 
511                         if (!util.isVisible(inputArea)) {
512                                 return;
513                         }
514                         
515                         // Update the selection start and end, text.
516                         if (inputArea.selectionStart || inputArea.selectionStart === 0) {
517                                 var start = inputArea.selectionStart;
518                                 var end = inputArea.selectionEnd;
519                                 if (start != lastStart || end != lastEnd) {
520                                         lastStart = start;
521                                         lastEnd = end;
522                                         
523                                         if (markdown != inputArea.value) {
524                                                 markdown = inputArea.value;
525                                                 return true;
526                                         }
527                                 }
528                         }
529                         return false;
530                 };
531                 
532                 
533                 var doTickCallback = function(){
534                 
535                         if (!util.isVisible(inputArea)) {
536                                 return;
537                         }
538                         
539                         // If anything has changed, call the function.
540                         if (pollerObj.tick()) {
541                                 callback();
542                         }
543                 };
544                 
545                 // Set how often we poll the textarea for changes.
546                 var assignInterval = function(){
547                         // previewPollInterval is set at the top of the namespace.
548                         killHandle = top.setInterval(doTickCallback, interval);
549                 };
550                 
551                 this.destroy = function(){
552                         top.clearInterval(killHandle);
553                 };
554                 
555                 assignInterval();
556         };
557         
558         // Handles pushing and popping TextareaStates for undo/redo commands.
559         // I should rename the stack variables to list.
560         wmd.undoManager = function(callback){
561         
562                 var undoObj = this;
563                 var undoStack = []; // A stack of undo states
564                 var stackPtr = 0; // The index of the current state
565                 var mode = "none";
566                 var lastState; // The last state
567                 var poller;
568                 var timer; // The setTimeout handle for cancelling the timer
569                 var inputStateObj;
570                 
571                 // Set the mode for later logic steps.
572                 var setMode = function(newMode, noSave){
573                 
574                         if (mode != newMode) {
575                                 mode = newMode;
576                                 if (!noSave) {
577                                         saveState();
578                                 }
579                         }
580                         
581                         if (!global.isIE || mode != "moving") {
582                                 timer = top.setTimeout(refreshState, 1);
583                         }
584                         else {
585                                 inputStateObj = null;
586                         }
587                 };
588                 
589                 var refreshState = function(){
590                         inputStateObj = new wmd.TextareaState();
591                         poller.tick();
592                         timer = undefined;
593                 };
594                 
595                 this.setCommandMode = function(){
596                         mode = "command";
597                         saveState();
598                         timer = top.setTimeout(refreshState, 0);
599                 };
600                 
601                 this.canUndo = function(){
602                         return stackPtr > 1;
603                 };
604                 
605                 this.canRedo = function(){
606                         if (undoStack[stackPtr + 1]) {
607                                 return true;
608                         }
609                         return false;
610                 };
611                 
612                 // Removes the last state and restores it.
613                 this.undo = function(){
614                 
615                         if (undoObj.canUndo()) {
616                                 if (lastState) {
617                                         // What about setting state -1 to null or checking for undefined?
618                                         lastState.restore();
619                                         lastState = null;
620                                 }
621                                 else {
622                                         undoStack[stackPtr] = new wmd.TextareaState();
623                                         undoStack[--stackPtr].restore();
624                                         
625                                         if (callback) {
626                                                 callback();
627                                         }
628                                 }
629                         }
630                         
631                         mode = "none";
632                         wmd.panels.input.focus();
633                         refreshState();
634                 };
635                 
636                 // Redo an action.
637                 this.redo = function(){
638                 
639                         if (undoObj.canRedo()) {
640                         
641                                 undoStack[++stackPtr].restore();
642                                 
643                                 if (callback) {
644                                         callback();
645                                 }
646                         }
647                         
648                         mode = "none";
649                         wmd.panels.input.focus();
650                         refreshState();
651                 };
652                 
653                 // Push the input area state to the stack.
654                 var saveState = function(){
655                 
656                         var currState = inputStateObj || new wmd.TextareaState();
657                         
658                         if (!currState) {
659                                 return false;
660                         }
661                         if (mode == "moving") {
662                                 if (!lastState) {
663                                         lastState = currState;
664                                 }
665                                 return;
666                         }
667                         if (lastState) {
668                                 if (undoStack[stackPtr - 1].text != lastState.text) {
669                                         undoStack[stackPtr++] = lastState;
670                                 }
671                                 lastState = null;
672                         }
673                         undoStack[stackPtr++] = currState;
674                         undoStack[stackPtr + 1] = null;
675                         if (callback) {
676                                 callback();
677                         }
678                 };
679                 
680                 var handleCtrlYZ = function(event){
681                 
682                         var handled = false;
683                         
684                         if (event.ctrlKey || event.metaKey) {
685                         
686                                 // IE and Opera do not support charCode.
687                                 var keyCode = event.charCode || event.keyCode;
688                                 var keyCodeChar = String.fromCharCode(keyCode);
689                                 
690                                 switch (keyCodeChar) {
691                                 
692                                         case "y":
693                                                 undoObj.redo();
694                                                 handled = true;
695                                                 break;
696                                                 
697                                         case "z":
698                                                 if (!event.shiftKey) {
699                                                         undoObj.undo();
700                                                 }
701                                                 else {
702                                                         undoObj.redo();
703                                                 }
704                                                 handled = true;
705                                                 break;
706                                 }
707                         }
708                         
709                         if (handled) {
710                                 if (event.preventDefault) {
711                                         event.preventDefault();
712                                 }
713                                 if (top.event) {
714                                         top.event.returnValue = false;
715                                 }
716                                 return;
717                         }
718                 };
719                 
720                 // Set the mode depending on what is going on in the input area.
721                 var handleModeChange = function(event){
722                 
723                         if (!event.ctrlKey && !event.metaKey) {
724                         
725                                 var keyCode = event.keyCode;
726                                 
727                                 if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
728                                         // 33 - 40: page up/dn and arrow keys
729                                         // 63232 - 63235: page up/dn and arrow keys on safari
730                                         setMode("moving");
731                                 }
732                                 else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
733                                         // 8: backspace
734                                         // 46: delete
735                                         // 127: delete
736                                         setMode("deleting");
737                                 }
738                                 else if (keyCode == 13) {
739                                         // 13: Enter
740                                         setMode("newlines");
741                                 }
742                                 else if (keyCode == 27) {
743                                         // 27: escape
744                                         setMode("escape");
745                                 }
746                                 else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
747                                         // 16-20 are shift, etc. 
748                                         // 91: left window key
749                                         // I think this might be a little messed up since there are
750                                         // a lot of nonprinting keys above 20.
751                                         setMode("typing");
752                                 }
753                         }
754                 };
755                 
756                 var setEventHandlers = function(){
757                 
758                         util.addEvent(wmd.panels.input, "keypress", function(event){
759                                 // keyCode 89: y
760                                 // keyCode 90: z
761                                 if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) {
762                                         event.preventDefault();
763                                 }
764                         });
765                         
766                         var handlePaste = function(){
767                                 if (global.isIE || (inputStateObj && inputStateObj.text != wmd.panels.input.value)) {
768                                         if (timer === undefined) {
769                                                 mode = "paste";
770                                                 saveState();
771                                                 refreshState();
772                                         }
773                                 }
774                         };
775                         
776                         // pastePollInterval is specified at the beginning of this namespace.
777                         poller = new wmd.inputPoller(handlePaste, pastePollInterval);
778                         
779                         util.addEvent(wmd.panels.input, "keydown", handleCtrlYZ);
780                         util.addEvent(wmd.panels.input, "keydown", handleModeChange);
781                         
782                         util.addEvent(wmd.panels.input, "mousedown", function(){
783                                 setMode("moving");
784                         });
785                         wmd.panels.input.onpaste = handlePaste;
786                         wmd.panels.input.ondrop = handlePaste;
787                 };
788                 
789                 var init = function(){
790                         setEventHandlers();
791                         refreshState();
792                         saveState();
793                 };
794                 
795                 this.destroy = function(){
796                         if (poller) {
797                                 poller.destroy();
798                         }
799                 };
800                 
801                 init();
802         };
803         
804         // I think my understanding of how the buttons and callbacks are stored in the array is incomplete.
805         wmd.editor = function(previewRefreshCallback){
806         
807                 if (!previewRefreshCallback) {
808                         previewRefreshCallback = function(){};
809                 }
810                 
811                 var inputBox = wmd.panels.input;
812                 
813                 var offsetHeight = 0;
814                 
815                 var editObj = this;
816                 
817                 var mainDiv;
818                 var mainSpan;
819                 
820                 var div; // This name is pretty ambiguous.  I should rename this.
821                 
822                 // Used to cancel recurring events from setInterval.
823                 var creationHandle;
824                 
825                 var undoMgr; // The undo manager
826                 
827                 // Perform the button's action.
828                 var doClick = function(button){
829                 
830                         inputBox.focus();
831                         
832                         if (button.textOp) {
833                                 
834                                 if (undoMgr) {
835                                         undoMgr.setCommandMode();
836                                 }
837                                 
838                                 var state = new wmd.TextareaState();
839                                 
840                                 if (!state) {
841                                         return;
842                                 }
843                                 
844                                 var chunks = state.getChunks();
845                                 
846                                 // Some commands launch a "modal" prompt dialog.  Javascript
847                                 // can't really make a modal dialog box and the WMD code
848                                 // will continue to execute while the dialog is displayed.
849                                 // This prevents the dialog pattern I'm used to and means
850                                 // I can't do something like this:
851                                 //
852                                 // var link = CreateLinkDialog();
853                                 // makeMarkdownLink(link);
854                                 // 
855                                 // Instead of this straightforward method of handling a
856                                 // dialog I have to pass any code which would execute
857                                 // after the dialog is dismissed (e.g. link creation)
858                                 // in a function parameter.
859                                 //
860                                 // Yes this is awkward and I think it sucks, but there's
861                                 // no real workaround.  Only the image and link code
862                                 // create dialogs and require the function pointers.
863                                 var fixupInputArea = function(){
864                                 
865                                         inputBox.focus();
866                                         
867                                         if (chunks) {
868                                                 state.setChunks(chunks);
869                                         }
870                                         
871                                         state.restore();
872                                         previewRefreshCallback();
873                                 };
874                                 
875                                 var noCleanup = button.textOp(chunks, fixupInputArea);
876                                 
877                                 if(!noCleanup) {
878                                         fixupInputArea();
879                                 }
880                                 
881                         }
882                         
883                         if (button.execute) {
884                                 button.execute(editObj);
885                         }
886                 };
887                         
888                 var setUndoRedoButtonStates = function(){
889                         if(undoMgr){
890                                 setupButton(document.getElementById("wmd-undo-button"), undoMgr.canUndo());
891                                 setupButton(document.getElementById("wmd-redo-button"), undoMgr.canRedo());
892                         }
893                 };
894                 
895                 var setupButton = function(button, isEnabled) {
896                 
897                         var normalYShift = "0px";
898                         var disabledYShift = "-20px";
899                         var highlightYShift = "-40px";
900                         
901                         if(isEnabled) {
902                                 button.style.backgroundPosition = button.XShift + " " + normalYShift;
903                                 button.onmouseover = function(){
904                                         this.style.backgroundPosition = this.XShift + " " + highlightYShift;
905                                 };
906                                                         
907                                 button.onmouseout = function(){
908                                         this.style.backgroundPosition = this.XShift + " " + normalYShift;
909                                 };
910                                 
911                                 // IE tries to select the background image "button" text (it's
912                                 // implemented in a list item) so we have to cache the selection
913                                 // on mousedown.
914                                 if(global.isIE) {
915                                         button.onmousedown =  function() { 
916                                                 wmd.ieRetardedClick = true;
917                                                 wmd.ieCachedRange = document.selection.createRange(); 
918                                         };
919                                 }
920                                 
921                                 if (!button.isHelp)
922                                 {
923                                         button.onclick = function() {
924                                                 if (this.onmouseout) {
925                                                         this.onmouseout();
926                                                 }
927                                                 doClick(this);
928                                                 return false;
929                                         };
930                                 }
931                         }
932                         else {
933                                 button.style.backgroundPosition = button.XShift + " " + disabledYShift;
934                                 button.onmouseover = button.onmouseout = button.onclick = function(){};
935                         }
936                 };
937
938                 var makeSpritedButtonRow = function(){
939                         var buttonBar = document.getElementById("wmd-button-bar");
940                         var normalYShift = "0px";
941                         var disabledYShift = "-20px";
942                         var highlightYShift = "-40px";
943                         
944                         var buttonRow = document.createElement("ul");
945                         buttonRow.id = "wmd-button-row";
946                         buttonRow = buttonBar.appendChild(buttonRow);
947
948                         
949                         var boldButton = document.createElement("li");
950                         boldButton.className = "wmd-button";
951                         boldButton.id = "wmd-bold-button";
952                         boldButton.title = toolbar_strong_label;
953                         boldButton.XShift = "0px";
954                         boldButton.textOp = command.doBold;
955                         setupButton(boldButton, true);
956                         buttonRow.appendChild(boldButton);
957                         
958                         var italicButton = document.createElement("li");
959                         italicButton.className = "wmd-button";
960                         italicButton.id = "wmd-italic-button";
961                         italicButton.title = toolbar_emphasis_label;
962                         italicButton.XShift = "-20px";
963                         italicButton.textOp = command.doItalic;
964                         setupButton(italicButton, true);
965                         buttonRow.appendChild(italicButton);
966
967                         var spacer1 = document.createElement("li");
968                         spacer1.className = "wmd-spacer";
969                         spacer1.id = "wmd-spacer1";
970                         buttonRow.appendChild(spacer1); 
971
972                         var linkButton = document.createElement("li");
973                         linkButton.className = "wmd-button";
974                         linkButton.id = "wmd-link-button";
975                         linkButton.title = toolbar_hyperlink_label;
976                         linkButton.XShift = "-40px";
977                         linkButton.textOp = function(chunk, postProcessing){
978                                 return command.doLinkOrImage(chunk, postProcessing, false);
979                         };
980                         setupButton(linkButton, true);
981                         buttonRow.appendChild(linkButton);
982
983                         var quoteButton = document.createElement("li");
984                         quoteButton.className = "wmd-button";
985                         quoteButton.id = "wmd-quote-button";
986                         quoteButton.title = toolbar_blockquote_label;
987                         quoteButton.XShift = "-60px";
988                         quoteButton.textOp = command.doBlockquote;
989                         setupButton(quoteButton, true);
990                         buttonRow.appendChild(quoteButton);
991                         
992                         var codeButton = document.createElement("li");
993                         codeButton.className = "wmd-button";
994                         codeButton.id = "wmd-code-button";
995                         codeButton.title = toolbar_code_label;
996                         codeButton.XShift = "-80px";
997                         codeButton.textOp = command.doCode;
998                         setupButton(codeButton, true);
999                         buttonRow.appendChild(codeButton);
1000
1001                         var imageButton = document.createElement("li");
1002                         imageButton.className = "wmd-button";
1003                         imageButton.id = "wmd-image-button";
1004                         imageButton.title = toolbar_image_label;
1005                         imageButton.XShift = "-100px";
1006                         imageButton.textOp = function(chunk, postProcessing){
1007                                 return command.doLinkOrImage(chunk, postProcessing, true);
1008                         };
1009                         setupButton(imageButton, true);
1010                         buttonRow.appendChild(imageButton);
1011
1012                         var spacer2 = document.createElement("li");
1013                         spacer2.className = "wmd-spacer";
1014                         spacer2.id = "wmd-spacer2";
1015                         buttonRow.appendChild(spacer2); 
1016
1017                         var olistButton = document.createElement("li");
1018                         olistButton.className = "wmd-button";
1019                         olistButton.id = "wmd-olist-button";
1020                         olistButton.title = toolbar_numbered_label;
1021                         olistButton.XShift = "-120px";
1022                         olistButton.textOp = function(chunk, postProcessing){
1023                                 command.doList(chunk, postProcessing, true);
1024                         };
1025                         setupButton(olistButton, true);
1026                         buttonRow.appendChild(olistButton);
1027                         
1028                         var ulistButton = document.createElement("li");
1029                         ulistButton.className = "wmd-button";
1030                         ulistButton.id = "wmd-ulist-button";
1031                         ulistButton.title = toolbar_bulleted_label;
1032                         ulistButton.XShift = "-140px";
1033                         ulistButton.textOp = function(chunk, postProcessing){
1034                                 command.doList(chunk, postProcessing, false);
1035                         };
1036                         setupButton(ulistButton, true);
1037                         buttonRow.appendChild(ulistButton);
1038                         
1039                         var headingButton = document.createElement("li");
1040                         headingButton.className = "wmd-button";
1041                         headingButton.id = "wmd-heading-button";
1042                         headingButton.title = toolbar_heading_label;
1043                         headingButton.XShift = "-160px";
1044                         headingButton.textOp = command.doHeading;
1045                         setupButton(headingButton, true);
1046                         buttonRow.appendChild(headingButton); 
1047                         
1048                         var hrButton = document.createElement("li");
1049                         hrButton.className = "wmd-button";
1050                         hrButton.id = "wmd-hr-button";
1051                         hrButton.title = toolbar_horizontal_label;
1052                         hrButton.XShift = "-180px";
1053                         hrButton.textOp = command.doHorizontalRule;
1054                         setupButton(hrButton, true);
1055                         buttonRow.appendChild(hrButton); 
1056                         
1057                         var spacer3 = document.createElement("li");
1058                         spacer3.className = "wmd-spacer";
1059                         spacer3.id = "wmd-spacer3";
1060                         buttonRow.appendChild(spacer3); 
1061                         
1062                         var undoButton = document.createElement("li");
1063                         undoButton.className = "wmd-button";
1064                         undoButton.id = "wmd-undo-button";
1065                         undoButton.title = toolbar_undo_label;
1066                         undoButton.XShift = "-200px";
1067                         undoButton.execute = function(manager){
1068                                 manager.undo();
1069                         };
1070                         setupButton(undoButton, true);
1071                         buttonRow.appendChild(undoButton); 
1072                         
1073                         var redoButton = document.createElement("li");
1074                         redoButton.className = "wmd-button";
1075                         redoButton.id = "wmd-redo-button";
1076                         redoButton.title = toolbar_redo_label;
1077                         if (/win/.test(nav.platform.toLowerCase())) {
1078                                 redoButton.title = toolbar_redo_label;
1079                         }
1080                         else {
1081                                 // mac and other non-Windows platforms
1082                                 redoButton.title = $.i18n._('redo') + " - Ctrl+Shift+Z";
1083                         }
1084                         redoButton.XShift = "-220px";
1085                         redoButton.execute = function(manager){
1086                                 manager.redo();
1087                         };
1088                         setupButton(redoButton, true);
1089                         buttonRow.appendChild(redoButton); 
1090                         /*
1091                         var helpButton = document.createElement("li");
1092                         helpButton.className = "wmd-button";
1093                         helpButton.id = "wmd-help-button";
1094                         helpButton.XShift = "-240px";
1095                         helpButton.isHelp = true;
1096                         
1097                         var helpAnchor = document.createElement("a");
1098                         helpAnchor.href = helpLink;
1099                         helpAnchor.target = helpTarget
1100                         helpAnchor.title = helpHoverTitle;
1101                         helpButton.appendChild(helpAnchor);
1102                         
1103                         setupButton(helpButton, true);
1104                         buttonRow.appendChild(helpButton);
1105                         */
1106                         setUndoRedoButtonStates();
1107                 };
1108                 
1109                 var setupEditor = function(){
1110                 
1111                         if (/\?noundo/.test(doc.location.href)) {
1112                                 wmd.nativeUndo = true;
1113                         }
1114                         
1115                         if (!wmd.nativeUndo) {
1116                                 undoMgr = new wmd.undoManager(function(){
1117                                         previewRefreshCallback();
1118                                         setUndoRedoButtonStates();
1119                                 });
1120                         }
1121                         
1122                         makeSpritedButtonRow();
1123                         
1124                         
1125                         var keyEvent = "keydown";
1126                         if (global.isOpera) {
1127                                 keyEvent = "keypress";
1128                         }
1129                         
1130                         util.addEvent(inputBox, keyEvent, function(key){
1131                                 
1132                                 // Check to see if we have a button key and, if so execute the callback.
1133                                 if (key.ctrlKey || key.metaKey) {
1134                                 
1135                                         var keyCode = key.charCode || key.keyCode;
1136                                         var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
1137                                         
1138                                         // Bugfix for messed up DEL and .
1139                                         if (keyCode === 46) {
1140                                                 keyCodeStr = "";
1141                                         }
1142                                         if (keyCode === 190) {
1143                                                 keyCodeStr = ".";
1144                                         }
1145                                         
1146                                         switch(keyCodeStr) {
1147                                                 case "b":
1148                                                         doClick(document.getElementById("wmd-bold-button"));
1149                                                         break;
1150                                                 case "i":
1151                                                         doClick(document.getElementById("wmd-italic-button"));
1152                                                         break;
1153                                                 case "l":
1154                                                         doClick(document.getElementById("wmd-link-button"));
1155                                                         break;
1156                                                 case ".":
1157                                                         doClick(document.getElementById("wmd-quote-button"));
1158                                                         break;
1159                                                 case "k":
1160                                                         doClick(document.getElementById("wmd-code-button"));
1161                                                         break;
1162                                                 case "g":
1163                                                         doClick(document.getElementById("wmd-image-button"));
1164                                                         break;
1165                                                 case "o":
1166                                                         doClick(document.getElementById("wmd-olist-button"));
1167                                                         break;
1168                                                 case "u":
1169                                                         doClick(document.getElementById("wmd-ulist-button"));
1170                                                         break;
1171                                                 case "h":
1172                                                         doClick(document.getElementById("wmd-heading-button"));
1173                                                         break;
1174                                                 case "r":
1175                                                         doClick(document.getElementById("wmd-hr-button"));
1176                                                         break;
1177                                                 case "y":
1178                                                         doClick(document.getElementById("wmd-redo-button"));
1179                                                         break;
1180                                                 case "z":
1181                                                         if(key.shiftKey) {
1182                                                                 doClick(document.getElementById("wmd-redo-button"));
1183                                                         }
1184                                                         else {
1185                                                                 doClick(document.getElementById("wmd-undo-button"));
1186                                                         }
1187                                                         break;
1188                                                 default:
1189                                                         return;
1190                                         }
1191                                         
1192
1193                                         if (key.preventDefault) {
1194                                                 key.preventDefault();
1195                                         }
1196                                         
1197                                         if (top.event) {
1198                                                 top.event.returnValue = false;
1199                                         }
1200                                 }
1201                         });
1202                         
1203                         // Auto-indent on shift-enter
1204                         util.addEvent(inputBox, "keyup", function(key){
1205                                 if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
1206                                         var keyCode = key.charCode || key.keyCode;
1207                                         // Character 13 is Enter
1208                                         if (keyCode === 13) {
1209                                                 fakeButton = {};
1210                                                 fakeButton.textOp = command.doAutoindent;
1211                                                 doClick(fakeButton);
1212                                         }
1213                                 }
1214                         });
1215                         
1216                         if (inputBox.form) {
1217                                 var submitCallback = inputBox.form.onsubmit;
1218                                 inputBox.form.onsubmit = function(){
1219                                         convertToHtml();
1220                                         if (submitCallback) {
1221                                                 return submitCallback.apply(this, arguments);
1222                                         }
1223                                 };
1224                         }
1225                 };
1226                 
1227                 // Convert the contents of the input textarea to HTML in the output/preview panels.
1228                 var convertToHtml = function(){
1229                 
1230                         if (wmd.showdown) {
1231                                 var markdownConverter = new wmd.showdown.converter();
1232                         }
1233                         var text = inputBox.value;
1234                         
1235                         var callback = function(){
1236                                 inputBox.value = text;
1237                         };
1238                         
1239                         if (!/markdown/.test(wmd.wmd_env.output.toLowerCase())) {
1240                                 if (markdownConverter) {
1241                                         inputBox.value = markdownConverter.makeHtml(text);
1242                                         top.setTimeout(callback, 0);
1243                                 }
1244                         }
1245                         return true;
1246                 };
1247                 
1248                 
1249                 this.undo = function(){
1250                         if (undoMgr) {
1251                                 undoMgr.undo();
1252                         }
1253                 };
1254                 
1255                 this.redo = function(){
1256                         if (undoMgr) {
1257                                 undoMgr.redo();
1258                         }
1259                 };
1260                 
1261                 // This is pretty useless.  The setupEditor function contents
1262                 // should just be copied here.
1263                 var init = function(){
1264                         setupEditor();
1265                 };
1266                 
1267                 this.destroy = function(){
1268                         if (undoMgr) {
1269                                 undoMgr.destroy();
1270                         }
1271                         if (div.parentNode) {
1272                                 div.parentNode.removeChild(div);
1273                         }
1274                         if (inputBox) {
1275                                 inputBox.style.marginTop = "";
1276                         }
1277                         top.clearInterval(creationHandle);
1278                 };
1279                 
1280                 init();
1281         };
1282         
1283         // The input textarea state/contents.
1284         // This is used to implement undo/redo by the undo manager.
1285         wmd.TextareaState = function(){
1286         
1287                 // Aliases
1288                 var stateObj = this;
1289                 var inputArea = wmd.panels.input;
1290                 
1291                 this.init = function() {
1292                 
1293                         if (!util.isVisible(inputArea)) {
1294                                 return;
1295                         }
1296                                 
1297                         this.setInputAreaSelectionStartEnd();
1298                         this.scrollTop = inputArea.scrollTop;
1299                         if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
1300                                 this.text = inputArea.value;
1301                         }
1302                         
1303                 };
1304                 
1305                 // Sets the selected text in the input box after we've performed an
1306                 // operation.
1307                 this.setInputAreaSelection = function(){
1308                 
1309                         if (!util.isVisible(inputArea)) {
1310                                 return;
1311                         }
1312                         
1313                         if (inputArea.selectionStart !== undefined && !global.isOpera) {
1314                         
1315                                 inputArea.focus();
1316                                 inputArea.selectionStart = stateObj.start;
1317                                 inputArea.selectionEnd = stateObj.end;
1318                                 inputArea.scrollTop = stateObj.scrollTop;
1319                         }
1320                         else if (doc.selection) {
1321                                 
1322                                 if (doc.activeElement && doc.activeElement !== inputArea) {
1323                                         return;
1324                                 }
1325                                         
1326                                 inputArea.focus();
1327                                 var range = inputArea.createTextRange();
1328                                 range.moveStart("character", -inputArea.value.length);
1329                                 range.moveEnd("character", -inputArea.value.length);
1330                                 range.moveEnd("character", stateObj.end);
1331                                 range.moveStart("character", stateObj.start);
1332                                 range.select();
1333                         }
1334                 };
1335                 
1336                 this.setInputAreaSelectionStartEnd = function(){
1337                 
1338                         if (inputArea.selectionStart || inputArea.selectionStart === 0) {
1339                         
1340                                 stateObj.start = inputArea.selectionStart;
1341                                 stateObj.end = inputArea.selectionEnd;
1342                         }
1343                         else if (doc.selection) {
1344                                 
1345                                 stateObj.text = util.fixEolChars(inputArea.value);
1346                                 
1347                                 // IE loses the selection in the textarea when buttons are
1348                                 // clicked.  On IE we cache the selection and set a flag
1349                                 // which we check for here.
1350                                 var range;
1351                                 if(wmd.ieRetardedClick && wmd.ieCachedRange) {
1352                                         range = wmd.ieCachedRange;
1353                                         wmd.ieRetardedClick = false;
1354                                 }
1355                                 else {
1356                                         range = doc.selection.createRange();
1357                                 }
1358
1359                                 var fixedRange = util.fixEolChars(range.text);
1360                                 var marker = "\x07";
1361                                 var markedRange = marker + fixedRange + marker;
1362                                 range.text = markedRange;
1363                                 var inputText = util.fixEolChars(inputArea.value);
1364                                         
1365                                 range.moveStart("character", -markedRange.length);
1366                                 range.text = fixedRange;
1367
1368                                 stateObj.start = inputText.indexOf(marker);
1369                                 stateObj.end = inputText.lastIndexOf(marker) - marker.length;
1370                                         
1371                                 var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
1372                                         
1373                                 if (len) {
1374                                         range.moveStart("character", -fixedRange.length);
1375                                         while (len--) {
1376                                                 fixedRange += "\n";
1377                                                 stateObj.end += 1;
1378                                         }
1379                                         range.text = fixedRange;
1380                                 }
1381                                         
1382                                 this.setInputAreaSelection();
1383                         }
1384                 };
1385                 
1386                 // Restore this state into the input area.
1387                 this.restore = function(){
1388                 
1389                         if (stateObj.text !== undefined && stateObj.text != inputArea.value) {
1390                                 inputArea.value = stateObj.text;
1391                         }
1392                         this.setInputAreaSelection();
1393                         inputArea.scrollTop = stateObj.scrollTop;
1394                 };
1395                 
1396                 // Gets a collection of HTML chunks from the inptut textarea.
1397                 this.getChunks = function(){
1398                 
1399                         var chunk = new wmd.Chunks();
1400                         
1401                         chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
1402                         chunk.startTag = "";
1403                         chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
1404                         chunk.endTag = "";
1405                         chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
1406                         chunk.scrollTop = stateObj.scrollTop;
1407                         
1408                         return chunk;
1409                 };
1410                 
1411                 // Sets the TextareaState properties given a chunk of markdown.
1412                 this.setChunks = function(chunk){
1413                 
1414                         chunk.before = chunk.before + chunk.startTag;
1415                         chunk.after = chunk.endTag + chunk.after;
1416                         
1417                         if (global.isOpera) {
1418                                 chunk.before = chunk.before.replace(/\n/g, "\r\n");
1419                                 chunk.selection = chunk.selection.replace(/\n/g, "\r\n");
1420                                 chunk.after = chunk.after.replace(/\n/g, "\r\n");
1421                         }
1422                         
1423                         this.start = chunk.before.length;
1424                         this.end = chunk.before.length + chunk.selection.length;
1425                         this.text = chunk.before + chunk.selection + chunk.after;
1426                         this.scrollTop = chunk.scrollTop;
1427                 };
1428
1429                 this.init();
1430         };
1431         
1432         // before: contains all the text in the input box BEFORE the selection.
1433         // after: contains all the text in the input box AFTER the selection.
1434         wmd.Chunks = function(){
1435         };
1436         
1437         // startRegex: a regular expression to find the start tag
1438         // endRegex: a regular expresssion to find the end tag
1439         wmd.Chunks.prototype.findTags = function(startRegex, endRegex){
1440         
1441                 var chunkObj = this;
1442                 var regex;
1443                 
1444                 if (startRegex) {
1445                         
1446                         regex = util.extendRegExp(startRegex, "", "$");
1447                         
1448                         this.before = this.before.replace(regex, 
1449                                 function(match){
1450                                         chunkObj.startTag = chunkObj.startTag + match;
1451                                         return "";
1452                                 });
1453                         
1454                         regex = util.extendRegExp(startRegex, "^", "");
1455                         
1456                         this.selection = this.selection.replace(regex, 
1457                                 function(match){
1458                                         chunkObj.startTag = chunkObj.startTag + match;
1459                                         return "";
1460                                 });
1461                 }
1462                 
1463                 if (endRegex) {
1464                         
1465                         regex = util.extendRegExp(endRegex, "", "$");
1466                         
1467                         this.selection = this.selection.replace(regex,
1468                                 function(match){
1469                                         chunkObj.endTag = match + chunkObj.endTag;
1470                                         return "";
1471                                 });
1472
1473                         regex = util.extendRegExp(endRegex, "^", "");
1474                         
1475                         this.after = this.after.replace(regex,
1476                                 function(match){
1477                                         chunkObj.endTag = match + chunkObj.endTag;
1478                                         return "";
1479                                 });
1480                 }
1481         };
1482         
1483         // If remove is false, the whitespace is transferred
1484         // to the before/after regions.
1485         //
1486         // If remove is true, the whitespace disappears.
1487         wmd.Chunks.prototype.trimWhitespace = function(remove){
1488         
1489                 this.selection = this.selection.replace(/^(\s*)/, "");
1490                 
1491                 if (!remove) {
1492                         this.before += re.$1;
1493                 }
1494                 
1495                 this.selection = this.selection.replace(/(\s*)$/, "");
1496                 
1497                 if (!remove) {
1498                         this.after = re.$1 + this.after;
1499                 }
1500         };
1501         
1502         
1503         wmd.Chunks.prototype.skipLines = function(nLinesBefore, nLinesAfter, findExtraNewlines){
1504         
1505                 if (nLinesBefore === undefined) {
1506                         nLinesBefore = 1;
1507                 }
1508                 
1509                 if (nLinesAfter === undefined) {
1510                         nLinesAfter = 1;
1511                 }
1512                 
1513                 nLinesBefore++;
1514                 nLinesAfter++;
1515                 
1516                 var regexText;
1517                 var replacementText;
1518                 
1519                 this.selection = this.selection.replace(/(^\n*)/, "");
1520                 this.startTag = this.startTag + re.$1;
1521                 this.selection = this.selection.replace(/(\n*$)/, "");
1522                 this.endTag = this.endTag + re.$1;
1523                 this.startTag = this.startTag.replace(/(^\n*)/, "");
1524                 this.before = this.before + re.$1;
1525                 this.endTag = this.endTag.replace(/(\n*$)/, "");
1526                 this.after = this.after + re.$1;
1527                 
1528                 if (this.before) {
1529                 
1530                         regexText = replacementText = "";
1531                         
1532                         while (nLinesBefore--) {
1533                                 regexText += "\\n?";
1534                                 replacementText += "\n";
1535                         }
1536                         
1537                         if (findExtraNewlines) {
1538                                 regexText = "\\n*";
1539                         }
1540                         this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
1541                 }
1542                 
1543                 if (this.after) {
1544                 
1545                         regexText = replacementText = "";
1546                         
1547                         while (nLinesAfter--) {
1548                                 regexText += "\\n?";
1549                                 replacementText += "\n";
1550                         }
1551                         if (findExtraNewlines) {
1552                                 regexText = "\\n*";
1553                         }
1554                         
1555                         this.after = this.after.replace(new re(regexText, ""), replacementText);
1556                 }
1557         };
1558         
1559         // The markdown symbols - 4 spaces = code, > = blockquote, etc.
1560         command.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
1561         
1562         // Remove markdown symbols from the chunk selection.
1563         command.unwrap = function(chunk){
1564                 var txt = new re("([^\\n])\\n(?!(\\n|" + command.prefixes + "))", "g");
1565                 chunk.selection = chunk.selection.replace(txt, "$1 $2");
1566         };
1567         
1568         command.wrap = function(chunk, len){
1569                 command.unwrap(chunk);
1570                 var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm");
1571                 
1572                 chunk.selection = chunk.selection.replace(regex, function(line, marked){
1573                         if (new re("^" + command.prefixes, "").test(line)) {
1574                                 return line;
1575                         }
1576                         return marked + "\n";
1577                 });
1578                 
1579                 chunk.selection = chunk.selection.replace(/\s+$/, "");
1580         };
1581         
1582         command.doBold = function(chunk, postProcessing){
1583                 return command.doBorI(chunk, postProcessing, 2, "strong text");
1584         };
1585         
1586         command.doItalic = function(chunk, postProcessing){
1587                 return command.doBorI(chunk, postProcessing, 1, "emphasized text");
1588         };
1589         
1590         // chunk: The selected region that will be enclosed with */**
1591         // nStars: 1 for italics, 2 for bold
1592         // insertText: If you just click the button without highlighting text, this gets inserted
1593         command.doBorI = function(chunk, postProcessing, nStars, insertText){
1594         
1595                 // Get rid of whitespace and fixup newlines.
1596                 chunk.trimWhitespace();
1597                 chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
1598                 
1599                 // Look for stars before and after.  Is the chunk already marked up?
1600                 chunk.before.search(/(\**$)/);
1601                 var starsBefore = re.$1;
1602                 
1603                 chunk.after.search(/(^\**)/);
1604                 var starsAfter = re.$1;
1605                 
1606                 var prevStars = Math.min(starsBefore.length, starsAfter.length);
1607                 
1608                 // Remove stars if we have to since the button acts as a toggle.
1609                 if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
1610                         chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
1611                         chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
1612                 }
1613                 else if (!chunk.selection && starsAfter) {
1614                         // It's not really clear why this code is necessary.  It just moves
1615                         // some arbitrary stuff around.
1616                         chunk.after = chunk.after.replace(/^([*_]*)/, "");
1617                         chunk.before = chunk.before.replace(/(\s?)$/, "");
1618                         var whitespace = re.$1;
1619                         chunk.before = chunk.before + starsAfter + whitespace;
1620                 }
1621                 else {
1622                 
1623                         // In most cases, if you don't have any selected text and click the button
1624                         // you'll get a selected, marked up region with the default text inserted.
1625                         if (!chunk.selection && !starsAfter) {
1626                                 chunk.selection = insertText;
1627                         }
1628                         
1629                         // Add the true markup.
1630                         var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
1631                         chunk.before = chunk.before + markup;
1632                         chunk.after = markup + chunk.after;
1633                 }
1634                 
1635                 return;
1636         };
1637         
1638         command.stripLinkDefs = function(text, defsToAdd){
1639         
1640                 text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, 
1641                         function(totalMatch, id, link, newlines, title){        
1642                                 defsToAdd[id] = totalMatch.replace(/\s*$/, "");
1643                                 if (newlines) {
1644                                         // Strip the title and return that separately.
1645                                         defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
1646                                         return newlines + title;
1647                                 }
1648                                 return "";
1649                         });
1650                 
1651                 return text;
1652         };
1653         
1654         command.addLinkDef = function(chunk, linkDef){
1655         
1656                 var refNumber = 0; // The current reference number
1657                 var defsToAdd = {}; //
1658                 // Start with a clean slate by removing all previous link definitions.
1659                 chunk.before = command.stripLinkDefs(chunk.before, defsToAdd);
1660                 chunk.selection = command.stripLinkDefs(chunk.selection, defsToAdd);
1661                 chunk.after = command.stripLinkDefs(chunk.after, defsToAdd);
1662                 
1663                 var defs = "";
1664                 var regex = /(\[(?:\[[^\]]*\]|[^\[\]])*\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
1665                 
1666                 var addDefNumber = function(def){
1667                         refNumber++;
1668                         def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, "  [" + refNumber + "]:");
1669                         defs += "\n" + def;
1670                 };
1671                 
1672                 var getLink = function(wholeMatch, link, id, end){
1673                 
1674                         if (defsToAdd[id]) {
1675                                 addDefNumber(defsToAdd[id]);
1676                                 return link + refNumber + end;
1677                                 
1678                         }
1679                         return wholeMatch;
1680                 };
1681                 
1682                 chunk.before = chunk.before.replace(regex, getLink);
1683                 
1684                 if (linkDef) {
1685                         addDefNumber(linkDef);
1686                 }
1687                 else {
1688                         chunk.selection = chunk.selection.replace(regex, getLink);
1689                 }
1690                 
1691                 var refOut = refNumber;
1692                 
1693                 chunk.after = chunk.after.replace(regex, getLink);
1694                 
1695                 if (chunk.after) {
1696                         chunk.after = chunk.after.replace(/\n*$/, "");
1697                 }
1698                 if (!chunk.after) {
1699                         chunk.selection = chunk.selection.replace(/\n*$/, "");
1700                 }
1701                 
1702                 chunk.after += "\n\n" + defs;
1703                 
1704                 return refOut;
1705         };
1706         
1707         command.doLinkOrImage = function(chunk, postProcessing, isImage){
1708         
1709                 chunk.trimWhitespace();
1710                 chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
1711                 
1712                 if (chunk.endTag.length > 1) {
1713                 
1714                         chunk.startTag = chunk.startTag.replace(/!?\[/, "");
1715                         chunk.endTag = "";
1716                         command.addLinkDef(chunk, null);
1717                         
1718                 }
1719                 else {
1720                 
1721                         if (/\n\n/.test(chunk.selection)) {
1722                                 command.addLinkDef(chunk, null);
1723                                 return;
1724                         }
1725                         
1726                         // The function to be executed when you enter a link and press OK or Cancel.
1727                         // Marks up the link and adds the ref.
1728                         var makeLinkMarkdown = function(link){
1729                         
1730                                 if (link !== null) {
1731                                 
1732                                         chunk.startTag = chunk.endTag = "";
1733                                         var linkDef = " [999]: " + link;
1734                                         
1735                                         var num = command.addLinkDef(chunk, linkDef);
1736                                         chunk.startTag = isImage ? "![" : "[";
1737                                         chunk.endTag = "][" + num + "]";
1738                                         
1739                                         if (!chunk.selection) {
1740                                                 if (isImage) {
1741                                                         chunk.selection = "alt text";
1742                                                 }
1743                                                 else {
1744                                                         chunk.selection = "link text";
1745                                                 }
1746                                         }
1747                                 }
1748                                 postProcessing();
1749                         };
1750                         
1751                         if (isImage) {
1752                 // add forth param to identify image window
1753                                 util.prompt(imageDialogText, imageDefaultText, makeLinkMarkdown, 1);
1754                         }
1755                         else {
1756                                 util.prompt(linkDialogText, linkDefaultText, makeLinkMarkdown);
1757                         }
1758                         return true;
1759                 }
1760         };
1761         
1762         util.makeAPI = function(){
1763                 wmd.wmd = {};
1764                 wmd.wmd.editor = wmd.editor;
1765                 wmd.wmd.previewManager = wmd.previewManager;
1766         };
1767         
1768         util.startEditor = function(){
1769         
1770                 if (wmd.wmd_env.autostart === false) {
1771                         util.makeAPI();
1772                         return;
1773                 }
1774
1775                 var edit;               // The editor (buttons + input + outputs) - the main object.
1776                 var previewMgr; // The preview manager.
1777                 
1778                 // Fired after the page has fully loaded.
1779                 var loadListener = function(){
1780                 
1781                         wmd.panels = new wmd.PanelCollection();
1782                         
1783                         previewMgr = new wmd.previewManager();
1784                         var previewRefreshCallback = previewMgr.refresh;
1785                                                 
1786                         edit = new wmd.editor(previewRefreshCallback);
1787                         
1788                         previewMgr.refresh(true);
1789                         
1790                 };
1791                 
1792                 util.addEvent(top, "load", loadListener);
1793         };
1794         
1795         wmd.previewManager = function(){
1796                 
1797                 var managerObj = this;
1798                 var converter;
1799                 var poller;
1800                 var timeout;
1801                 var elapsedTime;
1802                 var oldInputText;
1803                 var htmlOut;
1804                 var maxDelay = 3000;
1805                 var startType = "delayed"; // The other legal value is "manual"
1806                 
1807                 // Adds event listeners to elements and creates the input poller.
1808                 var setupEvents = function(inputElem, listener){
1809                 
1810                         util.addEvent(inputElem, "input", listener);
1811                         inputElem.onpaste = listener;
1812                         inputElem.ondrop = listener;
1813                         
1814                         util.addEvent(inputElem, "keypress", listener);
1815                         util.addEvent(inputElem, "keydown", listener);
1816                         // previewPollInterval is set at the top of this file.
1817                         poller = new wmd.inputPoller(listener, previewPollInterval);
1818                 };
1819                 
1820                 var getDocScrollTop = function(){
1821                 
1822                         var result = 0;
1823                         
1824                         if (top.innerHeight) {
1825                                 result = top.pageYOffset;
1826                         }
1827                         else 
1828                                 if (doc.documentElement && doc.documentElement.scrollTop) {
1829                                         result = doc.documentElement.scrollTop;
1830                                 }
1831                                 else 
1832                                         if (doc.body) {
1833                                                 result = doc.body.scrollTop;
1834                                         }
1835                         
1836                         return result;
1837                 };
1838                 
1839                 var makePreviewHtml = function(){
1840                 
1841                         // If there are no registered preview and output panels
1842                         // there is nothing to do.
1843                         if (!wmd.panels.preview && !wmd.panels.output) {
1844                                 return;
1845                         }
1846                         
1847                         var text = wmd.panels.input.value;
1848                         if (text && text == oldInputText) {
1849                                 return; // Input text hasn't changed.
1850                         }
1851                         else {
1852                                 oldInputText = text;
1853                         }
1854                         
1855                         var prevTime = new Date().getTime();
1856                         
1857                         if (!converter && wmd.showdown) {
1858                                 converter = new wmd.showdown.converter();
1859                         }
1860                         
1861                         if (converter) {
1862                                 text = converter.makeHtml(text);
1863                         }
1864                         
1865                         // Calculate the processing time of the HTML creation.
1866                         // It's used as the delay time in the event listener.
1867                         var currTime = new Date().getTime();
1868                         elapsedTime = currTime - prevTime;
1869                         
1870                         pushPreviewHtml(text);
1871                         htmlOut = text;
1872                 };
1873                 
1874                 // setTimeout is already used.  Used as an event listener.
1875                 var applyTimeout = function(){
1876                 
1877                         if (timeout) {
1878                                 top.clearTimeout(timeout);
1879                                 timeout = undefined;
1880                         }
1881                         
1882                         if (startType !== "manual") {
1883                         
1884                                 var delay = 0;
1885                                 
1886                                 if (startType === "delayed") {
1887                                         delay = elapsedTime;
1888                                 }
1889                                 
1890                                 if (delay > maxDelay) {
1891                                         delay = maxDelay;
1892                                 }
1893                                 timeout = top.setTimeout(makePreviewHtml, delay);
1894                         }
1895                 };
1896                 
1897                 var getScaleFactor = function(panel){
1898                         if (panel.scrollHeight <= panel.clientHeight) {
1899                                 return 1;
1900                         }
1901                         return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
1902                 };
1903                 
1904                 var setPanelScrollTops = function(){
1905                 
1906                         if (wmd.panels.preview) {
1907                                 wmd.panels.preview.scrollTop = (wmd.panels.preview.scrollHeight - wmd.panels.preview.clientHeight) * getScaleFactor(wmd.panels.preview);
1908                         }
1909                         
1910                         if (wmd.panels.output) {
1911                                 wmd.panels.output.scrollTop = (wmd.panels.output.scrollHeight - wmd.panels.output.clientHeight) * getScaleFactor(wmd.panels.output);
1912                         }
1913                 };
1914                 
1915                 this.refresh = function(requiresRefresh){
1916                 
1917                         if (requiresRefresh) {
1918                                 oldInputText = "";
1919                                 makePreviewHtml();
1920                         }
1921                         else {
1922                                 applyTimeout();
1923                         }
1924                 };
1925                 
1926                 this.processingTime = function(){
1927                         return elapsedTime;
1928                 };
1929                 
1930                 // The output HTML
1931                 this.output = function(){
1932                         return htmlOut;
1933                 };
1934                 
1935                 // The mode can be "manual" or "delayed"
1936                 this.setUpdateMode = function(mode){
1937                         startType = mode;
1938                         managerObj.refresh();
1939                 };
1940                 
1941                 var isFirstTimeFilled = true;
1942                 
1943                 var pushPreviewHtml = function(text){
1944                 
1945                         var emptyTop = position.getTop(wmd.panels.input) - getDocScrollTop();
1946                         
1947                         // Send the encoded HTML to the output textarea/div.
1948                         if (wmd.panels.output) {
1949                                 // The value property is only defined if the output is a textarea.
1950                                 if (wmd.panels.output.value !== undefined) {
1951                                         wmd.panels.output.value = text;
1952                                         wmd.panels.output.readOnly = true;
1953                                 }
1954                                 // Otherwise we are just replacing the text in a div.
1955                                 // Send the HTML wrapped in <pre><code>
1956                                 else {
1957                                         var newText = text.replace(/&/g, "&amp;");
1958                                         newText = newText.replace(/</g, "&lt;");
1959                                         wmd.panels.output.innerHTML = "<pre><code>" + newText + "</code></pre>";
1960                                 }
1961                         }
1962                         
1963                         if (wmd.panels.preview) {
1964                                 wmd.panels.preview.innerHTML = text;
1965                         }
1966                         
1967                         setPanelScrollTops();
1968                         
1969                         if (isFirstTimeFilled) {
1970                                 isFirstTimeFilled = false;
1971                                 return;
1972                         }
1973                         
1974                         var fullTop = position.getTop(wmd.panels.input) - getDocScrollTop();
1975                         
1976                         if (global.isIE) {
1977                                 top.setTimeout(function(){
1978                                         top.scrollBy(0, fullTop - emptyTop);
1979                                 }, 0);
1980                         }
1981                         else {
1982                                 top.scrollBy(0, fullTop - emptyTop);
1983                         }
1984                 };
1985                 
1986                 var init = function(){
1987                 
1988                         setupEvents(wmd.panels.input, applyTimeout);
1989                         makePreviewHtml();
1990                         
1991                         if (wmd.panels.preview) {
1992                                 wmd.panels.preview.scrollTop = 0;
1993                         }
1994                         if (wmd.panels.output) {
1995                                 wmd.panels.output.scrollTop = 0;
1996                         }
1997                 };
1998                 
1999                 this.destroy = function(){
2000                         if (poller) {
2001                                 poller.destroy();
2002                         }
2003                 };
2004                 
2005                 init();
2006         };
2007
2008         // When making a list, hitting shift-enter will put your cursor on the next line
2009         // at the current indent level.
2010         command.doAutoindent = function(chunk, postProcessing){
2011                 
2012                 chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
2013                 chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
2014                 chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
2015                 
2016                 if(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)){
2017                         if(command.doList){
2018                                 command.doList(chunk);
2019                         }
2020                 }
2021                 if(/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)){
2022                         if(command.doBlockquote){
2023                                 command.doBlockquote(chunk);
2024                         }
2025                 }
2026                 if(/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)){
2027                         if(command.doCode){
2028                                 command.doCode(chunk);
2029                         }
2030                 }
2031         };
2032         
2033         command.doBlockquote = function(chunk, postProcessing){
2034                 
2035                 chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
2036                         function(totalMatch, newlinesBefore, text, newlinesAfter){
2037                                 chunk.before += newlinesBefore;
2038                                 chunk.after = newlinesAfter + chunk.after;
2039                                 return text;
2040                         });
2041                         
2042                 chunk.before = chunk.before.replace(/(>[ \t]*)$/,
2043                         function(totalMatch, blankLine){
2044                                 chunk.selection = blankLine + chunk.selection;
2045                                 return "";
2046                         });
2047                         
2048                 chunk.selection = chunk.selection.replace(/^(\s|>)+$/ ,"");
2049                 chunk.selection = chunk.selection || "Blockquote";
2050                 
2051                 if(chunk.before){
2052                         chunk.before = chunk.before.replace(/\n?$/,"\n");
2053                 }
2054                 if(chunk.after){
2055                         chunk.after = chunk.after.replace(/^\n?/,"\n");
2056                 }
2057                 
2058                 chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
2059                         function(totalMatch){
2060                                 chunk.startTag = totalMatch;
2061                                 return "";
2062                         });
2063                         
2064                 chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
2065                         function(totalMatch){
2066                                 chunk.endTag = totalMatch;
2067                                 return "";
2068                         });
2069                 
2070                 var replaceBlanksInTags = function(useBracket){
2071                         
2072                         var replacement = useBracket ? "> " : "";
2073                         
2074                         if(chunk.startTag){
2075                                 chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
2076                                         function(totalMatch, markdown){
2077                                                 return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
2078                                         });
2079                         }
2080                         if(chunk.endTag){
2081                                 chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
2082                                         function(totalMatch, markdown){
2083                                                 return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
2084                                         });
2085                         }
2086                 };
2087                 
2088                 if(/^(?![ ]{0,3}>)/m.test(chunk.selection)){
2089                         command.wrap(chunk, wmd.wmd_env.lineLength - 2);
2090                         chunk.selection = chunk.selection.replace(/^/gm, "> ");
2091                         replaceBlanksInTags(true);
2092                         chunk.skipLines();
2093                 }
2094                 else{
2095                         chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
2096                         command.unwrap(chunk);
2097                         replaceBlanksInTags(false);
2098                         
2099                         if(!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag){
2100                                 chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
2101                         }
2102                         
2103                         if(!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag){
2104                                 chunk.endTag=chunk.endTag.replace(/^\n{0,2}/, "\n\n");
2105                         }
2106                 }
2107                 
2108                 if(!/\n/.test(chunk.selection)){
2109                         chunk.selection = chunk.selection.replace(/^(> *)/,
2110                         function(wholeMatch, blanks){
2111                                 chunk.startTag += blanks;
2112                                 return "";
2113                         });
2114                 }
2115         };
2116
2117         command.doCode = function(chunk, postProcessing){
2118                 
2119                 var hasTextBefore = /\S[ ]*$/.test(chunk.before);
2120                 var hasTextAfter = /^[ ]*\S/.test(chunk.after);
2121                 
2122                 // Use 'four space' markdown if the selection is on its own
2123                 // line or is multiline.
2124                 if((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)){
2125                         
2126                         chunk.before = chunk.before.replace(/[ ]{4}$/,
2127                                 function(totalMatch){
2128                                         chunk.selection = totalMatch + chunk.selection;
2129                                         return "";
2130                                 });
2131                                 
2132                         var nLinesBack = 1;
2133                         var nLinesForward = 1;
2134                         
2135                         if(/\n(\t|[ ]{4,}).*\n$/.test(chunk.before)){
2136                                 nLinesBack = 0;
2137                         }
2138                         if(/^\n(\t|[ ]{4,})/.test(chunk.after)){
2139                                 nLinesForward = 0;
2140                         }
2141                         
2142                         chunk.skipLines(nLinesBack, nLinesForward);
2143                         
2144                         if(!chunk.selection){
2145                                 chunk.startTag = "    ";
2146                                 chunk.selection = "enter code here";
2147                         }
2148                         else {
2149                                 if(/^[ ]{0,3}\S/m.test(chunk.selection)){
2150                                         chunk.selection = chunk.selection.replace(/^/gm, "    ");
2151                                 }
2152                                 else{
2153                                         chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
2154                                 }
2155                         }
2156                 }
2157                 else{
2158                         // Use backticks (`) to delimit the code block.
2159                         
2160                         chunk.trimWhitespace();
2161                         chunk.findTags(/`/, /`/);
2162                         
2163                         if(!chunk.startTag && !chunk.endTag){
2164                                 chunk.startTag = chunk.endTag="`";
2165                                 if(!chunk.selection){
2166                                         chunk.selection = "enter code here";
2167                                 }
2168                         }
2169                         else if(chunk.endTag && !chunk.startTag){
2170                                 chunk.before += chunk.endTag;
2171                                 chunk.endTag = "";
2172                         }
2173                         else{
2174                                 chunk.startTag = chunk.endTag="";
2175                         }
2176                 }
2177         };
2178         
2179         command.doList = function(chunk, postProcessing, isNumberedList){
2180                                 
2181                 // These are identical except at the very beginning and end.
2182                 // Should probably use the regex extension function to make this clearer.
2183                 var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
2184                 var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
2185                 
2186                 // The default bullet is a dash but others are possible.
2187                 // This has nothing to do with the particular HTML bullet,
2188                 // it's just a markdown bullet.
2189                 var bullet = "-";
2190                 
2191                 // The number in a numbered list.
2192                 var num = 1;
2193                 
2194                 // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
2195                 var getItemPrefix = function(){
2196                         var prefix;
2197                         if(isNumberedList){
2198                                 prefix = " " + num + ". ";
2199                                 num++;
2200                         }
2201                         else{
2202                                 prefix = " " + bullet + " ";
2203                         }
2204                         return prefix;
2205                 };
2206                 
2207                 // Fixes the prefixes of the other list items.
2208                 var getPrefixedItem = function(itemText){
2209                 
2210                         // The numbering flag is unset when called by autoindent.
2211                         if(isNumberedList === undefined){
2212                                 isNumberedList = /^\s*\d/.test(itemText);
2213                         }
2214                         
2215                         // Renumber/bullet the list element.
2216                         itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
2217                                 function( _ ){
2218                                         return getItemPrefix();
2219                                 });
2220                                 
2221                         return itemText;
2222                 };
2223                 
2224                 chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
2225                 
2226                 if(chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)){
2227                         chunk.before += chunk.startTag;
2228                         chunk.startTag = "";
2229                 }
2230                 
2231                 if(chunk.startTag){
2232                         
2233                         var hasDigits = /\d+[.]/.test(chunk.startTag);
2234                         chunk.startTag = "";
2235                         chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
2236                         command.unwrap(chunk);
2237                         chunk.skipLines();
2238                         
2239                         if(hasDigits){
2240                                 // Have to renumber the bullet points if this is a numbered list.
2241                                 chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
2242                         }
2243                         if(isNumberedList == hasDigits){
2244                                 return;
2245                         }
2246                 }
2247                 
2248                 var nLinesUp = 1;
2249                 
2250                 chunk.before = chunk.before.replace(previousItemsRegex,
2251                         function(itemText){
2252                                 if(/^\s*([*+-])/.test(itemText)){
2253                                         bullet = re.$1;
2254                                 }
2255                                 nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2256                                 return getPrefixedItem(itemText);
2257                         });
2258                         
2259                 if(!chunk.selection){
2260                         chunk.selection = "List item";
2261                 }
2262                 
2263                 var prefix = getItemPrefix();
2264                 
2265                 var nLinesDown = 1;
2266                 
2267                 chunk.after = chunk.after.replace(nextItemsRegex,
2268                         function(itemText){
2269                                 nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2270                                 return getPrefixedItem(itemText);
2271                         });
2272                         
2273                 chunk.trimWhitespace(true);
2274                 chunk.skipLines(nLinesUp, nLinesDown, true);
2275                 chunk.startTag = prefix;
2276                 var spaces = prefix.replace(/./g, " ");
2277                 command.wrap(chunk, wmd.wmd_env.lineLength - spaces.length);
2278                 chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
2279                 
2280         };
2281         
2282         command.doHeading = function(chunk, postProcessing){
2283                 
2284                 // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
2285                 chunk.selection = chunk.selection.replace(/\s+/g, " ");
2286                 chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
2287                 
2288                 // If we clicked the button with no selected text, we just
2289                 // make a level 2 hash header around some default text.
2290                 if(!chunk.selection){
2291                         chunk.startTag = "## ";
2292                         chunk.selection = "Heading";
2293                         chunk.endTag = " ##";
2294                         return;
2295                 }
2296                 
2297                 var headerLevel = 0;            // The existing header level of the selected text.
2298                 
2299                 // Remove any existing hash heading markdown and save the header level.
2300                 chunk.findTags(/#+[ ]*/, /[ ]*#+/);
2301                 if(/#+/.test(chunk.startTag)){
2302                         headerLevel = re.lastMatch.length;
2303                 }
2304                 chunk.startTag = chunk.endTag = "";
2305                 
2306                 // Try to get the current header level by looking for - and = in the line
2307                 // below the selection.
2308                 chunk.findTags(null, /\s?(-+|=+)/);
2309                 if(/=+/.test(chunk.endTag)){
2310                         headerLevel = 1;
2311                 }
2312                 if(/-+/.test(chunk.endTag)){
2313                         headerLevel = 2;
2314                 }
2315                 
2316                 // Skip to the next line so we can create the header markdown.
2317                 chunk.startTag = chunk.endTag = "";
2318                 chunk.skipLines(1, 1);
2319
2320                 // We make a level 2 header if there is no current header.
2321                 // If there is a header level, we substract one from the header level.
2322                 // If it's already a level 1 header, it's removed.
2323                 var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
2324                 
2325                 if(headerLevelToCreate > 0){
2326                         
2327                         // The button only creates level 1 and 2 underline headers.
2328                         // Why not have it iterate over hash header levels?  Wouldn't that be easier and cleaner?
2329                         var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
2330                         var len = chunk.selection.length;
2331                         if(len > wmd.wmd_env.lineLength){
2332                                 len = wmd.wmd_env.lineLength;
2333                         }
2334                         chunk.endTag = "\n";
2335                         while(len--){
2336                                 chunk.endTag += headerChar;
2337                         }
2338                 }
2339         };      
2340         
2341         command.doHorizontalRule = function(chunk, postProcessing){
2342                 chunk.startTag = "----------\n";
2343                 chunk.selection = "";
2344                 chunk.skipLines(2, 1, true);
2345         }
2346 };
2347
2348
2349 Attacklab.wmd_env = {};
2350 Attacklab.account_options = {};
2351 Attacklab.wmd_defaults = {version:1, output:"Markdown", lineLength:40, delayLoad:false};
2352
2353 if(!Attacklab.wmd)
2354 {
2355         Attacklab.wmd = function()
2356         {
2357                 Attacklab.loadEnv = function()
2358                 {
2359                         var mergeEnv = function(env)
2360                         {
2361                                 if(!env)
2362                                 {
2363                                         return;
2364                                 }
2365                         
2366                                 for(var key in env)
2367                                 {
2368                                         Attacklab.wmd_env[key] = env[key];
2369                                 }
2370                         };
2371                         
2372                         mergeEnv(Attacklab.wmd_defaults);
2373                         mergeEnv(Attacklab.account_options);
2374                         mergeEnv(top["wmd_options"]);
2375                         Attacklab.full = true;
2376                         
2377                         var defaultButtons = "bold italic link blockquote code image ol ul heading hr";
2378                         Attacklab.wmd_env.buttons = Attacklab.wmd_env.buttons || defaultButtons;
2379                 };
2380                 Attacklab.loadEnv();
2381
2382         };
2383         
2384         Attacklab.wmd();
2385         Attacklab.wmdBase();
2386         Attacklab.Util.startEditor();
2387 };
2388