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