bootstrap_pagedown 1.0.0 → 1.0.1

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