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