activeadmin_pagedown 0.0.1

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