5e839a9bd11c56a335dc4f86c650041c3afb27e7
[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 ieRetardedClick and ieCachedRange, 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.ieRetardedClick && (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 and set a flag\r
692                 // which we check for here.\r
693                 var range;\r
694                 if (panels.ieRetardedClick && panels.ieCachedRange) {\r
695                     range = panels.ieCachedRange;\r
696                     panels.ieRetardedClick = false;\r
697                 }\r
698                 else {\r
699                     range = doc.selection.createRange();\r
700                 }\r
701 \r
702                 var fixedRange = util.fixEolChars(range.text);\r
703                 var marker = "\x07";\r
704                 var markedRange = marker + fixedRange + marker;\r
705                 range.text = markedRange;\r
706                 var inputText = util.fixEolChars(inputArea.value);\r
707 \r
708                 range.moveStart("character", -markedRange.length);\r
709                 range.text = fixedRange;\r
710 \r
711                 stateObj.start = inputText.indexOf(marker);\r
712                 stateObj.end = inputText.lastIndexOf(marker) - marker.length;\r
713 \r
714                 var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;\r
715 \r
716                 if (len) {\r
717                     range.moveStart("character", -fixedRange.length);\r
718                     while (len--) {\r
719                         fixedRange += "\n";\r
720                         stateObj.end += 1;\r
721                     }\r
722                     range.text = fixedRange;\r
723                 }\r
724 \r
725                 this.setInputAreaSelection();\r
726             }\r
727         };\r
728 \r
729         // Restore this state into the input area.\r
730         this.restore = function () {\r
731 \r
732             if (stateObj.text != undefined && stateObj.text != inputArea.value) {\r
733                 inputArea.value = stateObj.text;\r
734             }\r
735             this.setInputAreaSelection();\r
736             inputArea.scrollTop = stateObj.scrollTop;\r
737         };\r
738 \r
739         // Gets a collection of HTML chunks from the inptut textarea.\r
740         this.getChunks = function () {\r
741 \r
742             var chunk = new Chunks();\r
743             chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));\r
744             chunk.startTag = "";\r
745             chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));\r
746             chunk.endTag = "";\r
747             chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));\r
748             chunk.scrollTop = stateObj.scrollTop;\r
749 \r
750             return chunk;\r
751         };\r
752 \r
753         // Sets the TextareaState properties given a chunk of markdown.\r
754         this.setChunks = function (chunk) {\r
755 \r
756             chunk.before = chunk.before + chunk.startTag;\r
757             chunk.after = chunk.endTag + chunk.after;\r
758 \r
759             this.start = chunk.before.length;\r
760             this.end = chunk.before.length + chunk.selection.length;\r
761             this.text = chunk.before + chunk.selection + chunk.after;\r
762             this.scrollTop = chunk.scrollTop;\r
763         };\r
764         this.init();\r
765     };\r
766 \r
767     function PreviewManager(converter, panels, previewRefreshCallback) {\r
768 \r
769         var managerObj = this;\r
770         var timeout;\r
771         var elapsedTime;\r
772         var oldInputText;\r
773         var maxDelay = 3000;\r
774         var startType = "delayed"; // The other legal value is "manual"\r
775 \r
776         // Adds event listeners to elements\r
777         var setupEvents = function (inputElem, listener) {\r
778 \r
779             util.addEvent(inputElem, "input", listener);\r
780             inputElem.onpaste = listener;\r
781             inputElem.ondrop = listener;\r
782 \r
783             util.addEvent(inputElem, "keypress", listener);\r
784             util.addEvent(inputElem, "keydown", listener);\r
785         };\r
786 \r
787         var getDocScrollTop = function () {\r
788 \r
789             var result = 0;\r
790 \r
791             if (top.innerHeight) {\r
792                 result = top.pageYOffset;\r
793             }\r
794             else\r
795                 if (doc.documentElement && doc.documentElement.scrollTop) {\r
796                     result = doc.documentElement.scrollTop;\r
797                 }\r
798                 else\r
799                     if (doc.body) {\r
800                         result = doc.body.scrollTop;\r
801                     }\r
802 \r
803             return result;\r
804         };\r
805 \r
806         var makePreviewHtml = function () {\r
807 \r
808             // If there is no registered preview panel\r
809             // there is nothing to do.\r
810             if (!panels.preview)\r
811                 return;\r
812 \r
813 \r
814             var text = panels.input.value;\r
815             if (text && text == oldInputText) {\r
816                 return; // Input text hasn't changed.\r
817             }\r
818             else {\r
819                 oldInputText = text;\r
820             }\r
821 \r
822             var prevTime = new Date().getTime();\r
823 \r
824             text = converter.makeHtml(text);\r
825 \r
826             // Calculate the processing time of the HTML creation.\r
827             // It's used as the delay time in the event listener.\r
828             var currTime = new Date().getTime();\r
829             elapsedTime = currTime - prevTime;\r
830 \r
831             pushPreviewHtml(text);\r
832         };\r
833 \r
834         // setTimeout is already used.  Used as an event listener.\r
835         var applyTimeout = function () {\r
836 \r
837             if (timeout) {\r
838                 top.clearTimeout(timeout);\r
839                 timeout = undefined;\r
840             }\r
841 \r
842             if (startType !== "manual") {\r
843 \r
844                 var delay = 0;\r
845 \r
846                 if (startType === "delayed") {\r
847                     delay = elapsedTime;\r
848                 }\r
849 \r
850                 if (delay > maxDelay) {\r
851                     delay = maxDelay;\r
852                 }\r
853                 timeout = top.setTimeout(makePreviewHtml, delay);\r
854             }\r
855         };\r
856 \r
857         var getScaleFactor = function (panel) {\r
858             if (panel.scrollHeight <= panel.clientHeight) {\r
859                 return 1;\r
860             }\r
861             return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);\r
862         };\r
863 \r
864         var setPanelScrollTops = function () {\r
865             if (panels.preview) {\r
866                 panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);\r
867             }\r
868         };\r
869 \r
870         this.refresh = function (requiresRefresh) {\r
871 \r
872             if (requiresRefresh) {\r
873                 oldInputText = "";\r
874                 makePreviewHtml();\r
875             }\r
876             else {\r
877                 applyTimeout();\r
878             }\r
879         };\r
880 \r
881         this.processingTime = function () {\r
882             return elapsedTime;\r
883         };\r
884 \r
885         var isFirstTimeFilled = true;\r
886 \r
887         // IE doesn't let you use innerHTML if the element is contained somewhere in a table\r
888         // (which is the case for inline editing) -- in that case, detach the element, set the\r
889         // value, and reattach. Yes, that *is* ridiculous.\r
890         var ieSafePreviewSet = function (text) {\r
891             var preview = panels.preview;\r
892             var parent = preview.parentNode;\r
893             var sibling = preview.nextSibling;\r
894             parent.removeChild(preview);\r
895             preview.innerHTML = text;\r
896             if (!sibling)\r
897                 parent.appendChild(preview);\r
898             else\r
899                 parent.insertBefore(preview, sibling);\r
900         }\r
901 \r
902         var nonSuckyBrowserPreviewSet = function (text) {\r
903             panels.preview.innerHTML = text;\r
904         }\r
905 \r
906         var previewSetter;\r
907 \r
908         var previewSet = function (text) {\r
909             if (previewSetter)\r
910                 return previewSetter(text);\r
911 \r
912             try {\r
913                 nonSuckyBrowserPreviewSet(text);\r
914                 previewSetter = nonSuckyBrowserPreviewSet;\r
915             } catch (e) {\r
916                 previewSetter = ieSafePreviewSet;\r
917                 previewSetter(text);\r
918             }\r
919         };\r
920 \r
921         var pushPreviewHtml = function (text) {\r
922 \r
923             var emptyTop = position.getTop(panels.input) - getDocScrollTop();\r
924 \r
925             if (panels.preview) {\r
926                 previewSet(text);\r
927                 previewRefreshCallback();\r
928             }\r
929 \r
930             setPanelScrollTops();\r
931 \r
932             if (isFirstTimeFilled) {\r
933                 isFirstTimeFilled = false;\r
934                 return;\r
935             }\r
936 \r
937             var fullTop = position.getTop(panels.input) - getDocScrollTop();\r
938 \r
939             if (uaSniffed.isIE) {\r
940                 top.setTimeout(function () {\r
941                     top.scrollBy(0, fullTop - emptyTop);\r
942                 }, 0);\r
943             }\r
944             else {\r
945                 top.scrollBy(0, fullTop - emptyTop);\r
946             }\r
947         };\r
948 \r
949         var init = function () {\r
950 \r
951             setupEvents(panels.input, applyTimeout);\r
952             makePreviewHtml();\r
953 \r
954             if (panels.preview) {\r
955                 panels.preview.scrollTop = 0;\r
956             }\r
957         };\r
958 \r
959         init();\r
960     };\r
961 \r
962     // Creates the background behind the hyperlink text entry box.\r
963     // And download dialog\r
964     // Most of this has been moved to CSS but the div creation and\r
965     // browser-specific hacks remain here.\r
966     ui.createBackground = function () {\r
967 \r
968         var background = doc.createElement("div");\r
969         background.className = "wmd-prompt-background";\r
970         style = background.style;\r
971         style.position = "absolute";\r
972         style.top = "0";\r
973 \r
974         style.zIndex = "1000";\r
975 \r
976         if (uaSniffed.isIE) {\r
977             style.filter = "alpha(opacity=50)";\r
978         }\r
979         else {\r
980             style.opacity = "0.5";\r
981         }\r
982 \r
983         var pageSize = position.getPageSize();\r
984         style.height = pageSize[1] + "px";\r
985 \r
986         if (uaSniffed.isIE) {\r
987             style.left = doc.documentElement.scrollLeft;\r
988             style.width = doc.documentElement.clientWidth;\r
989         }\r
990         else {\r
991             style.left = "0";\r
992             style.width = "100%";\r
993         }\r
994 \r
995         doc.body.appendChild(background);\r
996         return background;\r
997     };\r
998 \r
999     // This simulates a modal dialog box and asks for the URL when you\r
1000     // click the hyperlink or image buttons.\r
1001     //\r
1002     // text: The html for the input box.\r
1003     // defaultInputText: The default value that appears in the input box.\r
1004     // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.\r
1005     //      It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel\r
1006     //      was chosen).\r
1007     ui.prompt = function (text, defaultInputText, callback) {\r
1008 \r
1009         // These variables need to be declared at this level since they are used\r
1010         // in multiple functions.\r
1011         var dialog;         // The dialog box.\r
1012         var input;         // The text box where you enter the hyperlink.\r
1013 \r
1014 \r
1015         if (defaultInputText === undefined) {\r
1016             defaultInputText = "";\r
1017         }\r
1018 \r
1019         // Used as a keydown event handler. Esc dismisses the prompt.\r
1020         // Key code 27 is ESC.\r
1021         var checkEscape = function (key) {\r
1022             var code = (key.charCode || key.keyCode);\r
1023             if (code === 27) {\r
1024                 close(true);\r
1025             }\r
1026         };\r
1027 \r
1028         // Dismisses the hyperlink input box.\r
1029         // isCancel is true if we don't care about the input text.\r
1030         // isCancel is false if we are going to keep the text.\r
1031         var close = function (isCancel) {\r
1032             util.removeEvent(doc.body, "keydown", checkEscape);\r
1033             var text = input.value;\r
1034 \r
1035             if (isCancel) {\r
1036                 text = null;\r
1037             }\r
1038             else {\r
1039                 // Fixes common pasting errors.\r
1040                 text = text.replace('http://http://', 'http://');\r
1041                 text = text.replace('http://https://', 'https://');\r
1042                 text = text.replace('http://ftp://', 'ftp://');\r
1043 \r
1044                 if (text.indexOf('http://') === -1 && text.indexOf('ftp://') === -1 && text.indexOf('https://') === -1) {\r
1045                     text = 'http://' + text;\r
1046                 }\r
1047             }\r
1048 \r
1049             dialog.parentNode.removeChild(dialog);\r
1050 \r
1051             callback(text);\r
1052             return false;\r
1053         };\r
1054 \r
1055 \r
1056 \r
1057         // Create the text input box form/window.\r
1058         var createDialog = function () {\r
1059 \r
1060             // The main dialog box.\r
1061             dialog = doc.createElement("div");\r
1062             dialog.className = "wmd-prompt-dialog";\r
1063             dialog.style.padding = "10px;";\r
1064             dialog.style.position = "fixed";\r
1065             dialog.style.width = "400px";\r
1066             dialog.style.zIndex = "1001";\r
1067 \r
1068             // The dialog text.\r
1069             var question = doc.createElement("div");\r
1070             question.innerHTML = text;\r
1071             question.style.padding = "5px";\r
1072             dialog.appendChild(question);\r
1073 \r
1074             // The web form container for the text box and buttons.\r
1075             var form = doc.createElement("form");\r
1076             form.onsubmit = function () { return close(false); };\r
1077             style = form.style;\r
1078             style.padding = "0";\r
1079             style.margin = "0";\r
1080             style.cssFloat = "left";\r
1081             style.width = "100%";\r
1082             style.textAlign = "center";\r
1083             style.position = "relative";\r
1084             dialog.appendChild(form);\r
1085 \r
1086             // The input text box\r
1087             input = doc.createElement("input");\r
1088             input.type = "text";\r
1089             input.value = defaultInputText;\r
1090             style = input.style;\r
1091             style.display = "block";\r
1092             style.width = "80%";\r
1093             style.marginLeft = style.marginRight = "auto";\r
1094             form.appendChild(input);\r
1095 \r
1096             // The ok button\r
1097             var okButton = doc.createElement("input");\r
1098             okButton.type = "button";\r
1099             okButton.onclick = function () { return close(false); };\r
1100             okButton.value = "OK";\r
1101             style = okButton.style;\r
1102             style.margin = "10px";\r
1103             style.display = "inline";\r
1104             style.width = "7em";\r
1105 \r
1106 \r
1107             // The cancel button\r
1108             var cancelButton = doc.createElement("input");\r
1109             cancelButton.type = "button";\r
1110             cancelButton.onclick = function () { return close(true); };\r
1111             cancelButton.value = "Cancel";\r
1112             style = cancelButton.style;\r
1113             style.margin = "10px";\r
1114             style.display = "inline";\r
1115             style.width = "7em";\r
1116 \r
1117             form.appendChild(okButton);\r
1118             form.appendChild(cancelButton);\r
1119 \r
1120             util.addEvent(doc.body, "keydown", checkEscape);\r
1121             dialog.style.top = "50%";\r
1122             dialog.style.left = "50%";\r
1123             dialog.style.display = "block";\r
1124             if (uaSniffed.isIE_5or6) {\r
1125                 dialog.style.position = "absolute";\r
1126                 dialog.style.top = doc.documentElement.scrollTop + 200 + "px";\r
1127                 dialog.style.left = "50%";\r
1128             }\r
1129             doc.body.appendChild(dialog);\r
1130 \r
1131             // This has to be done AFTER adding the dialog to the form if you\r
1132             // want it to be centered.\r
1133             dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";\r
1134             dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";\r
1135 \r
1136         };\r
1137 \r
1138         // Why is this in a zero-length timeout?\r
1139         // Is it working around a browser bug?\r
1140         top.setTimeout(function () {\r
1141 \r
1142             createDialog();\r
1143 \r
1144             var defTextLen = defaultInputText.length;\r
1145             if (input.selectionStart !== undefined) {\r
1146                 input.selectionStart = 0;\r
1147                 input.selectionEnd = defTextLen;\r
1148             }\r
1149             else if (input.createTextRange) {\r
1150                 var range = input.createTextRange();\r
1151                 range.collapse(false);\r
1152                 range.moveStart("character", -defTextLen);\r
1153                 range.moveEnd("character", defTextLen);\r
1154                 range.select();\r
1155             }\r
1156 \r
1157             input.focus();\r
1158         }, 0);\r
1159     };\r
1160 \r
1161     function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) {\r
1162 \r
1163         var inputBox = panels.input,\r
1164             buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.\r
1165 \r
1166         makeSpritedButtonRow();\r
1167 \r
1168         var keyEvent = "keydown";\r
1169         if (uaSniffed.isOpera) {\r
1170             keyEvent = "keypress";\r
1171         }\r
1172 \r
1173         util.addEvent(inputBox, keyEvent, function (key) {\r
1174 \r
1175             // Check to see if we have a button key and, if so execute the callback.\r
1176             if ((key.ctrlKey || key.metaKey) && !key.altKey) {\r
1177 \r
1178                 var keyCode = key.charCode || key.keyCode;\r
1179                 var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();\r
1180 \r
1181                 switch (keyCodeStr) {\r
1182                     case "b":\r
1183                         doClick(buttons.bold);\r
1184                         break;\r
1185                     case "i":\r
1186                         doClick(buttons.italic);\r
1187                         break;\r
1188                     case "l":\r
1189                         doClick(buttons.link);\r
1190                         break;\r
1191                     case "q":\r
1192                         doClick(buttons.quote);\r
1193                         break;\r
1194                     case "k":\r
1195                         doClick(buttons.code);\r
1196                         break;\r
1197                     case "g":\r
1198                         doClick(buttons.image);\r
1199                         break;\r
1200                     case "o":\r
1201                         doClick(buttons.olist);\r
1202                         break;\r
1203                     case "u":\r
1204                         doClick(buttons.ulist);\r
1205                         break;\r
1206                     case "h":\r
1207                         doClick(buttons.heading);\r
1208                         break;\r
1209                     case "r":\r
1210                         doClick(buttons.hr);\r
1211                         break;\r
1212                     case "y":\r
1213                         doClick(buttons.redo);\r
1214                         break;\r
1215                     case "z":\r
1216                         if (key.shiftKey) {\r
1217                             doClick(buttons.redo);\r
1218                         }\r
1219                         else {\r
1220                             doClick(buttons.undo);\r
1221                         }\r
1222                         break;\r
1223                     default:\r
1224                         return;\r
1225                 }\r
1226 \r
1227 \r
1228                 if (key.preventDefault) {\r
1229                     key.preventDefault();\r
1230                 }\r
1231 \r
1232                 if (top.event) {\r
1233                     top.event.returnValue = false;\r
1234                 }\r
1235             }\r
1236         });\r
1237 \r
1238         // Auto-indent on shift-enter\r
1239         util.addEvent(inputBox, "keyup", function (key) {\r
1240             if (key.shiftKey && !key.ctrlKey && !key.metaKey) {\r
1241                 var keyCode = key.charCode || key.keyCode;\r
1242                 // Character 13 is Enter\r
1243                 if (keyCode === 13) {\r
1244                     fakeButton = {};\r
1245                     fakeButton.textOp = bindCommand("doAutoindent");\r
1246                     doClick(fakeButton);\r
1247                 }\r
1248             }\r
1249         });\r
1250 \r
1251         // special handler because IE clears the context of the textbox on ESC\r
1252         if (uaSniffed.isIE) {\r
1253             util.addEvent(inputBox, "keydown", function (key) {\r
1254                 var code = key.keyCode;\r
1255                 if (code === 27) {\r
1256                     return false;\r
1257                 }\r
1258             });\r
1259         }\r
1260 \r
1261 \r
1262         // Perform the button's action.\r
1263         function doClick(button) {\r
1264 \r
1265             inputBox.focus();\r
1266 \r
1267             if (button.textOp) {\r
1268 \r
1269                 if (undoManager) {\r
1270                     undoManager.setCommandMode();\r
1271                 }\r
1272 \r
1273                 var state = new TextareaState(panels);\r
1274 \r
1275                 if (!state) {\r
1276                     return;\r
1277                 }\r
1278 \r
1279                 var chunks = state.getChunks();\r
1280 \r
1281                 // Some commands launch a "modal" prompt dialog.  Javascript\r
1282                 // can't really make a modal dialog box and the WMD code\r
1283                 // will continue to execute while the dialog is displayed.\r
1284                 // This prevents the dialog pattern I'm used to and means\r
1285                 // I can't do something like this:\r
1286                 //\r
1287                 // var link = CreateLinkDialog();\r
1288                 // makeMarkdownLink(link);\r
1289                 // \r
1290                 // Instead of this straightforward method of handling a\r
1291                 // dialog I have to pass any code which would execute\r
1292                 // after the dialog is dismissed (e.g. link creation)\r
1293                 // in a function parameter.\r
1294                 //\r
1295                 // Yes this is awkward and I think it sucks, but there's\r
1296                 // no real workaround.  Only the image and link code\r
1297                 // create dialogs and require the function pointers.\r
1298                 var fixupInputArea = function () {\r
1299 \r
1300                     inputBox.focus();\r
1301 \r
1302                     if (chunks) {\r
1303                         state.setChunks(chunks);\r
1304                     }\r
1305 \r
1306                     state.restore();\r
1307                     previewManager.refresh();\r
1308                 };\r
1309 \r
1310                 var noCleanup = button.textOp(chunks, fixupInputArea);\r
1311 \r
1312                 if (!noCleanup) {\r
1313                     fixupInputArea();\r
1314                 }\r
1315 \r
1316             }\r
1317 \r
1318             if (button.execute) {\r
1319                 button.execute(undoManager);\r
1320             }\r
1321         };\r
1322 \r
1323         function setupButton(button, isEnabled) {\r
1324 \r
1325             var normalYShift = "0px";\r
1326             var disabledYShift = "-20px";\r
1327             var highlightYShift = "-40px";\r
1328             var image = button.getElementsByTagName("span")[0];\r
1329             if (isEnabled) {\r
1330                 image.style.backgroundPosition = button.XShift + " " + normalYShift;\r
1331                 button.onmouseover = function () {\r
1332                     image.style.backgroundPosition = this.XShift + " " + highlightYShift;\r
1333                 };\r
1334 \r
1335                 button.onmouseout = function () {\r
1336                     image.style.backgroundPosition = this.XShift + " " + normalYShift;\r
1337                 };\r
1338 \r
1339                 // IE tries to select the background image "button" text (it's\r
1340                 // implemented in a list item) so we have to cache the selection\r
1341                 // on mousedown.\r
1342                 if (uaSniffed.isIE) {\r
1343                     button.onmousedown = function () {\r
1344                         if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection\r
1345                             return;\r
1346                         }\r
1347                         panels.ieRetardedClick = true;\r
1348                         panels.ieCachedRange = document.selection.createRange();\r
1349                     };\r
1350                 }\r
1351 \r
1352                 if (!button.isHelp) {\r
1353                     button.onclick = function () {\r
1354                         if (this.onmouseout) {\r
1355                             this.onmouseout();\r
1356                         }\r
1357                         doClick(this);\r
1358                         return false;\r
1359                     }\r
1360                 }\r
1361             }\r
1362             else {\r
1363                 image.style.backgroundPosition = button.XShift + " " + disabledYShift;\r
1364                 button.onmouseover = button.onmouseout = button.onclick = function () { };\r
1365             }\r
1366         }\r
1367 \r
1368         function bindCommand(method) {\r
1369             if (typeof method === "string")\r
1370                 method = commandManager[method];\r
1371             return function () { method.apply(commandManager, arguments); }\r
1372         }\r
1373 \r
1374         function makeSpritedButtonRow() {\r
1375 \r
1376             var buttonBar = panels.buttonBar;\r
1377 \r
1378             var normalYShift = "0px";\r
1379             var disabledYShift = "-20px";\r
1380             var highlightYShift = "-40px";\r
1381 \r
1382             var buttonRow = document.createElement("ul");\r
1383             buttonRow.id = "wmd-button-row" + postfix;\r
1384             buttonRow.className = 'wmd-button-row';\r
1385             buttonRow = buttonBar.appendChild(buttonRow);\r
1386             var xPosition = 0;\r
1387             var makeButton = function (id, title, XShift, textOp) {\r
1388                 var button = document.createElement("li");\r
1389                 button.className = "wmd-button";\r
1390                 button.style.left = xPosition + "px";\r
1391                 xPosition += 25;\r
1392                 var buttonImage = document.createElement("span");\r
1393                 button.id = id + postfix;\r
1394                 button.appendChild(buttonImage);\r
1395                 button.title = title;\r
1396                 button.XShift = XShift;\r
1397                 if (textOp)\r
1398                     button.textOp = textOp;\r
1399                 setupButton(button, true);\r
1400                 buttonRow.appendChild(button);\r
1401                 return button;\r
1402             };\r
1403             var makeSpacer = function (num) {\r
1404                 var spacer = document.createElement("li");\r
1405                 spacer.className = "wmd-spacer wmd-spacer" + num;\r
1406                 spacer.id = "wmd-spacer" + num + postfix;\r
1407                 buttonRow.appendChild(spacer);\r
1408                 xPosition += 25;\r
1409             }\r
1410 \r
1411             buttons.bold = makeButton("wmd-bold-button", "Strong <strong> Ctrl+B", "0px", bindCommand("doBold"));\r
1412             buttons.italic = makeButton("wmd-italic-button", "Emphasis <em> Ctrl+I", "-20px", bindCommand("doItalic"));\r
1413             makeSpacer(1);\r
1414             buttons.link = makeButton("wmd-link-button", "Hyperlink <a> Ctrl+L", "-40px", bindCommand(function (chunk, postProcessing) {\r
1415                 return this.doLinkOrImage(chunk, postProcessing, false);\r
1416             }));\r
1417             buttons.quote = makeButton("wmd-quote-button", "Blockquote <blockquote> Ctrl+Q", "-60px", bindCommand("doBlockquote"));\r
1418             buttons.code = makeButton("wmd-code-button", "Code Sample <pre><code> Ctrl+K", "-80px", bindCommand("doCode"));\r
1419             buttons.image = makeButton("wmd-image-button", "Image <img> Ctrl+G", "-100px", bindCommand(function (chunk, postProcessing) {\r
1420                 return this.doLinkOrImage(chunk, postProcessing, true);\r
1421             }));\r
1422             makeSpacer(2);\r
1423             buttons.olist = makeButton("wmd-olist-button", "Numbered List <ol> Ctrl+O", "-120px", bindCommand(function (chunk, postProcessing) {\r
1424                 this.doList(chunk, postProcessing, true);\r
1425             }));\r
1426             buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List <ul> Ctrl+U", "-140px", bindCommand(function (chunk, postProcessing) {\r
1427                 this.doList(chunk, postProcessing, false);\r
1428             }));\r
1429             buttons.heading = makeButton("wmd-heading-button", "Heading <h1>/<h2> Ctrl+H", "-160px", bindCommand("doHeading"));\r
1430             buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule <hr> Ctrl+R", "-180px", bindCommand("doHorizontalRule"));\r
1431             makeSpacer(3);\r
1432             buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "-200px", null);\r
1433             buttons.undo.execute = function (manager) { if (manager) manager.undo(); };\r
1434 \r
1435             var redoTitle = /win/.test(nav.platform.toLowerCase()) ?\r
1436                 "Redo - Ctrl+Y" :\r
1437                 "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms\r
1438 \r
1439             buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null);\r
1440             buttons.redo.execute = function (manager) { if (manager) manager.redo(); };\r
1441 \r
1442             if (helpOptions) {\r
1443                 var helpButton = document.createElement("li");\r
1444                 var helpButtonImage = document.createElement("span");\r
1445                 helpButton.appendChild(helpButtonImage);\r
1446                 helpButton.className = "wmd-button wmd-help-button";\r
1447                 helpButton.id = "wmd-help-button" + postfix;\r
1448                 helpButton.XShift = "-240px";\r
1449                 helpButton.isHelp = true;\r
1450                 helpButton.style.right = "0px";\r
1451                 helpButton.title = helpOptions.title || defaultHelpHoverTitle;\r
1452                 helpButton.onclick = helpOptions.handler;\r
1453 \r
1454                 setupButton(helpButton, true);\r
1455                 buttonRow.appendChild(helpButton);\r
1456                 buttons.help = helpButton;\r
1457             }\r
1458 \r
1459             setUndoRedoButtonStates();\r
1460         }\r
1461 \r
1462         function setUndoRedoButtonStates() {\r
1463             if (undoManager) {\r
1464                 setupButton(buttons.undo, undoManager.canUndo());\r
1465                 setupButton(buttons.redo, undoManager.canRedo());\r
1466             }\r
1467         };\r
1468 \r
1469         this.setUndoRedoButtonStates = setUndoRedoButtonStates;\r
1470 \r
1471     }\r
1472 \r
1473     function CommandManager(pluginHooks) {\r
1474         this.hooks = pluginHooks;\r
1475     }\r
1476 \r
1477     var commandProto = CommandManager.prototype;\r
1478 \r
1479     // The markdown symbols - 4 spaces = code, > = blockquote, etc.\r
1480     commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";\r
1481 \r
1482     // Remove markdown symbols from the chunk selection.\r
1483     commandProto.unwrap = function (chunk) {\r
1484         var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");\r
1485         chunk.selection = chunk.selection.replace(txt, "$1 $2");\r
1486     };\r
1487 \r
1488     commandProto.wrap = function (chunk, len) {\r
1489         this.unwrap(chunk);\r
1490         var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm");\r
1491 \r
1492         chunk.selection = chunk.selection.replace(regex, function (line, marked) {\r
1493             if (new re("^" + this.prefixes, "").test(line)) {\r
1494                 return line;\r
1495             }\r
1496             return marked + "\n";\r
1497         });\r
1498 \r
1499         chunk.selection = chunk.selection.replace(/\s+$/, "");\r
1500     };\r
1501 \r
1502     commandProto.doBold = function (chunk, postProcessing) {\r
1503         return this.doBorI(chunk, postProcessing, 2, "strong text");\r
1504     };\r
1505 \r
1506     commandProto.doItalic = function (chunk, postProcessing) {\r
1507         return this.doBorI(chunk, postProcessing, 1, "emphasized text");\r
1508     };\r
1509 \r
1510     // chunk: The selected region that will be enclosed with */**\r
1511     // nStars: 1 for italics, 2 for bold\r
1512     // insertText: If you just click the button without highlighting text, this gets inserted\r
1513     commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {\r
1514 \r
1515         // Get rid of whitespace and fixup newlines.\r
1516         chunk.trimWhitespace();\r
1517         chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");\r
1518 \r
1519         // Look for stars before and after.  Is the chunk already marked up?\r
1520         // note that these regex matches cannot fail\r
1521         var starsBefore = /(\**$)/.exec(chunk.before)[0];\r
1522         var starsAfter = /(^\**)/.exec(chunk.after)[0];\r
1523 \r
1524         var prevStars = Math.min(starsBefore.length, starsAfter.length);\r
1525 \r
1526         // Remove stars if we have to since the button acts as a toggle.\r
1527         if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {\r
1528             chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");\r
1529             chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");\r
1530         }\r
1531         else if (!chunk.selection && starsAfter) {\r
1532             // It's not really clear why this code is necessary.  It just moves\r
1533             // some arbitrary stuff around.\r
1534             chunk.after = chunk.after.replace(/^([*_]*)/, "");\r
1535             chunk.before = chunk.before.replace(/(\s?)$/, "");\r
1536             var whitespace = re.$1;\r
1537             chunk.before = chunk.before + starsAfter + whitespace;\r
1538         }\r
1539         else {\r
1540 \r
1541             // In most cases, if you don't have any selected text and click the button\r
1542             // you'll get a selected, marked up region with the default text inserted.\r
1543             if (!chunk.selection && !starsAfter) {\r
1544                 chunk.selection = insertText;\r
1545             }\r
1546 \r
1547             // Add the true markup.\r
1548             var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?\r
1549             chunk.before = chunk.before + markup;\r
1550             chunk.after = markup + chunk.after;\r
1551         }\r
1552 \r
1553         return;\r
1554     };\r
1555 \r
1556     commandProto.stripLinkDefs = function (text, defsToAdd) {\r
1557 \r
1558         text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,\r
1559             function (totalMatch, id, link, newlines, title) {\r
1560                 defsToAdd[id] = totalMatch.replace(/\s*$/, "");\r
1561                 if (newlines) {\r
1562                     // Strip the title and return that separately.\r
1563                     defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");\r
1564                     return newlines + title;\r
1565                 }\r
1566                 return "";\r
1567             });\r
1568 \r
1569         return text;\r
1570     };\r
1571 \r
1572     commandProto.addLinkDef = function (chunk, linkDef) {\r
1573 \r
1574         var refNumber = 0; // The current reference number\r
1575         var defsToAdd = {}; //\r
1576         // Start with a clean slate by removing all previous link definitions.\r
1577         chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);\r
1578         chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);\r
1579         chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);\r
1580 \r
1581         var defs = "";\r
1582         var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;\r
1583 \r
1584         var addDefNumber = function (def) {\r
1585             refNumber++;\r
1586             def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, "  [" + refNumber + "]:");\r
1587             defs += "\n" + def;\r
1588         };\r
1589 \r
1590         // note that\r
1591         // a) the recursive call to getLink cannot go infinite, because by definition\r
1592         //    of regex, inner is always a proper substring of wholeMatch, and\r
1593         // b) more than one level of nesting is neither supported by the regex\r
1594         //    nor making a lot of sense (the only use case for nesting is a linked image)\r
1595         var getLink = function (wholeMatch, before, inner, afterInner, id, end) {\r
1596             inner = inner.replace(regex, getLink);\r
1597             if (defsToAdd[id]) {\r
1598                 addDefNumber(defsToAdd[id]);\r
1599                 return before + inner + afterInner + refNumber + end;\r
1600             }\r
1601             return wholeMatch;\r
1602         };\r
1603 \r
1604         chunk.before = chunk.before.replace(regex, getLink);\r
1605 \r
1606         if (linkDef) {\r
1607             addDefNumber(linkDef);\r
1608         }\r
1609         else {\r
1610             chunk.selection = chunk.selection.replace(regex, getLink);\r
1611         }\r
1612 \r
1613         var refOut = refNumber;\r
1614 \r
1615         chunk.after = chunk.after.replace(regex, getLink);\r
1616 \r
1617         if (chunk.after) {\r
1618             chunk.after = chunk.after.replace(/\n*$/, "");\r
1619         }\r
1620         if (!chunk.after) {\r
1621             chunk.selection = chunk.selection.replace(/\n*$/, "");\r
1622         }\r
1623 \r
1624         chunk.after += "\n\n" + defs;\r
1625 \r
1626         return refOut;\r
1627     };\r
1628 \r
1629     // takes the line as entered into the add link/as image dialog and makes\r
1630     // sure the URL and the optinal title are "nice".\r
1631     function properlyEncoded(linkdef) {\r
1632         return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {\r
1633             link = link.replace(/\?.*$/, function (querypart) {\r
1634                 return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical\r
1635             });\r
1636             link = decodeURIComponent(link); // unencode first, to prevent double encoding\r
1637             link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');\r
1638             link = link.replace(/\?.*$/, function (querypart) {\r
1639                 return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded\r
1640             });\r
1641             if (title) {\r
1642                 title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");\r
1643                 title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/</g, "&lt;").replace(/>/g, "&gt;");\r
1644             }\r
1645             return title ? link + ' "' + title + '"' : link;\r
1646         });\r
1647     }\r
1648 \r
1649     commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {\r
1650 \r
1651         chunk.trimWhitespace();\r
1652         chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);\r
1653         var background;\r
1654 \r
1655         if (chunk.endTag.length > 1) {\r
1656 \r
1657             chunk.startTag = chunk.startTag.replace(/!?\[/, "");\r
1658             chunk.endTag = "";\r
1659             this.addLinkDef(chunk, null);\r
1660 \r
1661         }\r
1662         else {\r
1663 \r
1664             if (/\n\n/.test(chunk.selection)) {\r
1665                 this.addLinkDef(chunk, null);\r
1666                 return;\r
1667             }\r
1668             var that = this;\r
1669             // The function to be executed when you enter a link and press OK or Cancel.\r
1670             // Marks up the link and adds the ref.\r
1671             var linkEnteredCallback = function (link) {\r
1672 \r
1673                 background.parentNode.removeChild(background);\r
1674 \r
1675                 if (link !== null) {\r
1676 \r
1677                     chunk.startTag = chunk.endTag = "";\r
1678                     var linkDef = " [999]: " + properlyEncoded(link);\r
1679 \r
1680                     var num = that.addLinkDef(chunk, linkDef);\r
1681                     chunk.startTag = isImage ? "![" : "[";\r
1682                     chunk.endTag = "][" + num + "]";\r
1683 \r
1684                     if (!chunk.selection) {\r
1685                         if (isImage) {\r
1686                             chunk.selection = "enter image description here";\r
1687                         }\r
1688                         else {\r
1689                             chunk.selection = "enter link description here";\r
1690                         }\r
1691                     }\r
1692                 }\r
1693                 postProcessing();\r
1694             };\r
1695 \r
1696             background = ui.createBackground();\r
1697 \r
1698             if (isImage) {\r
1699                 if (!this.hooks.insertImageDialog(linkEnteredCallback))\r
1700                     ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback);\r
1701             }\r
1702             else {\r
1703                 ui.prompt(linkDialogText, linkDefaultText, linkEnteredCallback);\r
1704             }\r
1705             return true;\r
1706         }\r
1707     };\r
1708 \r
1709     // When making a list, hitting shift-enter will put your cursor on the next line\r
1710     // at the current indent level.\r
1711     commandProto.doAutoindent = function (chunk, postProcessing) {\r
1712 \r
1713         var commandMgr = this;\r
1714 \r
1715         chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");\r
1716         chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");\r
1717         chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");\r
1718 \r
1719         if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {\r
1720             if (commandMgr.doList) {\r
1721                 commandMgr.doList(chunk);\r
1722             }\r
1723         }\r
1724         if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {\r
1725             if (commandMgr.doBlockquote) {\r
1726                 commandMgr.doBlockquote(chunk);\r
1727             }\r
1728         }\r
1729         if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {\r
1730             if (commandMgr.doCode) {\r
1731                 commandMgr.doCode(chunk);\r
1732             }\r
1733         }\r
1734     };\r
1735 \r
1736     commandProto.doBlockquote = function (chunk, postProcessing) {\r
1737 \r
1738         chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,\r
1739             function (totalMatch, newlinesBefore, text, newlinesAfter) {\r
1740                 chunk.before += newlinesBefore;\r
1741                 chunk.after = newlinesAfter + chunk.after;\r
1742                 return text;\r
1743             });\r
1744 \r
1745         chunk.before = chunk.before.replace(/(>[ \t]*)$/,\r
1746             function (totalMatch, blankLine) {\r
1747                 chunk.selection = blankLine + chunk.selection;\r
1748                 return "";\r
1749             });\r
1750 \r
1751         chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");\r
1752         chunk.selection = chunk.selection || "Blockquote";\r
1753 \r
1754         // The original code uses a regular expression to find out how much of the\r
1755         // text *directly before* the selection already was a blockquote:\r
1756 \r
1757         /*\r
1758         if (chunk.before) {\r
1759         chunk.before = chunk.before.replace(/\n?$/, "\n");\r
1760         }\r
1761         chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,\r
1762         function (totalMatch) {\r
1763         chunk.startTag = totalMatch;\r
1764         return "";\r
1765         });\r
1766         */\r
1767 \r
1768         // This comes down to:\r
1769         // Go backwards as many lines a possible, such that each line\r
1770         //  a) starts with ">", or\r
1771         //  b) is almost empty, except for whitespace, or\r
1772         //  c) is preceeded by an unbroken chain of non-empty lines\r
1773         //     leading up to a line that starts with ">" and at least one more character\r
1774         // and in addition\r
1775         //  d) at least one line fulfills a)\r
1776         //\r
1777         // Since this is essentially a backwards-moving regex, it's susceptible to\r
1778         // catstrophic backtracking and can cause the browser to hang;\r
1779         // see e.g. http://meta.stackoverflow.com/questions/9807.\r
1780         //\r
1781         // Hence we replaced this by a simple state machine that just goes through the\r
1782         // lines and checks for a), b), and c).\r
1783 \r
1784         var match = "",\r
1785             leftOver = "",\r
1786             line;\r
1787         if (chunk.before) {\r
1788             var lines = chunk.before.replace(/\n$/, "").split("\n");\r
1789             var inChain = false;\r
1790             for (var i = 0; i < lines.length; i++) {\r
1791                 var good = false;\r
1792                 line = lines[i];\r
1793                 inChain = inChain && line.length > 0; // c) any non-empty line continues the chain\r
1794                 if (/^>/.test(line)) {                // a)\r
1795                     good = true;\r
1796                     if (!inChain && line.length > 1)  // c) any line that starts with ">" and has at least one more character starts the chain\r
1797                         inChain = true;\r
1798                 } else if (/^[ \t]*$/.test(line)) {   // b)\r
1799                     good = true;\r
1800                 } else {\r
1801                     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
1802                 }\r
1803                 if (good) {\r
1804                     match += line + "\n";\r
1805                 } else {\r
1806                     leftOver += match + line;\r
1807                     match = "\n";\r
1808                 }\r
1809             }\r
1810             if (!/(^|\n)>/.test(match)) {             // d)\r
1811                 leftOver += match;\r
1812                 match = "";\r
1813             }\r
1814         }\r
1815 \r
1816         chunk.startTag = match;\r
1817         chunk.before = leftOver;\r
1818 \r
1819         // end of change\r
1820 \r
1821         if (chunk.after) {\r
1822             chunk.after = chunk.after.replace(/^\n?/, "\n");\r
1823         }\r
1824 \r
1825         chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,\r
1826             function (totalMatch) {\r
1827                 chunk.endTag = totalMatch;\r
1828                 return "";\r
1829             }\r
1830         );\r
1831 \r
1832         var replaceBlanksInTags = function (useBracket) {\r
1833 \r
1834             var replacement = useBracket ? "> " : "";\r
1835 \r
1836             if (chunk.startTag) {\r
1837                 chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,\r
1838                     function (totalMatch, markdown) {\r
1839                         return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";\r
1840                     });\r
1841             }\r
1842             if (chunk.endTag) {\r
1843                 chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,\r
1844                     function (totalMatch, markdown) {\r
1845                         return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";\r
1846                     });\r
1847             }\r
1848         };\r
1849 \r
1850         if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {\r
1851             this.wrap(chunk, SETTINGS.lineLength - 2);\r
1852             chunk.selection = chunk.selection.replace(/^/gm, "> ");\r
1853             replaceBlanksInTags(true);\r
1854             chunk.skipLines();\r
1855         } else {\r
1856             chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");\r
1857             this.unwrap(chunk);\r
1858             replaceBlanksInTags(false);\r
1859 \r
1860             if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {\r
1861                 chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");\r
1862             }\r
1863 \r
1864             if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {\r
1865                 chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");\r
1866             }\r
1867         }\r
1868 \r
1869         chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);\r
1870 \r
1871         if (!/\n/.test(chunk.selection)) {\r
1872             chunk.selection = chunk.selection.replace(/^(> *)/,\r
1873             function (wholeMatch, blanks) {\r
1874                 chunk.startTag += blanks;\r
1875                 return "";\r
1876             });\r
1877         }\r
1878     };\r
1879 \r
1880     commandProto.doCode = function (chunk, postProcessing) {\r
1881 \r
1882         var hasTextBefore = /\S[ ]*$/.test(chunk.before);\r
1883         var hasTextAfter = /^[ ]*\S/.test(chunk.after);\r
1884 \r
1885         // Use 'four space' markdown if the selection is on its own\r
1886         // line or is multiline.\r
1887         if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {\r
1888 \r
1889             chunk.before = chunk.before.replace(/[ ]{4}$/,\r
1890                 function (totalMatch) {\r
1891                     chunk.selection = totalMatch + chunk.selection;\r
1892                     return "";\r
1893                 });\r
1894 \r
1895             var nLinesBack = 1;\r
1896             var nLinesForward = 1;\r
1897 \r
1898             if (/\n(\t|[ ]{4,}).*\n$/.test(chunk.before)) {\r
1899                 nLinesBack = 0;\r
1900             }\r
1901             if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {\r
1902                 nLinesForward = 0;\r
1903             }\r
1904 \r
1905             chunk.skipLines(nLinesBack, nLinesForward);\r
1906 \r
1907             if (!chunk.selection) {\r
1908                 chunk.startTag = "    ";\r
1909                 chunk.selection = "enter code here";\r
1910             }\r
1911             else {\r
1912                 if (/^[ ]{0,3}\S/m.test(chunk.selection)) {\r
1913                     chunk.selection = chunk.selection.replace(/^/gm, "    ");\r
1914                 }\r
1915                 else {\r
1916                     chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");\r
1917                 }\r
1918             }\r
1919         }\r
1920         else {\r
1921             // Use backticks (`) to delimit the code block.\r
1922 \r
1923             chunk.trimWhitespace();\r
1924             chunk.findTags(/`/, /`/);\r
1925 \r
1926             if (!chunk.startTag && !chunk.endTag) {\r
1927                 chunk.startTag = chunk.endTag = "`";\r
1928                 if (!chunk.selection) {\r
1929                     chunk.selection = "enter code here";\r
1930                 }\r
1931             }\r
1932             else if (chunk.endTag && !chunk.startTag) {\r
1933                 chunk.before += chunk.endTag;\r
1934                 chunk.endTag = "";\r
1935             }\r
1936             else {\r
1937                 chunk.startTag = chunk.endTag = "";\r
1938             }\r
1939         }\r
1940     };\r
1941 \r
1942     commandProto.doList = function (chunk, postProcessing, isNumberedList) {\r
1943 \r
1944         // These are identical except at the very beginning and end.\r
1945         // Should probably use the regex extension function to make this clearer.\r
1946         var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;\r
1947         var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;\r
1948 \r
1949         // The default bullet is a dash but others are possible.\r
1950         // This has nothing to do with the particular HTML bullet,\r
1951         // it's just a markdown bullet.\r
1952         var bullet = "-";\r
1953 \r
1954         // The number in a numbered list.\r
1955         var num = 1;\r
1956 \r
1957         // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.\r
1958         var getItemPrefix = function () {\r
1959             var prefix;\r
1960             if (isNumberedList) {\r
1961                 prefix = " " + num + ". ";\r
1962                 num++;\r
1963             }\r
1964             else {\r
1965                 prefix = " " + bullet + " ";\r
1966             }\r
1967             return prefix;\r
1968         };\r
1969 \r
1970         // Fixes the prefixes of the other list items.\r
1971         var getPrefixedItem = function (itemText) {\r
1972 \r
1973             // The numbering flag is unset when called by autoindent.\r
1974             if (isNumberedList === undefined) {\r
1975                 isNumberedList = /^\s*\d/.test(itemText);\r
1976             }\r
1977 \r
1978             // Renumber/bullet the list element.\r
1979             itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,\r
1980                 function (_) {\r
1981                     return getItemPrefix();\r
1982                 });\r
1983 \r
1984             return itemText;\r
1985         };\r
1986 \r
1987         chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);\r
1988 \r
1989         if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {\r
1990             chunk.before += chunk.startTag;\r
1991             chunk.startTag = "";\r
1992         }\r
1993 \r
1994         if (chunk.startTag) {\r
1995 \r
1996             var hasDigits = /\d+[.]/.test(chunk.startTag);\r
1997             chunk.startTag = "";\r
1998             chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");\r
1999             this.unwrap(chunk);\r
2000             chunk.skipLines();\r
2001 \r
2002             if (hasDigits) {\r
2003                 // Have to renumber the bullet points if this is a numbered list.\r
2004                 chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);\r
2005             }\r
2006             if (isNumberedList == hasDigits) {\r
2007                 return;\r
2008             }\r
2009         }\r
2010 \r
2011         var nLinesUp = 1;\r
2012 \r
2013         chunk.before = chunk.before.replace(previousItemsRegex,\r
2014             function (itemText) {\r
2015                 if (/^\s*([*+-])/.test(itemText)) {\r
2016                     bullet = re.$1;\r
2017                 }\r
2018                 nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;\r
2019                 return getPrefixedItem(itemText);\r
2020             });\r
2021 \r
2022         if (!chunk.selection) {\r
2023             chunk.selection = "List item";\r
2024         }\r
2025 \r
2026         var prefix = getItemPrefix();\r
2027 \r
2028         var nLinesDown = 1;\r
2029 \r
2030         chunk.after = chunk.after.replace(nextItemsRegex,\r
2031             function (itemText) {\r
2032                 nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;\r
2033                 return getPrefixedItem(itemText);\r
2034             });\r
2035 \r
2036         chunk.trimWhitespace(true);\r
2037         chunk.skipLines(nLinesUp, nLinesDown, true);\r
2038         chunk.startTag = prefix;\r
2039         var spaces = prefix.replace(/./g, " ");\r
2040         this.wrap(chunk, SETTINGS.lineLength - spaces.length);\r
2041         chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);\r
2042 \r
2043     };\r
2044 \r
2045     commandProto.doHeading = function (chunk, postProcessing) {\r
2046 \r
2047         // Remove leading/trailing whitespace and reduce internal spaces to single spaces.\r
2048         chunk.selection = chunk.selection.replace(/\s+/g, " ");\r
2049         chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");\r
2050 \r
2051         // If we clicked the button with no selected text, we just\r
2052         // make a level 2 hash header around some default text.\r
2053         if (!chunk.selection) {\r
2054             chunk.startTag = "## ";\r
2055             chunk.selection = "Heading";\r
2056             chunk.endTag = " ##";\r
2057             return;\r
2058         }\r
2059 \r
2060         var headerLevel = 0;     // The existing header level of the selected text.\r
2061 \r
2062         // Remove any existing hash heading markdown and save the header level.\r
2063         chunk.findTags(/#+[ ]*/, /[ ]*#+/);\r
2064         if (/#+/.test(chunk.startTag)) {\r
2065             headerLevel = re.lastMatch.length;\r
2066         }\r
2067         chunk.startTag = chunk.endTag = "";\r
2068 \r
2069         // Try to get the current header level by looking for - and = in the line\r
2070         // below the selection.\r
2071         chunk.findTags(null, /\s?(-+|=+)/);\r
2072         if (/=+/.test(chunk.endTag)) {\r
2073             headerLevel = 1;\r
2074         }\r
2075         if (/-+/.test(chunk.endTag)) {\r
2076             headerLevel = 2;\r
2077         }\r
2078 \r
2079         // Skip to the next line so we can create the header markdown.\r
2080         chunk.startTag = chunk.endTag = "";\r
2081         chunk.skipLines(1, 1);\r
2082 \r
2083         // We make a level 2 header if there is no current header.\r
2084         // If there is a header level, we substract one from the header level.\r
2085         // If it's already a level 1 header, it's removed.\r
2086         var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;\r
2087 \r
2088         if (headerLevelToCreate > 0) {\r
2089 \r
2090             // The button only creates level 1 and 2 underline headers.\r
2091             // Why not have it iterate over hash header levels?  Wouldn't that be easier and cleaner?\r
2092             var headerChar = headerLevelToCreate >= 2 ? "-" : "=";\r
2093             var len = chunk.selection.length;\r
2094             if (len > SETTINGS.lineLength) {\r
2095                 len = SETTINGS.lineLength;\r
2096             }\r
2097             chunk.endTag = "\n";\r
2098             while (len--) {\r
2099                 chunk.endTag += headerChar;\r
2100             }\r
2101         }\r
2102     };\r
2103 \r
2104     commandProto.doHorizontalRule = function (chunk, postProcessing) {\r
2105         chunk.startTag = "----------\n";\r
2106         chunk.selection = "";\r
2107         chunk.skipLines(2, 1, true);\r
2108     }\r
2109 \r
2110 \r
2111 })();