wmd-rails 0.0.1

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