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