simple_form_markdown_editor_bootstrap 0.0.1

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