pagedown-rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2160 @@
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><b>Insert Hyperlink</b></p><p>http://example.com/ \"optional title\"</p>";
31
+ var imageDialogText = "<p><b>Insert Image</b></p><p>http://example.com/images/diagram.jpg \"optional title\"<br><br>Need <a href='http://www.google.com/search?q=free+image+hosting' target='_blank'>free image hosting?</a></p>";
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
+ // Creates the background behind the hyperlink text entry box.
968
+ // And download dialog
969
+ // Most of this has been moved to CSS but the div creation and
970
+ // browser-specific hacks remain here.
971
+ ui.createBackground = function () {
972
+
973
+ var background = doc.createElement("div"),
974
+ style = background.style;
975
+
976
+ background.className = "wmd-prompt-background";
977
+
978
+ style.position = "absolute";
979
+ style.top = "0";
980
+
981
+ style.zIndex = "1000";
982
+
983
+ if (uaSniffed.isIE) {
984
+ style.filter = "alpha(opacity=50)";
985
+ }
986
+ else {
987
+ style.opacity = "0.5";
988
+ }
989
+
990
+ var pageSize = position.getPageSize();
991
+ style.height = pageSize[1] + "px";
992
+
993
+ if (uaSniffed.isIE) {
994
+ style.left = doc.documentElement.scrollLeft;
995
+ style.width = doc.documentElement.clientWidth;
996
+ }
997
+ else {
998
+ style.left = "0";
999
+ style.width = "100%";
1000
+ }
1001
+
1002
+ doc.body.appendChild(background);
1003
+ return background;
1004
+ };
1005
+
1006
+ // This simulates a modal dialog box and asks for the URL when you
1007
+ // click the hyperlink or image buttons.
1008
+ //
1009
+ // text: The html for the input box.
1010
+ // defaultInputText: The default value that appears in the input box.
1011
+ // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
1012
+ // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
1013
+ // was chosen).
1014
+ ui.prompt = function (text, defaultInputText, callback) {
1015
+
1016
+ // These variables need to be declared at this level since they are used
1017
+ // in multiple functions.
1018
+ var dialog; // The dialog box.
1019
+ var input; // The text box where you enter the hyperlink.
1020
+
1021
+
1022
+ if (defaultInputText === undefined) {
1023
+ defaultInputText = "";
1024
+ }
1025
+
1026
+ // Used as a keydown event handler. Esc dismisses the prompt.
1027
+ // Key code 27 is ESC.
1028
+ var checkEscape = function (key) {
1029
+ var code = (key.charCode || key.keyCode);
1030
+ if (code === 27) {
1031
+ close(true);
1032
+ }
1033
+ };
1034
+
1035
+ // Dismisses the hyperlink input box.
1036
+ // isCancel is true if we don't care about the input text.
1037
+ // isCancel is false if we are going to keep the text.
1038
+ var close = function (isCancel) {
1039
+ util.removeEvent(doc.body, "keydown", checkEscape);
1040
+ var text = input.value;
1041
+
1042
+ if (isCancel) {
1043
+ text = null;
1044
+ }
1045
+ else {
1046
+ // Fixes common pasting errors.
1047
+ text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
1048
+ if (!/^(?:https?|ftp):\/\//.test(text))
1049
+ text = 'http://' + text;
1050
+ }
1051
+
1052
+ dialog.parentNode.removeChild(dialog);
1053
+
1054
+ callback(text);
1055
+ return false;
1056
+ };
1057
+
1058
+
1059
+
1060
+ // Create the text input box form/window.
1061
+ var createDialog = function () {
1062
+
1063
+ // The main dialog box.
1064
+ dialog = doc.createElement("div");
1065
+ dialog.className = "wmd-prompt-dialog";
1066
+ dialog.style.padding = "10px;";
1067
+ dialog.style.position = "fixed";
1068
+ dialog.style.width = "400px";
1069
+ dialog.style.zIndex = "1001";
1070
+
1071
+ // The dialog text.
1072
+ var question = doc.createElement("div");
1073
+ question.innerHTML = text;
1074
+ question.style.padding = "5px";
1075
+ dialog.appendChild(question);
1076
+
1077
+ // The web form container for the text box and buttons.
1078
+ var form = doc.createElement("form"),
1079
+ style = form.style;
1080
+ form.onsubmit = function () { return close(false); };
1081
+ style.padding = "0";
1082
+ style.margin = "0";
1083
+ style.cssFloat = "left";
1084
+ style.width = "100%";
1085
+ style.textAlign = "center";
1086
+ style.position = "relative";
1087
+ dialog.appendChild(form);
1088
+
1089
+ // The input text box
1090
+ input = doc.createElement("input");
1091
+ input.type = "text";
1092
+ input.value = defaultInputText;
1093
+ style = input.style;
1094
+ style.display = "block";
1095
+ style.width = "80%";
1096
+ style.marginLeft = style.marginRight = "auto";
1097
+ form.appendChild(input);
1098
+
1099
+ // The ok button
1100
+ var okButton = doc.createElement("input");
1101
+ okButton.type = "button";
1102
+ okButton.onclick = function () { return close(false); };
1103
+ okButton.value = "OK";
1104
+ style = okButton.style;
1105
+ style.margin = "10px";
1106
+ style.display = "inline";
1107
+ style.width = "7em";
1108
+
1109
+
1110
+ // The cancel button
1111
+ var cancelButton = doc.createElement("input");
1112
+ cancelButton.type = "button";
1113
+ cancelButton.onclick = function () { return close(true); };
1114
+ cancelButton.value = "Cancel";
1115
+ style = cancelButton.style;
1116
+ style.margin = "10px";
1117
+ style.display = "inline";
1118
+ style.width = "7em";
1119
+
1120
+ form.appendChild(okButton);
1121
+ form.appendChild(cancelButton);
1122
+
1123
+ util.addEvent(doc.body, "keydown", checkEscape);
1124
+ dialog.style.top = "50%";
1125
+ dialog.style.left = "50%";
1126
+ dialog.style.display = "block";
1127
+ if (uaSniffed.isIE_5or6) {
1128
+ dialog.style.position = "absolute";
1129
+ dialog.style.top = doc.documentElement.scrollTop + 200 + "px";
1130
+ dialog.style.left = "50%";
1131
+ }
1132
+ doc.body.appendChild(dialog);
1133
+
1134
+ // This has to be done AFTER adding the dialog to the form if you
1135
+ // want it to be centered.
1136
+ dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";
1137
+ dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";
1138
+
1139
+ };
1140
+
1141
+ // Why is this in a zero-length timeout?
1142
+ // Is it working around a browser bug?
1143
+ setTimeout(function () {
1144
+
1145
+ createDialog();
1146
+
1147
+ var defTextLen = defaultInputText.length;
1148
+ if (input.selectionStart !== undefined) {
1149
+ input.selectionStart = 0;
1150
+ input.selectionEnd = defTextLen;
1151
+ }
1152
+ else if (input.createTextRange) {
1153
+ var range = input.createTextRange();
1154
+ range.collapse(false);
1155
+ range.moveStart("character", -defTextLen);
1156
+ range.moveEnd("character", defTextLen);
1157
+ range.select();
1158
+ }
1159
+
1160
+ input.focus();
1161
+ }, 0);
1162
+ };
1163
+
1164
+ function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) {
1165
+
1166
+ var inputBox = panels.input,
1167
+ buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
1168
+
1169
+ makeSpritedButtonRow();
1170
+
1171
+ var keyEvent = "keydown";
1172
+ if (uaSniffed.isOpera) {
1173
+ keyEvent = "keypress";
1174
+ }
1175
+
1176
+ util.addEvent(inputBox, keyEvent, function (key) {
1177
+
1178
+ // Check to see if we have a button key and, if so execute the callback.
1179
+ if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) {
1180
+
1181
+ var keyCode = key.charCode || key.keyCode;
1182
+ var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
1183
+
1184
+ switch (keyCodeStr) {
1185
+ case "b":
1186
+ doClick(buttons.bold);
1187
+ break;
1188
+ case "i":
1189
+ doClick(buttons.italic);
1190
+ break;
1191
+ case "l":
1192
+ doClick(buttons.link);
1193
+ break;
1194
+ case "q":
1195
+ doClick(buttons.quote);
1196
+ break;
1197
+ case "k":
1198
+ doClick(buttons.code);
1199
+ break;
1200
+ case "g":
1201
+ doClick(buttons.image);
1202
+ break;
1203
+ case "o":
1204
+ doClick(buttons.olist);
1205
+ break;
1206
+ case "u":
1207
+ doClick(buttons.ulist);
1208
+ break;
1209
+ case "h":
1210
+ doClick(buttons.heading);
1211
+ break;
1212
+ case "r":
1213
+ doClick(buttons.hr);
1214
+ break;
1215
+ case "y":
1216
+ doClick(buttons.redo);
1217
+ break;
1218
+ case "z":
1219
+ if (key.shiftKey) {
1220
+ doClick(buttons.redo);
1221
+ }
1222
+ else {
1223
+ doClick(buttons.undo);
1224
+ }
1225
+ break;
1226
+ default:
1227
+ return;
1228
+ }
1229
+
1230
+
1231
+ if (key.preventDefault) {
1232
+ key.preventDefault();
1233
+ }
1234
+
1235
+ if (window.event) {
1236
+ window.event.returnValue = false;
1237
+ }
1238
+ }
1239
+ });
1240
+
1241
+ // Auto-indent on shift-enter
1242
+ util.addEvent(inputBox, "keyup", function (key) {
1243
+ if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
1244
+ var keyCode = key.charCode || key.keyCode;
1245
+ // Character 13 is Enter
1246
+ if (keyCode === 13) {
1247
+ var fakeButton = {};
1248
+ fakeButton.textOp = bindCommand("doAutoindent");
1249
+ doClick(fakeButton);
1250
+ }
1251
+ }
1252
+ });
1253
+
1254
+ // special handler because IE clears the context of the textbox on ESC
1255
+ if (uaSniffed.isIE) {
1256
+ util.addEvent(inputBox, "keydown", function (key) {
1257
+ var code = key.keyCode;
1258
+ if (code === 27) {
1259
+ return false;
1260
+ }
1261
+ });
1262
+ }
1263
+
1264
+
1265
+ // Perform the button's action.
1266
+ function doClick(button) {
1267
+
1268
+ inputBox.focus();
1269
+
1270
+ if (button.textOp) {
1271
+
1272
+ if (undoManager) {
1273
+ undoManager.setCommandMode();
1274
+ }
1275
+
1276
+ var state = new TextareaState(panels);
1277
+
1278
+ if (!state) {
1279
+ return;
1280
+ }
1281
+
1282
+ var chunks = state.getChunks();
1283
+
1284
+ // Some commands launch a "modal" prompt dialog. Javascript
1285
+ // can't really make a modal dialog box and the WMD code
1286
+ // will continue to execute while the dialog is displayed.
1287
+ // This prevents the dialog pattern I'm used to and means
1288
+ // I can't do something like this:
1289
+ //
1290
+ // var link = CreateLinkDialog();
1291
+ // makeMarkdownLink(link);
1292
+ //
1293
+ // Instead of this straightforward method of handling a
1294
+ // dialog I have to pass any code which would execute
1295
+ // after the dialog is dismissed (e.g. link creation)
1296
+ // in a function parameter.
1297
+ //
1298
+ // Yes this is awkward and I think it sucks, but there's
1299
+ // no real workaround. Only the image and link code
1300
+ // create dialogs and require the function pointers.
1301
+ var fixupInputArea = function () {
1302
+
1303
+ inputBox.focus();
1304
+
1305
+ if (chunks) {
1306
+ state.setChunks(chunks);
1307
+ }
1308
+
1309
+ state.restore();
1310
+ previewManager.refresh();
1311
+ };
1312
+
1313
+ var noCleanup = button.textOp(chunks, fixupInputArea);
1314
+
1315
+ if (!noCleanup) {
1316
+ fixupInputArea();
1317
+ }
1318
+
1319
+ }
1320
+
1321
+ if (button.execute) {
1322
+ button.execute(undoManager);
1323
+ }
1324
+ };
1325
+
1326
+ function setupButton(button, isEnabled) {
1327
+
1328
+ var normalYShift = "0px";
1329
+ var disabledYShift = "-20px";
1330
+ var highlightYShift = "-40px";
1331
+ var image = button.getElementsByTagName("span")[0];
1332
+ if (isEnabled) {
1333
+ image.style.backgroundPosition = button.XShift + " " + normalYShift;
1334
+ button.onmouseover = function () {
1335
+ image.style.backgroundPosition = this.XShift + " " + highlightYShift;
1336
+ };
1337
+
1338
+ button.onmouseout = function () {
1339
+ image.style.backgroundPosition = this.XShift + " " + normalYShift;
1340
+ };
1341
+
1342
+ // IE tries to select the background image "button" text (it's
1343
+ // implemented in a list item) so we have to cache the selection
1344
+ // on mousedown.
1345
+ if (uaSniffed.isIE) {
1346
+ button.onmousedown = function () {
1347
+ if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection
1348
+ return;
1349
+ }
1350
+ panels.ieCachedRange = document.selection.createRange();
1351
+ panels.ieCachedScrollTop = panels.input.scrollTop;
1352
+ };
1353
+ }
1354
+
1355
+ if (!button.isHelp) {
1356
+ button.onclick = function () {
1357
+ if (this.onmouseout) {
1358
+ this.onmouseout();
1359
+ }
1360
+ doClick(this);
1361
+ return false;
1362
+ }
1363
+ }
1364
+ }
1365
+ else {
1366
+ image.style.backgroundPosition = button.XShift + " " + disabledYShift;
1367
+ button.onmouseover = button.onmouseout = button.onclick = function () { };
1368
+ }
1369
+ }
1370
+
1371
+ function bindCommand(method) {
1372
+ if (typeof method === "string")
1373
+ method = commandManager[method];
1374
+ return function () { method.apply(commandManager, arguments); }
1375
+ }
1376
+
1377
+ function makeSpritedButtonRow() {
1378
+
1379
+ var buttonBar = panels.buttonBar;
1380
+
1381
+ var normalYShift = "0px";
1382
+ var disabledYShift = "-20px";
1383
+ var highlightYShift = "-40px";
1384
+
1385
+ var buttonRow = document.createElement("ul");
1386
+ buttonRow.id = "wmd-button-row" + postfix;
1387
+ buttonRow.className = 'wmd-button-row';
1388
+ buttonRow = buttonBar.appendChild(buttonRow);
1389
+ var xPosition = 0;
1390
+ var makeButton = function (id, title, XShift, textOp) {
1391
+ var button = document.createElement("li");
1392
+ button.className = "wmd-button";
1393
+ button.style.left = xPosition + "px";
1394
+ xPosition += 25;
1395
+ var buttonImage = document.createElement("span");
1396
+ button.id = id + postfix;
1397
+ button.appendChild(buttonImage);
1398
+ button.title = title;
1399
+ button.XShift = XShift;
1400
+ if (textOp)
1401
+ button.textOp = textOp;
1402
+ setupButton(button, true);
1403
+ buttonRow.appendChild(button);
1404
+ return button;
1405
+ };
1406
+ var makeSpacer = function (num) {
1407
+ var spacer = document.createElement("li");
1408
+ spacer.className = "wmd-spacer wmd-spacer" + num;
1409
+ spacer.id = "wmd-spacer" + num + postfix;
1410
+ buttonRow.appendChild(spacer);
1411
+ xPosition += 25;
1412
+ }
1413
+
1414
+ buttons.bold = makeButton("wmd-bold-button", "Strong <strong> Ctrl+B", "0px", bindCommand("doBold"));
1415
+ buttons.italic = makeButton("wmd-italic-button", "Emphasis <em> Ctrl+I", "-20px", bindCommand("doItalic"));
1416
+ makeSpacer(1);
1417
+ buttons.link = makeButton("wmd-link-button", "Hyperlink <a> Ctrl+L", "-40px", bindCommand(function (chunk, postProcessing) {
1418
+ return this.doLinkOrImage(chunk, postProcessing, false);
1419
+ }));
1420
+ buttons.quote = makeButton("wmd-quote-button", "Blockquote <blockquote> Ctrl+Q", "-60px", bindCommand("doBlockquote"));
1421
+ buttons.code = makeButton("wmd-code-button", "Code Sample <pre><code> Ctrl+K", "-80px", bindCommand("doCode"));
1422
+ buttons.image = makeButton("wmd-image-button", "Image <img> Ctrl+G", "-100px", bindCommand(function (chunk, postProcessing) {
1423
+ return this.doLinkOrImage(chunk, postProcessing, true);
1424
+ }));
1425
+ makeSpacer(2);
1426
+ buttons.olist = makeButton("wmd-olist-button", "Numbered List <ol> Ctrl+O", "-120px", bindCommand(function (chunk, postProcessing) {
1427
+ this.doList(chunk, postProcessing, true);
1428
+ }));
1429
+ buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List <ul> Ctrl+U", "-140px", bindCommand(function (chunk, postProcessing) {
1430
+ this.doList(chunk, postProcessing, false);
1431
+ }));
1432
+ buttons.heading = makeButton("wmd-heading-button", "Heading <h1>/<h2> Ctrl+H", "-160px", bindCommand("doHeading"));
1433
+ buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule <hr> Ctrl+R", "-180px", bindCommand("doHorizontalRule"));
1434
+ makeSpacer(3);
1435
+ buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "-200px", null);
1436
+ buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
1437
+
1438
+ var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
1439
+ "Redo - Ctrl+Y" :
1440
+ "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms
1441
+
1442
+ buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null);
1443
+ buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
1444
+
1445
+ if (helpOptions) {
1446
+ var helpButton = document.createElement("li");
1447
+ var helpButtonImage = document.createElement("span");
1448
+ helpButton.appendChild(helpButtonImage);
1449
+ helpButton.className = "wmd-button wmd-help-button";
1450
+ helpButton.id = "wmd-help-button" + postfix;
1451
+ helpButton.XShift = "-240px";
1452
+ helpButton.isHelp = true;
1453
+ helpButton.style.right = "0px";
1454
+ helpButton.title = helpOptions.title || defaultHelpHoverTitle;
1455
+ helpButton.onclick = helpOptions.handler;
1456
+
1457
+ setupButton(helpButton, true);
1458
+ buttonRow.appendChild(helpButton);
1459
+ buttons.help = helpButton;
1460
+ }
1461
+
1462
+ setUndoRedoButtonStates();
1463
+ }
1464
+
1465
+ function setUndoRedoButtonStates() {
1466
+ if (undoManager) {
1467
+ setupButton(buttons.undo, undoManager.canUndo());
1468
+ setupButton(buttons.redo, undoManager.canRedo());
1469
+ }
1470
+ };
1471
+
1472
+ this.setUndoRedoButtonStates = setUndoRedoButtonStates;
1473
+
1474
+ }
1475
+
1476
+ function CommandManager(pluginHooks) {
1477
+ this.hooks = pluginHooks;
1478
+ }
1479
+
1480
+ var commandProto = CommandManager.prototype;
1481
+
1482
+ // The markdown symbols - 4 spaces = code, > = blockquote, etc.
1483
+ commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
1484
+
1485
+ // Remove markdown symbols from the chunk selection.
1486
+ commandProto.unwrap = function (chunk) {
1487
+ var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");
1488
+ chunk.selection = chunk.selection.replace(txt, "$1 $2");
1489
+ };
1490
+
1491
+ commandProto.wrap = function (chunk, len) {
1492
+ this.unwrap(chunk);
1493
+ var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"),
1494
+ that = this;
1495
+
1496
+ chunk.selection = chunk.selection.replace(regex, function (line, marked) {
1497
+ if (new re("^" + that.prefixes, "").test(line)) {
1498
+ return line;
1499
+ }
1500
+ return marked + "\n";
1501
+ });
1502
+
1503
+ chunk.selection = chunk.selection.replace(/\s+$/, "");
1504
+ };
1505
+
1506
+ commandProto.doBold = function (chunk, postProcessing) {
1507
+ return this.doBorI(chunk, postProcessing, 2, "strong text");
1508
+ };
1509
+
1510
+ commandProto.doItalic = function (chunk, postProcessing) {
1511
+ return this.doBorI(chunk, postProcessing, 1, "emphasized text");
1512
+ };
1513
+
1514
+ // chunk: The selected region that will be enclosed with */**
1515
+ // nStars: 1 for italics, 2 for bold
1516
+ // insertText: If you just click the button without highlighting text, this gets inserted
1517
+ commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {
1518
+
1519
+ // Get rid of whitespace and fixup newlines.
1520
+ chunk.trimWhitespace();
1521
+ chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
1522
+
1523
+ // Look for stars before and after. Is the chunk already marked up?
1524
+ // note that these regex matches cannot fail
1525
+ var starsBefore = /(\**$)/.exec(chunk.before)[0];
1526
+ var starsAfter = /(^\**)/.exec(chunk.after)[0];
1527
+
1528
+ var prevStars = Math.min(starsBefore.length, starsAfter.length);
1529
+
1530
+ // Remove stars if we have to since the button acts as a toggle.
1531
+ if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
1532
+ chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
1533
+ chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
1534
+ }
1535
+ else if (!chunk.selection && starsAfter) {
1536
+ // It's not really clear why this code is necessary. It just moves
1537
+ // some arbitrary stuff around.
1538
+ chunk.after = chunk.after.replace(/^([*_]*)/, "");
1539
+ chunk.before = chunk.before.replace(/(\s?)$/, "");
1540
+ var whitespace = re.$1;
1541
+ chunk.before = chunk.before + starsAfter + whitespace;
1542
+ }
1543
+ else {
1544
+
1545
+ // In most cases, if you don't have any selected text and click the button
1546
+ // you'll get a selected, marked up region with the default text inserted.
1547
+ if (!chunk.selection && !starsAfter) {
1548
+ chunk.selection = insertText;
1549
+ }
1550
+
1551
+ // Add the true markup.
1552
+ var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
1553
+ chunk.before = chunk.before + markup;
1554
+ chunk.after = markup + chunk.after;
1555
+ }
1556
+
1557
+ return;
1558
+ };
1559
+
1560
+ commandProto.stripLinkDefs = function (text, defsToAdd) {
1561
+
1562
+ text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,
1563
+ function (totalMatch, id, link, newlines, title) {
1564
+ defsToAdd[id] = totalMatch.replace(/\s*$/, "");
1565
+ if (newlines) {
1566
+ // Strip the title and return that separately.
1567
+ defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
1568
+ return newlines + title;
1569
+ }
1570
+ return "";
1571
+ });
1572
+
1573
+ return text;
1574
+ };
1575
+
1576
+ commandProto.addLinkDef = function (chunk, linkDef) {
1577
+
1578
+ var refNumber = 0; // The current reference number
1579
+ var defsToAdd = {}; //
1580
+ // Start with a clean slate by removing all previous link definitions.
1581
+ chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);
1582
+ chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);
1583
+ chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);
1584
+
1585
+ var defs = "";
1586
+ var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
1587
+
1588
+ var addDefNumber = function (def) {
1589
+ refNumber++;
1590
+ def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:");
1591
+ defs += "\n" + def;
1592
+ };
1593
+
1594
+ // note that
1595
+ // a) the recursive call to getLink cannot go infinite, because by definition
1596
+ // of regex, inner is always a proper substring of wholeMatch, and
1597
+ // b) more than one level of nesting is neither supported by the regex
1598
+ // nor making a lot of sense (the only use case for nesting is a linked image)
1599
+ var getLink = function (wholeMatch, before, inner, afterInner, id, end) {
1600
+ inner = inner.replace(regex, getLink);
1601
+ if (defsToAdd[id]) {
1602
+ addDefNumber(defsToAdd[id]);
1603
+ return before + inner + afterInner + refNumber + end;
1604
+ }
1605
+ return wholeMatch;
1606
+ };
1607
+
1608
+ chunk.before = chunk.before.replace(regex, getLink);
1609
+
1610
+ if (linkDef) {
1611
+ addDefNumber(linkDef);
1612
+ }
1613
+ else {
1614
+ chunk.selection = chunk.selection.replace(regex, getLink);
1615
+ }
1616
+
1617
+ var refOut = refNumber;
1618
+
1619
+ chunk.after = chunk.after.replace(regex, getLink);
1620
+
1621
+ if (chunk.after) {
1622
+ chunk.after = chunk.after.replace(/\n*$/, "");
1623
+ }
1624
+ if (!chunk.after) {
1625
+ chunk.selection = chunk.selection.replace(/\n*$/, "");
1626
+ }
1627
+
1628
+ chunk.after += "\n\n" + defs;
1629
+
1630
+ return refOut;
1631
+ };
1632
+
1633
+ // takes the line as entered into the add link/as image dialog and makes
1634
+ // sure the URL and the optinal title are "nice".
1635
+ function properlyEncoded(linkdef) {
1636
+ return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
1637
+ link = link.replace(/\?.*$/, function (querypart) {
1638
+ return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical
1639
+ });
1640
+ link = decodeURIComponent(link); // unencode first, to prevent double encoding
1641
+ link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
1642
+ link = link.replace(/\?.*$/, function (querypart) {
1643
+ return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded
1644
+ });
1645
+ if (title) {
1646
+ title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");
1647
+ title = title.replace(/"/g, "quot;").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1648
+ }
1649
+ return title ? link + ' "' + title + '"' : link;
1650
+ });
1651
+ }
1652
+
1653
+ commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
1654
+
1655
+ chunk.trimWhitespace();
1656
+ chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
1657
+ var background;
1658
+
1659
+ if (chunk.endTag.length > 1 && chunk.startTag.length > 0) {
1660
+
1661
+ chunk.startTag = chunk.startTag.replace(/!?\[/, "");
1662
+ chunk.endTag = "";
1663
+ this.addLinkDef(chunk, null);
1664
+
1665
+ }
1666
+ else {
1667
+
1668
+ // We're moving start and end tag back into the selection, since (as we're in the else block) we're not
1669
+ // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the
1670
+ // link text. linkEnteredCallback takes care of escaping any brackets.
1671
+ chunk.selection = chunk.startTag + chunk.selection + chunk.endTag;
1672
+ chunk.startTag = chunk.endTag = "";
1673
+
1674
+ if (/\n\n/.test(chunk.selection)) {
1675
+ this.addLinkDef(chunk, null);
1676
+ return;
1677
+ }
1678
+ var that = this;
1679
+ // The function to be executed when you enter a link and press OK or Cancel.
1680
+ // Marks up the link and adds the ref.
1681
+ var linkEnteredCallback = function (link) {
1682
+
1683
+ background.parentNode.removeChild(background);
1684
+
1685
+ if (link !== null) {
1686
+ // ( $1
1687
+ // [^\\] anything that's not a backslash
1688
+ // (?:\\\\)* an even number (this includes zero) of backslashes
1689
+ // )
1690
+ // (?= followed by
1691
+ // [[\]] an opening or closing bracket
1692
+ // )
1693
+ //
1694
+ // In other words, a non-escaped bracket. These have to be escaped now to make sure they
1695
+ // don't count as the end of the link or similar.
1696
+ // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets),
1697
+ // the bracket in one match may be the "not a backslash" character in the next match, so it
1698
+ // should not be consumed by the first match.
1699
+ // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the
1700
+ // start of the string, so this also works if the selection begins with a bracket. We cannot solve
1701
+ // this by anchoring with ^, because in the case that the selection starts with two brackets, this
1702
+ // would mean a zero-width match at the start. Since zero-width matches advance the string position,
1703
+ // the first bracket could then not act as the "not a backslash" for the second.
1704
+ chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1);
1705
+
1706
+ var linkDef = " [999]: " + properlyEncoded(link);
1707
+
1708
+ var num = that.addLinkDef(chunk, linkDef);
1709
+ chunk.startTag = isImage ? "![" : "[";
1710
+ chunk.endTag = "][" + num + "]";
1711
+
1712
+ if (!chunk.selection) {
1713
+ if (isImage) {
1714
+ chunk.selection = "enter image description here";
1715
+ }
1716
+ else {
1717
+ chunk.selection = "enter link description here";
1718
+ }
1719
+ }
1720
+ }
1721
+ postProcessing();
1722
+ };
1723
+
1724
+ background = ui.createBackground();
1725
+
1726
+ if (isImage) {
1727
+ if (!this.hooks.insertImageDialog(linkEnteredCallback))
1728
+ ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback);
1729
+ }
1730
+ else {
1731
+ ui.prompt(linkDialogText, linkDefaultText, linkEnteredCallback);
1732
+ }
1733
+ return true;
1734
+ }
1735
+ };
1736
+
1737
+ // When making a list, hitting shift-enter will put your cursor on the next line
1738
+ // at the current indent level.
1739
+ commandProto.doAutoindent = function (chunk, postProcessing) {
1740
+
1741
+ var commandMgr = this,
1742
+ fakeSelection = false;
1743
+
1744
+ chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
1745
+ chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
1746
+ chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
1747
+
1748
+ // There's no selection, end the cursor wasn't at the end of the line:
1749
+ // The user wants to split the current list item / code line / blockquote line
1750
+ // (for the latter it doesn't really matter) in two. Temporarily select the
1751
+ // (rest of the) line to achieve this.
1752
+ if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) {
1753
+ chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) {
1754
+ chunk.selection = wholeMatch;
1755
+ return "";
1756
+ });
1757
+ fakeSelection = true;
1758
+ }
1759
+
1760
+ if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
1761
+ if (commandMgr.doList) {
1762
+ commandMgr.doList(chunk);
1763
+ }
1764
+ }
1765
+ if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
1766
+ if (commandMgr.doBlockquote) {
1767
+ commandMgr.doBlockquote(chunk);
1768
+ }
1769
+ }
1770
+ if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
1771
+ if (commandMgr.doCode) {
1772
+ commandMgr.doCode(chunk);
1773
+ }
1774
+ }
1775
+
1776
+ if (fakeSelection) {
1777
+ chunk.after = chunk.selection + chunk.after;
1778
+ chunk.selection = "";
1779
+ }
1780
+ };
1781
+
1782
+ commandProto.doBlockquote = function (chunk, postProcessing) {
1783
+
1784
+ chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
1785
+ function (totalMatch, newlinesBefore, text, newlinesAfter) {
1786
+ chunk.before += newlinesBefore;
1787
+ chunk.after = newlinesAfter + chunk.after;
1788
+ return text;
1789
+ });
1790
+
1791
+ chunk.before = chunk.before.replace(/(>[ \t]*)$/,
1792
+ function (totalMatch, blankLine) {
1793
+ chunk.selection = blankLine + chunk.selection;
1794
+ return "";
1795
+ });
1796
+
1797
+ chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");
1798
+ chunk.selection = chunk.selection || "Blockquote";
1799
+
1800
+ // The original code uses a regular expression to find out how much of the
1801
+ // text *directly before* the selection already was a blockquote:
1802
+
1803
+ /*
1804
+ if (chunk.before) {
1805
+ chunk.before = chunk.before.replace(/\n?$/, "\n");
1806
+ }
1807
+ chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
1808
+ function (totalMatch) {
1809
+ chunk.startTag = totalMatch;
1810
+ return "";
1811
+ });
1812
+ */
1813
+
1814
+ // This comes down to:
1815
+ // Go backwards as many lines a possible, such that each line
1816
+ // a) starts with ">", or
1817
+ // b) is almost empty, except for whitespace, or
1818
+ // c) is preceeded by an unbroken chain of non-empty lines
1819
+ // leading up to a line that starts with ">" and at least one more character
1820
+ // and in addition
1821
+ // d) at least one line fulfills a)
1822
+ //
1823
+ // Since this is essentially a backwards-moving regex, it's susceptible to
1824
+ // catstrophic backtracking and can cause the browser to hang;
1825
+ // see e.g. http://meta.stackoverflow.com/questions/9807.
1826
+ //
1827
+ // Hence we replaced this by a simple state machine that just goes through the
1828
+ // lines and checks for a), b), and c).
1829
+
1830
+ var match = "",
1831
+ leftOver = "",
1832
+ line;
1833
+ if (chunk.before) {
1834
+ var lines = chunk.before.replace(/\n$/, "").split("\n");
1835
+ var inChain = false;
1836
+ for (var i = 0; i < lines.length; i++) {
1837
+ var good = false;
1838
+ line = lines[i];
1839
+ inChain = inChain && line.length > 0; // c) any non-empty line continues the chain
1840
+ if (/^>/.test(line)) { // a)
1841
+ good = true;
1842
+ if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain
1843
+ inChain = true;
1844
+ } else if (/^[ \t]*$/.test(line)) { // b)
1845
+ good = true;
1846
+ } else {
1847
+ 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
1848
+ }
1849
+ if (good) {
1850
+ match += line + "\n";
1851
+ } else {
1852
+ leftOver += match + line;
1853
+ match = "\n";
1854
+ }
1855
+ }
1856
+ if (!/(^|\n)>/.test(match)) { // d)
1857
+ leftOver += match;
1858
+ match = "";
1859
+ }
1860
+ }
1861
+
1862
+ chunk.startTag = match;
1863
+ chunk.before = leftOver;
1864
+
1865
+ // end of change
1866
+
1867
+ if (chunk.after) {
1868
+ chunk.after = chunk.after.replace(/^\n?/, "\n");
1869
+ }
1870
+
1871
+ chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
1872
+ function (totalMatch) {
1873
+ chunk.endTag = totalMatch;
1874
+ return "";
1875
+ }
1876
+ );
1877
+
1878
+ var replaceBlanksInTags = function (useBracket) {
1879
+
1880
+ var replacement = useBracket ? "> " : "";
1881
+
1882
+ if (chunk.startTag) {
1883
+ chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
1884
+ function (totalMatch, markdown) {
1885
+ return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
1886
+ });
1887
+ }
1888
+ if (chunk.endTag) {
1889
+ chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
1890
+ function (totalMatch, markdown) {
1891
+ return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
1892
+ });
1893
+ }
1894
+ };
1895
+
1896
+ if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {
1897
+ this.wrap(chunk, SETTINGS.lineLength - 2);
1898
+ chunk.selection = chunk.selection.replace(/^/gm, "> ");
1899
+ replaceBlanksInTags(true);
1900
+ chunk.skipLines();
1901
+ } else {
1902
+ chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
1903
+ this.unwrap(chunk);
1904
+ replaceBlanksInTags(false);
1905
+
1906
+ if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {
1907
+ chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
1908
+ }
1909
+
1910
+ if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {
1911
+ chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");
1912
+ }
1913
+ }
1914
+
1915
+ chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);
1916
+
1917
+ if (!/\n/.test(chunk.selection)) {
1918
+ chunk.selection = chunk.selection.replace(/^(> *)/,
1919
+ function (wholeMatch, blanks) {
1920
+ chunk.startTag += blanks;
1921
+ return "";
1922
+ });
1923
+ }
1924
+ };
1925
+
1926
+ commandProto.doCode = function (chunk, postProcessing) {
1927
+
1928
+ var hasTextBefore = /\S[ ]*$/.test(chunk.before);
1929
+ var hasTextAfter = /^[ ]*\S/.test(chunk.after);
1930
+
1931
+ // Use 'four space' markdown if the selection is on its own
1932
+ // line or is multiline.
1933
+ if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
1934
+
1935
+ chunk.before = chunk.before.replace(/[ ]{4}$/,
1936
+ function (totalMatch) {
1937
+ chunk.selection = totalMatch + chunk.selection;
1938
+ return "";
1939
+ });
1940
+
1941
+ var nLinesBack = 1;
1942
+ var nLinesForward = 1;
1943
+
1944
+ if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
1945
+ nLinesBack = 0;
1946
+ }
1947
+ if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
1948
+ nLinesForward = 0;
1949
+ }
1950
+
1951
+ chunk.skipLines(nLinesBack, nLinesForward);
1952
+
1953
+ if (!chunk.selection) {
1954
+ chunk.startTag = " ";
1955
+ chunk.selection = "enter code here";
1956
+ }
1957
+ else {
1958
+ if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
1959
+ if (/\n/.test(chunk.selection))
1960
+ chunk.selection = chunk.selection.replace(/^/gm, " ");
1961
+ else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior
1962
+ chunk.before += " ";
1963
+ }
1964
+ else {
1965
+ chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
1966
+ }
1967
+ }
1968
+ }
1969
+ else {
1970
+ // Use backticks (`) to delimit the code block.
1971
+
1972
+ chunk.trimWhitespace();
1973
+ chunk.findTags(/`/, /`/);
1974
+
1975
+ if (!chunk.startTag && !chunk.endTag) {
1976
+ chunk.startTag = chunk.endTag = "`";
1977
+ if (!chunk.selection) {
1978
+ chunk.selection = "enter code here";
1979
+ }
1980
+ }
1981
+ else if (chunk.endTag && !chunk.startTag) {
1982
+ chunk.before += chunk.endTag;
1983
+ chunk.endTag = "";
1984
+ }
1985
+ else {
1986
+ chunk.startTag = chunk.endTag = "";
1987
+ }
1988
+ }
1989
+ };
1990
+
1991
+ commandProto.doList = function (chunk, postProcessing, isNumberedList) {
1992
+
1993
+ // These are identical except at the very beginning and end.
1994
+ // Should probably use the regex extension function to make this clearer.
1995
+ var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
1996
+ var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
1997
+
1998
+ // The default bullet is a dash but others are possible.
1999
+ // This has nothing to do with the particular HTML bullet,
2000
+ // it's just a markdown bullet.
2001
+ var bullet = "-";
2002
+
2003
+ // The number in a numbered list.
2004
+ var num = 1;
2005
+
2006
+ // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
2007
+ var getItemPrefix = function () {
2008
+ var prefix;
2009
+ if (isNumberedList) {
2010
+ prefix = " " + num + ". ";
2011
+ num++;
2012
+ }
2013
+ else {
2014
+ prefix = " " + bullet + " ";
2015
+ }
2016
+ return prefix;
2017
+ };
2018
+
2019
+ // Fixes the prefixes of the other list items.
2020
+ var getPrefixedItem = function (itemText) {
2021
+
2022
+ // The numbering flag is unset when called by autoindent.
2023
+ if (isNumberedList === undefined) {
2024
+ isNumberedList = /^\s*\d/.test(itemText);
2025
+ }
2026
+
2027
+ // Renumber/bullet the list element.
2028
+ itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
2029
+ function (_) {
2030
+ return getItemPrefix();
2031
+ });
2032
+
2033
+ return itemText;
2034
+ };
2035
+
2036
+ chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
2037
+
2038
+ if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {
2039
+ chunk.before += chunk.startTag;
2040
+ chunk.startTag = "";
2041
+ }
2042
+
2043
+ if (chunk.startTag) {
2044
+
2045
+ var hasDigits = /\d+[.]/.test(chunk.startTag);
2046
+ chunk.startTag = "";
2047
+ chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
2048
+ this.unwrap(chunk);
2049
+ chunk.skipLines();
2050
+
2051
+ if (hasDigits) {
2052
+ // Have to renumber the bullet points if this is a numbered list.
2053
+ chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
2054
+ }
2055
+ if (isNumberedList == hasDigits) {
2056
+ return;
2057
+ }
2058
+ }
2059
+
2060
+ var nLinesUp = 1;
2061
+
2062
+ chunk.before = chunk.before.replace(previousItemsRegex,
2063
+ function (itemText) {
2064
+ if (/^\s*([*+-])/.test(itemText)) {
2065
+ bullet = re.$1;
2066
+ }
2067
+ nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2068
+ return getPrefixedItem(itemText);
2069
+ });
2070
+
2071
+ if (!chunk.selection) {
2072
+ chunk.selection = "List item";
2073
+ }
2074
+
2075
+ var prefix = getItemPrefix();
2076
+
2077
+ var nLinesDown = 1;
2078
+
2079
+ chunk.after = chunk.after.replace(nextItemsRegex,
2080
+ function (itemText) {
2081
+ nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2082
+ return getPrefixedItem(itemText);
2083
+ });
2084
+
2085
+ chunk.trimWhitespace(true);
2086
+ chunk.skipLines(nLinesUp, nLinesDown, true);
2087
+ chunk.startTag = prefix;
2088
+ var spaces = prefix.replace(/./g, " ");
2089
+ this.wrap(chunk, SETTINGS.lineLength - spaces.length);
2090
+ chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
2091
+
2092
+ };
2093
+
2094
+ commandProto.doHeading = function (chunk, postProcessing) {
2095
+
2096
+ // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
2097
+ chunk.selection = chunk.selection.replace(/\s+/g, " ");
2098
+ chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
2099
+
2100
+ // If we clicked the button with no selected text, we just
2101
+ // make a level 2 hash header around some default text.
2102
+ if (!chunk.selection) {
2103
+ chunk.startTag = "## ";
2104
+ chunk.selection = "Heading";
2105
+ chunk.endTag = " ##";
2106
+ return;
2107
+ }
2108
+
2109
+ var headerLevel = 0; // The existing header level of the selected text.
2110
+
2111
+ // Remove any existing hash heading markdown and save the header level.
2112
+ chunk.findTags(/#+[ ]*/, /[ ]*#+/);
2113
+ if (/#+/.test(chunk.startTag)) {
2114
+ headerLevel = re.lastMatch.length;
2115
+ }
2116
+ chunk.startTag = chunk.endTag = "";
2117
+
2118
+ // Try to get the current header level by looking for - and = in the line
2119
+ // below the selection.
2120
+ chunk.findTags(null, /\s?(-+|=+)/);
2121
+ if (/=+/.test(chunk.endTag)) {
2122
+ headerLevel = 1;
2123
+ }
2124
+ if (/-+/.test(chunk.endTag)) {
2125
+ headerLevel = 2;
2126
+ }
2127
+
2128
+ // Skip to the next line so we can create the header markdown.
2129
+ chunk.startTag = chunk.endTag = "";
2130
+ chunk.skipLines(1, 1);
2131
+
2132
+ // We make a level 2 header if there is no current header.
2133
+ // If there is a header level, we substract one from the header level.
2134
+ // If it's already a level 1 header, it's removed.
2135
+ var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
2136
+
2137
+ if (headerLevelToCreate > 0) {
2138
+
2139
+ // The button only creates level 1 and 2 underline headers.
2140
+ // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner?
2141
+ var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
2142
+ var len = chunk.selection.length;
2143
+ if (len > SETTINGS.lineLength) {
2144
+ len = SETTINGS.lineLength;
2145
+ }
2146
+ chunk.endTag = "\n";
2147
+ while (len--) {
2148
+ chunk.endTag += headerChar;
2149
+ }
2150
+ }
2151
+ };
2152
+
2153
+ commandProto.doHorizontalRule = function (chunk, postProcessing) {
2154
+ chunk.startTag = "----------\n";
2155
+ chunk.selection = "";
2156
+ chunk.skipLines(2, 1, true);
2157
+ }
2158
+
2159
+
2160
+ })();