comparison static/js/pagedown/Markdown.Editor.js @ 60:8250c977bc50

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