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