]> git.openstreetmap.org Git - osqa.git/blob - forum/skins/default/media/js/wmd/wmd.js
fixed up some english and changed markdown default editor to show markdown instead...
[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                                         // Bugfix for messed up DEL and .
1101                                         if (keyCode === 46) {
1102                                                 keyCodeStr = "";
1103                                         }
1104                                         if (keyCode === 190) {
1105                                                 keyCodeStr = ".";
1106                                         }
1107
1108                                         switch(keyCodeStr) {
1109                                                 case "b":
1110                                                         doClick(document.getElementById("wmd-bold-button"));
1111                                                         break;
1112                                                 case "i":
1113                                                         doClick(document.getElementById("wmd-italic-button"));
1114                                                         break;
1115                                                 case "l":
1116                                                         doClick(document.getElementById("wmd-link-button"));
1117                                                         break;
1118                                                 case "q":
1119                                                         doClick(document.getElementById("wmd-quote-button"));
1120                                                         break;
1121                                                 case "k":
1122                                                         doClick(document.getElementById("wmd-code-button"));
1123                                                         break;
1124                                                 case "g":
1125                                                         doClick(document.getElementById("wmd-image-button"));
1126                                                         break;
1127                                                 case "o":
1128                                                         doClick(document.getElementById("wmd-olist-button"));
1129                                                         break;
1130                                                 case "u":
1131                                                         doClick(document.getElementById("wmd-ulist-button"));
1132                                                         break;
1133                                                 case "h":
1134                                                         doClick(document.getElementById("wmd-heading-button"));
1135                                                         break;
1136                                                 case "r":
1137                                                         doClick(document.getElementById("wmd-hr-button"));
1138                                                         break;
1139                                                 case "y":
1140                                                         doClick(document.getElementById("wmd-redo-button"));
1141                                                         break;
1142                                                 case "z":
1143                                                         if(key.shiftKey) {
1144                                                                 doClick(document.getElementById("wmd-redo-button"));
1145                                                         }
1146                                                         else {
1147                                                                 doClick(document.getElementById("wmd-undo-button"));
1148                                                         }
1149                                                         break;
1150                                                 default:
1151                                                         return;
1152                                         }
1153                                         
1154
1155                                         if (key.preventDefault) {
1156                                                 key.preventDefault();
1157                                         }
1158                                         
1159                                         if (top.event) {
1160                                                 top.event.returnValue = false;
1161                                         }
1162                                 }
1163                         });
1164                         
1165                         // Auto-continue lists, code blocks and block quotes when
1166                         // the enter key is pressed.
1167                         util.addEvent(inputBox, "keyup", function(key){
1168                                 if (!key.shiftKey && !key.ctrlKey && !key.metaKey) {
1169                                         var keyCode = key.charCode || key.keyCode;
1170                                         // Key code 13 is Enter
1171                                         if (keyCode === 13) {
1172                                                 fakeButton = {};
1173                                                 fakeButton.textOp = command.doAutoindent;
1174                                                 doClick(fakeButton);
1175                                         }
1176                                 }
1177                         });
1178                         
1179                         // Disable ESC clearing the input textarea on IE
1180                         if (global.isIE) {
1181                                 util.addEvent(inputBox, "keydown", function(key){
1182                                         var code = key.keyCode;
1183                                         // Key code 27 is ESC
1184                                         if (code === 27) {
1185                                                 return false;
1186                                         }
1187                                 });
1188                         }
1189                         
1190                         if (inputBox.form) {
1191                                 var submitCallback = inputBox.form.onsubmit;
1192                                 inputBox.form.onsubmit = function(){
1193                                         convertToHtml();
1194                                         if (submitCallback) {
1195                                                 return submitCallback.apply(this, arguments);
1196                                         }
1197                                 };
1198                         }
1199                 };
1200                 
1201                 // Convert the contents of the input textarea to HTML in the output/preview panels.
1202                 var convertToHtml = function(){
1203                 
1204                         if (wmd.showdown) {
1205                                 var markdownConverter = new wmd.showdown.converter();
1206                         }
1207                         var text = inputBox.value;
1208                         
1209                         var callback = function(){
1210                                 inputBox.value = text;
1211                         };
1212                         
1213                         if (!/markdown/.test(wmd.wmd_env.output.toLowerCase())) {
1214                                 if (markdownConverter) {
1215                                         inputBox.value = markdownConverter.makeHtml(text);
1216                                         top.setTimeout(callback, 0);
1217                                 }
1218                         }
1219                         return true;
1220                 };
1221                 
1222                 
1223                 this.undo = function(){
1224                         if (undoMgr) {
1225                                 undoMgr.undo();
1226                         }
1227                 };
1228                 
1229                 this.redo = function(){
1230                         if (undoMgr) {
1231                                 undoMgr.redo();
1232                         }
1233                 };
1234                 
1235                 // This is pretty useless.  The setupEditor function contents
1236                 // should just be copied here.
1237                 var init = function(){
1238                         setupEditor();
1239                 };
1240                 
1241                 this.destroy = function(){
1242                         if (undoMgr) {
1243                                 undoMgr.destroy();
1244                         }
1245                         if (div.parentNode) {
1246                                 div.parentNode.removeChild(div);
1247                         }
1248                         if (inputBox) {
1249                                 inputBox.style.marginTop = "";
1250                         }
1251                         top.clearInterval(creationHandle);
1252                 };
1253                 
1254                 init();
1255         };
1256         
1257         // The input textarea state/contents.
1258         // This is used to implement undo/redo by the undo manager.
1259         wmd.TextareaState = function(){
1260         
1261                 // Aliases
1262                 var stateObj = this;
1263                 var inputArea = wmd.panels.input;
1264                 
1265                 this.init = function() {
1266                 
1267                         if (!util.isVisible(inputArea)) {
1268                                 return;
1269                         }
1270                                 
1271                         this.setInputAreaSelectionStartEnd();
1272                         this.scrollTop = inputArea.scrollTop;
1273                         if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
1274                                 this.text = inputArea.value;
1275                         }
1276                         
1277                 };
1278                 
1279                 // Sets the selected text in the input box after we've performed an
1280                 // operation.
1281                 this.setInputAreaSelection = function(){
1282                 
1283                         if (!util.isVisible(inputArea)) {
1284                                 return;
1285                         }
1286                         
1287                         if (inputArea.selectionStart !== undefined && !global.isOpera) {
1288                         
1289                                 inputArea.focus();
1290                                 inputArea.selectionStart = stateObj.start;
1291                                 inputArea.selectionEnd = stateObj.end;
1292                                 inputArea.scrollTop = stateObj.scrollTop;
1293                         }
1294                         else if (doc.selection) {
1295                                 
1296                                 if (doc.activeElement && doc.activeElement !== inputArea) {
1297                                         return;
1298                                 }
1299                                         
1300                                 inputArea.focus();
1301                                 var range = inputArea.createTextRange();
1302                                 range.moveStart("character", -inputArea.value.length);
1303                                 range.moveEnd("character", -inputArea.value.length);
1304                                 range.moveEnd("character", stateObj.end);
1305                                 range.moveStart("character", stateObj.start);
1306                                 range.select();
1307                         }
1308                 };
1309                 
1310                 this.setInputAreaSelectionStartEnd = function(){
1311                 
1312                         if (inputArea.selectionStart || inputArea.selectionStart === 0) {
1313                         
1314                                 stateObj.start = inputArea.selectionStart;
1315                                 stateObj.end = inputArea.selectionEnd;
1316                         }
1317                         else if (doc.selection) {
1318                                 
1319                                 stateObj.text = util.fixEolChars(inputArea.value);
1320                                 
1321                                 // IE loses the selection in the textarea when buttons are
1322                                 // clicked.  On IE we cache the selection and set a flag
1323                                 // which we check for here.
1324                                 var range;
1325                                 if(wmd.ieRetardedClick && wmd.ieCachedRange) {
1326                                         range = wmd.ieCachedRange;
1327                                         wmd.ieRetardedClick = false;
1328                                 }
1329                                 else {
1330                                         range = doc.selection.createRange();
1331                                 }
1332
1333                                 var fixedRange = util.fixEolChars(range.text);
1334                                 var marker = "\x07";
1335                                 var markedRange = marker + fixedRange + marker;
1336                                 range.text = markedRange;
1337                                 var inputText = util.fixEolChars(inputArea.value);
1338                                         
1339                                 range.moveStart("character", -markedRange.length);
1340                                 range.text = fixedRange;
1341
1342                                 stateObj.start = inputText.indexOf(marker);
1343                                 stateObj.end = inputText.lastIndexOf(marker) - marker.length;
1344                                         
1345                                 var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
1346                                         
1347                                 if (len) {
1348                                         range.moveStart("character", -fixedRange.length);
1349                                         while (len--) {
1350                                                 fixedRange += "\n";
1351                                                 stateObj.end += 1;
1352                                         }
1353                                         range.text = fixedRange;
1354                                 }
1355                                         
1356                                 this.setInputAreaSelection();
1357                         }
1358                 };
1359                 
1360                 // Restore this state into the input area.
1361                 this.restore = function(){
1362                 
1363                         if (stateObj.text != undefined && stateObj.text != inputArea.value) {
1364                                 inputArea.value = stateObj.text;
1365                         }
1366                         this.setInputAreaSelection();
1367                         inputArea.scrollTop = stateObj.scrollTop;
1368                 };
1369                 
1370                 // Gets a collection of HTML chunks from the inptut textarea.
1371                 this.getChunks = function(){
1372                 
1373                         var chunk = new wmd.Chunks();
1374                         
1375                         chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
1376                         chunk.startTag = "";
1377                         chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
1378                         chunk.endTag = "";
1379                         chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
1380                         chunk.scrollTop = stateObj.scrollTop;
1381                         
1382                         return chunk;
1383                 };
1384                 
1385                 // Sets the TextareaState properties given a chunk of markdown.
1386                 this.setChunks = function(chunk){
1387                 
1388                         chunk.before = chunk.before + chunk.startTag;
1389                         chunk.after = chunk.endTag + chunk.after;
1390                         
1391                         if (global.isOpera) {
1392                                 chunk.before = chunk.before.replace(/\n/g, "\r\n");
1393                                 chunk.selection = chunk.selection.replace(/\n/g, "\r\n");
1394                                 chunk.after = chunk.after.replace(/\n/g, "\r\n");
1395                         }
1396                         
1397                         this.start = chunk.before.length;
1398                         this.end = chunk.before.length + chunk.selection.length;
1399                         this.text = chunk.before + chunk.selection + chunk.after;
1400                         this.scrollTop = chunk.scrollTop;
1401                 };
1402
1403                 this.init();
1404         };
1405         
1406         // before: contains all the text in the input box BEFORE the selection.
1407         // after: contains all the text in the input box AFTER the selection.
1408         wmd.Chunks = function(){
1409         };
1410         
1411         // startRegex: a regular expression to find the start tag
1412         // endRegex: a regular expresssion to find the end tag
1413         wmd.Chunks.prototype.findTags = function(startRegex, endRegex){
1414         
1415                 var chunkObj = this;
1416                 var regex;
1417                 
1418                 if (startRegex) {
1419                         
1420                         regex = util.extendRegExp(startRegex, "", "$");
1421                         
1422                         this.before = this.before.replace(regex, 
1423                                 function(match){
1424                                         chunkObj.startTag = chunkObj.startTag + match;
1425                                         return "";
1426                                 });
1427                         
1428                         regex = util.extendRegExp(startRegex, "^", "");
1429                         
1430                         this.selection = this.selection.replace(regex, 
1431                                 function(match){
1432                                         chunkObj.startTag = chunkObj.startTag + match;
1433                                         return "";
1434                                 });
1435                 }
1436                 
1437                 if (endRegex) {
1438                         
1439                         regex = util.extendRegExp(endRegex, "", "$");
1440                         
1441                         this.selection = this.selection.replace(regex,
1442                                 function(match){
1443                                         chunkObj.endTag = match + chunkObj.endTag;
1444                                         return "";
1445                                 });
1446
1447                         regex = util.extendRegExp(endRegex, "^", "");
1448                         
1449                         this.after = this.after.replace(regex,
1450                                 function(match){
1451                                         chunkObj.endTag = match + chunkObj.endTag;
1452                                         return "";
1453                                 });
1454                 }
1455         };
1456         
1457         // If remove is false, the whitespace is transferred
1458         // to the before/after regions.
1459         //
1460         // If remove is true, the whitespace disappears.
1461         wmd.Chunks.prototype.trimWhitespace = function(remove){
1462         
1463                 this.selection = this.selection.replace(/^(\s*)/, "");
1464                 
1465                 if (!remove) {
1466                         this.before += re.$1;
1467                 }
1468                 
1469                 this.selection = this.selection.replace(/(\s*)$/, "");
1470                 
1471                 if (!remove) {
1472                         this.after = re.$1 + this.after;
1473                 }
1474         };
1475         
1476         
1477         wmd.Chunks.prototype.addBlankLines = function(nLinesBefore, nLinesAfter, findExtraNewlines){
1478         
1479                 if (nLinesBefore === undefined) {
1480                         nLinesBefore = 1;
1481                 }
1482                 
1483                 if (nLinesAfter === undefined) {
1484                         nLinesAfter = 1;
1485                 }
1486                 
1487                 nLinesBefore++;
1488                 nLinesAfter++;
1489                 
1490                 var regexText;
1491                 var replacementText;
1492                 
1493                 this.selection = this.selection.replace(/(^\n*)/, "");
1494                 this.startTag = this.startTag + re.$1;
1495                 this.selection = this.selection.replace(/(\n*$)/, "");
1496                 this.endTag = this.endTag + re.$1;
1497                 this.startTag = this.startTag.replace(/(^\n*)/, "");
1498                 this.before = this.before + re.$1;
1499                 this.endTag = this.endTag.replace(/(\n*$)/, "");
1500                 this.after = this.after + re.$1;
1501                 
1502                 if (this.before) {
1503                 
1504                         regexText = replacementText = "";
1505                         
1506                         while (nLinesBefore--) {
1507                                 regexText += "\\n?";
1508                                 replacementText += "\n";
1509                         }
1510                         
1511                         if (findExtraNewlines) {
1512                                 regexText = "\\n*";
1513                         }
1514                         this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
1515                 }
1516                 
1517                 if (this.after) {
1518                 
1519                         regexText = replacementText = "";
1520                         
1521                         while (nLinesAfter--) {
1522                                 regexText += "\\n?";
1523                                 replacementText += "\n";
1524                         }
1525                         if (findExtraNewlines) {
1526                                 regexText = "\\n*";
1527                         }
1528                         
1529                         this.after = this.after.replace(new re(regexText, ""), replacementText);
1530                 }
1531         };
1532         
1533         // The markdown symbols - 4 spaces = code, > = blockquote, etc.
1534         command.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
1535         
1536         // Remove markdown symbols from the chunk selection.
1537         command.unwrap = function(chunk){
1538                 var txt = new re("([^\\n])\\n(?!(\\n|" + command.prefixes + "))", "g");
1539                 chunk.selection = chunk.selection.replace(txt, "$1 $2");
1540         };
1541         
1542         command.wrap = function(chunk, len){
1543                 command.unwrap(chunk);
1544                 var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm");
1545                 
1546                 chunk.selection = chunk.selection.replace(regex, function(line, marked){
1547                         if (new re("^" + command.prefixes, "").test(line)) {
1548                                 return line;
1549                         }
1550                         return marked + "\n";
1551                 });
1552                 
1553                 chunk.selection = chunk.selection.replace(/\s+$/, "");
1554         };
1555         
1556         command.doBold = function(chunk, postProcessing, useDefaultText){
1557                 return command.doBorI(chunk, 2, "strong text");
1558         };
1559         
1560         command.doItalic = function(chunk, postProcessing, useDefaultText){
1561                 return command.doBorI(chunk, 1, "emphasized text");
1562         };
1563         
1564         // chunk: The selected region that will be enclosed with */**
1565         // nStars: 1 for italics, 2 for bold
1566         // insertText: If you just click the button without highlighting text, this gets inserted
1567         command.doBorI = function(chunk, nStars, insertText){
1568         
1569                 // Get rid of whitespace and fixup newlines.
1570                 chunk.trimWhitespace();
1571                 chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
1572                 
1573                 // Look for stars before and after.  Is the chunk already marked up?
1574                 chunk.before.search(/(\**$)/);
1575                 var starsBefore = re.$1;
1576                 
1577                 chunk.after.search(/(^\**)/);
1578                 var starsAfter = re.$1;
1579                 
1580                 var prevStars = Math.min(starsBefore.length, starsAfter.length);
1581                 
1582                 // Remove stars if we have to since the button acts as a toggle.
1583                 if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
1584                         chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
1585                         chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
1586                 }
1587                 else if (!chunk.selection && starsAfter) {
1588                         // It's not really clear why this code is necessary.  It just moves
1589                         // some arbitrary stuff around.
1590                         chunk.after = chunk.after.replace(/^([*_]*)/, "");
1591                         chunk.before = chunk.before.replace(/(\s?)$/, "");
1592                         var whitespace = re.$1;
1593                         chunk.before = chunk.before + starsAfter + whitespace;
1594                 }
1595                 else {
1596                 
1597                         // In most cases, if you don't have any selected text and click the button
1598                         // you'll get a selected, marked up region with the default text inserted.
1599                         if (!chunk.selection && !starsAfter) {
1600                                 chunk.selection = insertText;
1601                         }
1602                         
1603                         // Add the true markup.
1604                         var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
1605                         chunk.before = chunk.before + markup;
1606                         chunk.after = markup + chunk.after;
1607                 }
1608                 
1609                 return;
1610         };
1611         
1612         command.stripLinkDefs = function(text, defsToAdd){
1613         
1614                 text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, 
1615                         function(totalMatch, id, link, newlines, title){        
1616                                 defsToAdd[id] = totalMatch.replace(/\s*$/, "");
1617                                 if (newlines) {
1618                                         // Strip the title and return that separately.
1619                                         defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
1620                                         return newlines + title;
1621                                 }
1622                                 return "";
1623                         });
1624                 
1625                 return text;
1626         };
1627         
1628         command.addLinkDef = function(chunk, linkDef){
1629         
1630                 var refNumber = 0; // The current reference number
1631                 var defsToAdd = {}; //
1632                 // Start with a clean slate by removing all previous link definitions.
1633                 chunk.before = command.stripLinkDefs(chunk.before, defsToAdd);
1634                 chunk.selection = command.stripLinkDefs(chunk.selection, defsToAdd);
1635                 chunk.after = command.stripLinkDefs(chunk.after, defsToAdd);
1636                 
1637                 var defs = "";
1638                 var regex = /(\[(?:\[[^\]]*\]|[^\[\]])*\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
1639                 
1640                 var addDefNumber = function(def){
1641                         refNumber++;
1642                         def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, "  [" + refNumber + "]:");
1643                         defs += "\n" + def;
1644                 };
1645                 
1646                 var getLink = function(wholeMatch, link, id, end){
1647                 
1648                         if (defsToAdd[id]) {
1649                                 addDefNumber(defsToAdd[id]);
1650                                 return link + refNumber + end;
1651                                 
1652                         }
1653                         return wholeMatch;
1654                 };
1655                 
1656                 chunk.before = chunk.before.replace(regex, getLink);
1657                 
1658                 if (linkDef) {
1659                         addDefNumber(linkDef);
1660                 }
1661                 else {
1662                         chunk.selection = chunk.selection.replace(regex, getLink);
1663                 }
1664                 
1665                 var refOut = refNumber;
1666                 
1667                 chunk.after = chunk.after.replace(regex, getLink);
1668                 
1669                 if (chunk.after) {
1670                         chunk.after = chunk.after.replace(/\n*$/, "");
1671                 }
1672                 if (!chunk.after) {
1673                         chunk.selection = chunk.selection.replace(/\n*$/, "");
1674                 }
1675                 
1676                 chunk.after += "\n\n" + defs;
1677                 
1678                 return refOut;
1679         };
1680         
1681         command.doLinkOrImage = function(chunk, postProcessing, isImage){
1682         
1683                 chunk.trimWhitespace();
1684                 chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
1685                 
1686                 if (chunk.endTag.length > 1) {
1687                 
1688                         chunk.startTag = chunk.startTag.replace(/!?\[/, "");
1689                         chunk.endTag = "";
1690                         command.addLinkDef(chunk, null);
1691                         
1692                 }
1693                 else {
1694                 
1695                         if (/\n\n/.test(chunk.selection)) {
1696                                 command.addLinkDef(chunk, null);
1697                                 return;
1698                         }
1699                         
1700                         // The function to be executed when you enter a link and press OK or Cancel.
1701                         // Marks up the link and adds the ref.
1702                         var makeLinkMarkdown = function(link){
1703                         
1704                                 if (link !== null) {
1705                                 
1706                                         chunk.startTag = chunk.endTag = "";
1707                                         var linkDef = " [999]: " + link;
1708                                         
1709                                         var num = command.addLinkDef(chunk, linkDef);
1710                                         chunk.startTag = isImage ? "![" : "[";
1711                                         chunk.endTag = "][" + num + "]";
1712                                         
1713                                         if (!chunk.selection) {
1714                                                 if (isImage) {
1715                                                         chunk.selection = "alt text";
1716                                                 }
1717                                                 else {
1718                                                         chunk.selection = "link text";
1719                                                 }
1720                                         }
1721                                 }
1722                                 postProcessing();
1723                         };
1724                         
1725                         if (isImage) {
1726                                 util.prompt(imageDialogText, imageDefaultText, makeLinkMarkdown);
1727                         }
1728                         else {
1729                                 util.prompt(linkDialogText, linkDefaultText, makeLinkMarkdown);
1730                         }
1731                         return true;
1732                 }
1733         };
1734         
1735         util.makeAPI = function(){
1736                 wmd.wmd = {};
1737                 wmd.wmd.editor = wmd.editor;
1738                 wmd.wmd.previewManager = wmd.previewManager;
1739         };
1740         
1741         util.startEditor = function(){
1742         
1743                 if (wmd.wmd_env.autostart === false) {
1744                         util.makeAPI();
1745                         return;
1746                 }
1747
1748                 var edit;               // The editor (buttons + input + outputs) - the main object.
1749                 var previewMgr; // The preview manager.
1750                 
1751                 // Fired after the page has fully loaded.
1752                 var loadListener = function(){
1753                 
1754                         wmd.panels = new wmd.PanelCollection();
1755                         
1756                         previewMgr = new wmd.previewManager();
1757                         var previewRefreshCallback = previewMgr.refresh;
1758                                                 
1759                         edit = new wmd.editor(previewRefreshCallback);
1760                         
1761                         previewMgr.refresh(true);
1762                         
1763                 };
1764                 
1765                 util.addEvent(top, "load", loadListener);
1766         };
1767         
1768         wmd.previewManager = function(){
1769                 
1770                 var managerObj = this;
1771                 var converter;
1772                 var poller;
1773                 var timeout;
1774                 var elapsedTime;
1775                 var oldInputText;
1776                 var htmlOut;
1777                 var maxDelay = 3000;
1778                 var startType = "delayed"; // The other legal value is "manual"
1779                 
1780                 // Adds event listeners to elements and creates the input poller.
1781                 var setupEvents = function(inputElem, listener){
1782                 
1783                         util.addEvent(inputElem, "input", listener);
1784                         inputElem.onpaste = listener;
1785                         inputElem.ondrop = listener;
1786                         
1787                         util.addEvent(inputElem, "keypress", listener);
1788                         util.addEvent(inputElem, "keydown", listener);
1789                         // previewPollInterval is set at the top of this file.
1790                         poller = new wmd.inputPoller(listener, previewPollInterval);
1791                 };
1792                 
1793                 var getDocScrollTop = function(){
1794                 
1795                         var result = 0;
1796                         
1797                         if (top.innerHeight) {
1798                                 result = top.pageYOffset;
1799                         }
1800                         else 
1801                                 if (doc.documentElement && doc.documentElement.scrollTop) {
1802                                         result = doc.documentElement.scrollTop;
1803                                 }
1804                                 else 
1805                                         if (doc.body) {
1806                                                 result = doc.body.scrollTop;
1807                                         }
1808                         
1809                         return result;
1810                 };
1811                 
1812                 var makePreviewHtml = function(){
1813                 
1814                         // If there are no registered preview and output panels
1815                         // there is nothing to do.
1816                         if (!wmd.panels.preview && !wmd.panels.output) {
1817                                 return;
1818                         }
1819                         
1820                         var text = wmd.panels.input.value;
1821                         if (text && text == oldInputText) {
1822                                 return; // Input text hasn't changed.
1823                         }
1824                         else {
1825                                 oldInputText = text;
1826                         }
1827                         
1828                         var prevTime = new Date().getTime();
1829                         
1830                         if (!converter && wmd.showdown) {
1831                                 converter = new wmd.showdown.converter();
1832                         }
1833                         
1834                         if (converter) {
1835                                 text = converter.makeHtml(text);
1836                         }
1837                         
1838                         // Calculate the processing time of the HTML creation.
1839                         // It's used as the delay time in the event listener.
1840                         var currTime = new Date().getTime();
1841                         elapsedTime = currTime - prevTime;
1842                         
1843                         pushPreviewHtml(text);
1844                         htmlOut = text;
1845                 };
1846                 
1847                 // setTimeout is already used.  Used as an event listener.
1848                 var applyTimeout = function(){
1849                 
1850                         if (timeout) {
1851                                 top.clearTimeout(timeout);
1852                                 timeout = undefined;
1853                         }
1854                         
1855                         if (startType !== "manual") {
1856                         
1857                                 var delay = 0;
1858                                 
1859                                 if (startType === "delayed") {
1860                                         delay = elapsedTime;
1861                                 }
1862                                 
1863                                 if (delay > maxDelay) {
1864                                         delay = maxDelay;
1865                                 }
1866                                 timeout = top.setTimeout(makePreviewHtml, delay);
1867                         }
1868                 };
1869                 
1870                 var getScaleFactor = function(panel){
1871                         if (panel.scrollHeight <= panel.clientHeight) {
1872                                 return 1;
1873                         }
1874                         return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
1875                 };
1876                 
1877                 var setPanelScrollTops = function(){
1878                 
1879                         if (wmd.panels.preview) {
1880                                 wmd.panels.preview.scrollTop = (wmd.panels.preview.scrollHeight - wmd.panels.preview.clientHeight) * getScaleFactor(wmd.panels.preview);
1881                                 ;
1882                         }
1883                         
1884                         if (wmd.panels.output) {
1885                                 wmd.panels.output.scrollTop = (wmd.panels.output.scrollHeight - wmd.panels.output.clientHeight) * getScaleFactor(wmd.panels.output);
1886                                 ;
1887                         }
1888                 };
1889                 
1890                 this.refresh = function(requiresRefresh){
1891                 
1892                         if (requiresRefresh) {
1893                                 oldInputText = "";
1894                                 makePreviewHtml();
1895                         }
1896                         else {
1897                                 applyTimeout();
1898                         }
1899                 };
1900                 
1901                 this.processingTime = function(){
1902                         return elapsedTime;
1903                 };
1904                 
1905                 // The output HTML
1906                 this.output = function(){
1907                         return htmlOut;
1908                 };
1909                 
1910                 // The mode can be "manual" or "delayed"
1911                 this.setUpdateMode = function(mode){
1912                         startType = mode;
1913                         managerObj.refresh();
1914                 };
1915                 
1916                 var isFirstTimeFilled = true;
1917                 
1918                 var pushPreviewHtml = function(text){
1919                 
1920                         var emptyTop = position.getTop(wmd.panels.input) - getDocScrollTop();
1921                         
1922                         // Send the encoded HTML to the output textarea/div.
1923                         if (wmd.panels.output) {
1924                                 // The value property is only defined if the output is a textarea.
1925                                 if (wmd.panels.output.value !== undefined) {
1926                                         wmd.panels.output.value = text;
1927                                         wmd.panels.output.readOnly = true;
1928                                 }
1929                                 // Otherwise we are just replacing the text in a div.
1930                                 // Send the HTML wrapped in <pre><code>
1931                                 else {
1932                                         var newText = text.replace(/&/g, "&amp;");
1933                                         newText = newText.replace(/</g, "&lt;");
1934                                         wmd.panels.output.innerHTML = "<pre><code>" + newText + "</code></pre>";
1935                                 }
1936                         }
1937                         
1938                         if (wmd.panels.preview) {
1939                                 wmd.panels.preview.innerHTML = text;
1940                         }
1941                         
1942                         setPanelScrollTops();
1943                         
1944                         if (isFirstTimeFilled) {
1945                                 isFirstTimeFilled = false;
1946                                 return;
1947                         }
1948                         
1949                         var fullTop = position.getTop(wmd.panels.input) - getDocScrollTop();
1950                         
1951                         if (global.isIE) {
1952                                 top.setTimeout(function(){
1953                                         top.scrollBy(0, fullTop - emptyTop);
1954                                 }, 0);
1955                         }
1956                         else {
1957                                 top.scrollBy(0, fullTop - emptyTop);
1958                         }
1959                 };
1960                 
1961                 var init = function(){
1962                 
1963                         setupEvents(wmd.panels.input, applyTimeout);
1964                         makePreviewHtml();
1965                         
1966                         if (wmd.panels.preview) {
1967                                 wmd.panels.preview.scrollTop = 0;
1968                         }
1969                         if (wmd.panels.output) {
1970                                 wmd.panels.output.scrollTop = 0;
1971                         }
1972                 };
1973                 
1974                 this.destroy = function(){
1975                         if (poller) {
1976                                 poller.destroy();
1977                         }
1978                 };
1979                 
1980                 init();
1981         };
1982
1983         // Moves the cursor to the next line and continues lists, quotes and code.
1984         command.doAutoindent = function(chunk, postProcessing, useDefaultText){
1985                 
1986                 chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
1987                 chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
1988                 chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
1989                 
1990                 useDefaultText = false;
1991                 
1992                 if(/(\n|^)[ ]{0,3}([*+-])[ \t]+.*\n$/.test(chunk.before)){
1993                         if(command.doList){
1994                                 command.doList(chunk, postProcessing, false, true);
1995                         }
1996                 }
1997                 if(/(\n|^)[ ]{0,3}(\d+[.])[ \t]+.*\n$/.test(chunk.before)){
1998                         if(command.doList){
1999                                 command.doList(chunk, postProcessing, true, true);
2000                         }
2001                 }
2002                 if(/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)){
2003                         if(command.doBlockquote){
2004                                 command.doBlockquote(chunk, postProcessing, useDefaultText);
2005                         }
2006                 }
2007                 if(/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)){
2008                         if(command.doCode){
2009                                 command.doCode(chunk, postProcessing, useDefaultText);
2010                         }
2011                 }
2012         };
2013         
2014         command.doBlockquote = function(chunk, postProcessing, useDefaultText){
2015                 
2016                 chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
2017                         function(totalMatch, newlinesBefore, text, newlinesAfter){
2018                                 chunk.before += newlinesBefore;
2019                                 chunk.after = newlinesAfter + chunk.after;
2020                                 return text;
2021                         });
2022                         
2023                 chunk.before = chunk.before.replace(/(>[ \t]*)$/,
2024                         function(totalMatch, blankLine){
2025                                 chunk.selection = blankLine + chunk.selection;
2026                                 return "";
2027                         });
2028                 
2029                 var defaultText = useDefaultText ? "Blockquote" : "";
2030                 chunk.selection = chunk.selection.replace(/^(\s|>)+$/ ,"");
2031                 chunk.selection = chunk.selection || defaultText;
2032                 
2033                 if(chunk.before){
2034                         chunk.before = chunk.before.replace(/\n?$/,"\n");
2035                 }
2036                 if(chunk.after){
2037                         chunk.after = chunk.after.replace(/^\n?/,"\n");
2038                 }
2039                 
2040                 chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
2041                         function(totalMatch){
2042                                 chunk.startTag = totalMatch;
2043                                 return "";
2044                         });
2045                         
2046                 chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
2047                         function(totalMatch){
2048                                 chunk.endTag = totalMatch;
2049                                 return "";
2050                         });
2051                 
2052                 var replaceBlanksInTags = function(useBracket){
2053                         
2054                         var replacement = useBracket ? "> " : "";
2055                         
2056                         if(chunk.startTag){
2057                                 chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
2058                                         function(totalMatch, markdown){
2059                                                 return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
2060                                         });
2061                         }
2062                         if(chunk.endTag){
2063                                 chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
2064                                         function(totalMatch, markdown){
2065                                                 return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
2066                                         });
2067                         }
2068                 };
2069                 
2070                 if(/^(?![ ]{0,3}>)/m.test(chunk.selection)){
2071                         command.wrap(chunk, wmd.wmd_env.lineLength - 2);
2072                         chunk.selection = chunk.selection.replace(/^/gm, "> ");
2073                         replaceBlanksInTags(true);
2074                         chunk.addBlankLines();
2075                 }
2076                 else{
2077                         chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
2078                         command.unwrap(chunk);
2079                         replaceBlanksInTags(false);
2080                         
2081                         if(!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag){
2082                                 chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
2083                         }
2084                         
2085                         if(!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag){
2086                                 chunk.endTag=chunk.endTag.replace(/^\n{0,2}/, "\n\n");
2087                         }
2088                 }
2089                 
2090                 if(!/\n/.test(chunk.selection)){
2091                         chunk.selection = chunk.selection.replace(/^(> *)/,
2092                         function(wholeMatch, blanks){
2093                                 chunk.startTag += blanks;
2094                                 return "";
2095                         });
2096                 }
2097         };
2098
2099         command.doCode = function(chunk, postProcessing, useDefaultText){
2100                 
2101                 var hasTextBefore = /\S[ ]*$/.test(chunk.before);
2102                 var hasTextAfter = /^[ ]*\S/.test(chunk.after);
2103                 
2104                 // Use 'four space' markdown if the selection is on its own
2105                 // line or is multiline.
2106                 if((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)){
2107                         
2108                         chunk.before = chunk.before.replace(/[ ]{4}$/,
2109                                 function(totalMatch){
2110                                         chunk.selection = totalMatch + chunk.selection;
2111                                         return "";
2112                                 });
2113                                 
2114                         var nLinesBefore = 1;
2115                         var nLinesAfter = 1;
2116                         
2117                         
2118                         if(/\n(\t|[ ]{4,}).*\n$/.test(chunk.before) || chunk.after === ""){
2119                                 nLinesBefore = 0; 
2120                         }
2121                         if(/^\n(\t|[ ]{4,})/.test(chunk.after)){
2122                                 nLinesAfter = 0; // This needs to happen on line 1
2123                         }
2124                         
2125                         chunk.addBlankLines(nLinesBefore, nLinesAfter);
2126                         
2127                         if(!chunk.selection){
2128                                 chunk.startTag = "    ";
2129                                 chunk.selection = useDefaultText ? "enter code here" : "";
2130                         }
2131                         else {
2132                                 if(/^[ ]{0,3}\S/m.test(chunk.selection)){
2133                                         chunk.selection = chunk.selection.replace(/^/gm, "    ");
2134                                 }
2135                                 else{
2136                                         chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
2137                                 }
2138                         }
2139                 }
2140                 else{
2141                         // Use backticks (`) to delimit the code block.
2142                         
2143                         chunk.trimWhitespace();
2144                         chunk.findTags(/`/, /`/);
2145                         
2146                         if(!chunk.startTag && !chunk.endTag){
2147                                 chunk.startTag = chunk.endTag="`";
2148                                 if(!chunk.selection){
2149                                         chunk.selection = useDefaultText ? "enter code here" : "";
2150                                 }
2151                         }
2152                         else if(chunk.endTag && !chunk.startTag){
2153                                 chunk.before += chunk.endTag;
2154                                 chunk.endTag = "";
2155                         }
2156                         else{
2157                                 chunk.startTag = chunk.endTag="";
2158                         }
2159                 }
2160         };
2161         
2162         command.doList = function(chunk, postProcessing, isNumberedList, useDefaultText){
2163                                 
2164                 // These are identical except at the very beginning and end.
2165                 // Should probably use the regex extension function to make this clearer.
2166                 var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
2167                 var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
2168                 
2169                 // The default bullet is a dash but others are possible.
2170                 // This has nothing to do with the particular HTML bullet,
2171                 // it's just a markdown bullet.
2172                 var bullet = "-";
2173                 
2174                 // The number in a numbered list.
2175                 var num = 1;
2176                 
2177                 // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
2178                 var getItemPrefix = function(){
2179                         var prefix;
2180                         if(isNumberedList){
2181                                 prefix = " " + num + ". ";
2182                                 num++;
2183                         }
2184                         else{
2185                                 prefix = " " + bullet + " ";
2186                         }
2187                         return prefix;
2188                 };
2189                 
2190                 // Fixes the prefixes of the other list items.
2191                 var getPrefixedItem = function(itemText){
2192                 
2193                         // The numbering flag is unset when called by autoindent.
2194                         if(isNumberedList === undefined){
2195                                 isNumberedList = /^\s*\d/.test(itemText);
2196                         }
2197                         
2198                         // Renumber/bullet the list element.
2199                         itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
2200                                 function( _ ){
2201                                         return getItemPrefix();
2202                                 });
2203                                 
2204                         return itemText;
2205                 };
2206                 
2207                 chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
2208                 
2209                 if(chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)){
2210                         chunk.before += chunk.startTag;
2211                         chunk.startTag = "";
2212                 }
2213                 
2214                 if(chunk.startTag){
2215                         
2216                         var hasDigits = /\d+[.]/.test(chunk.startTag);
2217                         chunk.startTag = "";
2218                         chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
2219                         command.unwrap(chunk);
2220                         chunk.addBlankLines();
2221                         
2222                         if(hasDigits){
2223                                 // Have to renumber the bullet points if this is a numbered list.
2224                                 chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
2225                         }
2226                         if(isNumberedList == hasDigits){
2227                                 return;
2228                         }
2229                 }
2230                 
2231                 var nLinesBefore = 1;
2232                 
2233                 chunk.before = chunk.before.replace(previousItemsRegex,
2234                         function(itemText){
2235                                 if(/^\s*([*+-])/.test(itemText)){
2236                                         bullet = re.$1;
2237                                 }
2238                                 nLinesBefore = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2239                                 return getPrefixedItem(itemText);
2240                         });
2241                         
2242                 if(!chunk.selection){
2243                         chunk.selection = useDefaultText ? "List item" : " ";
2244                 }
2245                 
2246                 var prefix = getItemPrefix();
2247                 
2248                 var nLinesAfter = 1;
2249                 
2250                 chunk.after = chunk.after.replace(nextItemsRegex,
2251                         function(itemText){
2252                                 nLinesAfter = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2253                                 return getPrefixedItem(itemText);
2254                         });
2255                         
2256                 chunk.trimWhitespace(true);
2257                 chunk.addBlankLines(nLinesBefore, nLinesAfter, true);
2258                 chunk.startTag = prefix;
2259                 var spaces = prefix.replace(/./g, " ");
2260                 command.wrap(chunk, wmd.wmd_env.lineLength - spaces.length);
2261                 chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
2262                 
2263         };
2264         
2265         command.doHeading = function(chunk, postProcessing, useDefaultText){
2266                 
2267                 // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
2268                 chunk.selection = chunk.selection.replace(/\s+/g, " ");
2269                 chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
2270                 
2271                 // If we clicked the button with no selected text, we just
2272                 // make a level 2 hash header around some default text.
2273                 if(!chunk.selection){
2274                         chunk.startTag = "## ";
2275                         chunk.selection = "Heading";
2276                         chunk.endTag = " ##";
2277                         return;
2278                 }
2279                 
2280                 var headerLevel = 0;            // The existing header level of the selected text.
2281                 
2282                 // Remove any existing hash heading markdown and save the header level.
2283                 chunk.findTags(/#+[ ]*/, /[ ]*#+/);
2284                 if(/#+/.test(chunk.startTag)){
2285                         headerLevel = re.lastMatch.length;
2286                 }
2287                 chunk.startTag = chunk.endTag = "";
2288                 
2289                 // Try to get the current header level by looking for - and = in the line
2290                 // below the selection.
2291                 chunk.findTags(null, /\s?(-+|=+)/);
2292                 if(/=+/.test(chunk.endTag)){
2293                         headerLevel = 1;
2294                 }
2295                 if(/-+/.test(chunk.endTag)){
2296                         headerLevel = 2;
2297                 }
2298                 
2299                 // Skip to the next line so we can create the header markdown.
2300                 chunk.startTag = chunk.endTag = "";
2301                 chunk.addBlankLines(1, 1);
2302
2303                 // We make a level 2 header if there is no current header.
2304                 // If there is a header level, we substract one from the header level.
2305                 // If it's already a level 1 header, it's removed.
2306                 var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
2307                 
2308                 if(headerLevelToCreate > 0){
2309                         
2310                         // The button only creates level 1 and 2 underline headers.
2311                         // Why not have it iterate over hash header levels?  Wouldn't that be easier and cleaner?
2312                         var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
2313                         var len = chunk.selection.length;
2314                         if(len > wmd.wmd_env.lineLength){
2315                                 len = wmd.wmd_env.lineLength;
2316                         }
2317                         chunk.endTag = "\n";
2318                         while(len--){
2319                                 chunk.endTag += headerChar;
2320                         }
2321                 }
2322         };      
2323         
2324         command.doHorizontalRule = function(chunk, postProcessing, useDefaultText){
2325                 chunk.startTag = "----------\n";
2326                 chunk.selection = "";
2327                 chunk.addBlankLines(2, 1, true);
2328         }
2329 };
2330
2331
2332 Attacklab.wmd_env = {};
2333 Attacklab.account_options = {};
2334 Attacklab.wmd_defaults = {version:1, output:"Markdown", lineLength:40, delayLoad:false};
2335
2336 if(!Attacklab.wmd)
2337 {
2338         Attacklab.wmd = function()
2339         {
2340                 Attacklab.loadEnv = function()
2341                 {
2342                         var mergeEnv = function(env)
2343                         {
2344                                 if(!env)
2345                                 {
2346                                         return;
2347                                 }
2348                         
2349                                 for(var key in env)
2350                                 {
2351                                         Attacklab.wmd_env[key] = env[key];
2352                                 }
2353                         };
2354                         
2355                         mergeEnv(Attacklab.wmd_defaults);
2356                         mergeEnv(Attacklab.account_options);
2357                         mergeEnv(top["wmd_options"]);
2358                         Attacklab.full = true;
2359                         
2360                         var defaultButtons = "bold italic link blockquote code image ol ul heading hr";
2361                         Attacklab.wmd_env.buttons = Attacklab.wmd_env.buttons || defaultButtons;
2362                 };
2363                 Attacklab.loadEnv();
2364
2365         };
2366         
2367         Attacklab.wmd();
2368         Attacklab.wmdBase();
2369         Attacklab.Util.startEditor();
2370 };
2371