pagedown-bootstrap-rails 1.1.0 → 2.0.0

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