1 // needs Markdown.Converter.js at the moment
\r
10 nav = top.navigator,
\r
11 SETTINGS = { lineLength: 72 },
\r
13 // Used to work around some browser bugs where we can't use feature testing.
\r
15 isIE: /msie/.test(nav.userAgent.toLowerCase()),
\r
16 isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()),
\r
17 isOpera: /opera/.test(nav.userAgent.toLowerCase())
\r
21 // -------------------------------------------------------------------
\r
22 // YOUR CHANGES GO HERE
\r
24 // I've tried to localize the things you are likely to change to
\r
26 // -------------------------------------------------------------------
\r
28 // The text that appears on the upper part of the dialog box when
\r
30 var linkDialogText = "<p><b>Insert Hyperlink</b></p><p>http://example.com/ \"optional title\"</p>";
\r
31 var imageDialogText = "<p><b>Insert Image</b></p><p>http://example.com/images/diagram.jpg \"optional title\"<br><br>Need <a href='http://www.google.com/search?q=free+image+hosting' target='_blank'>free image hosting?</a></p>";
\r
33 // The default text that appears in the dialog input box when entering
\r
35 var imageDefaultText = "http://";
\r
36 var linkDefaultText = "http://";
\r
38 var defaultHelpHoverTitle = "Markdown Editing Help";
\r
40 // -------------------------------------------------------------------
\r
41 // END OF YOUR CHANGES
\r
42 // -------------------------------------------------------------------
\r
44 // help, if given, should have a property "handler", the click handler for the help button,
\r
45 // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help").
\r
46 // If help isn't given, not help button is created.
\r
48 // The constructed editor object has the methods:
\r
49 // - getConverter() returns the markdown converter object that was passed to the constructor
\r
50 // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op.
\r
51 // - refreshPreview() forces the preview to be updated. This method is only available after run() was called.
\r
52 Markdown.Editor = function (markdownConverter, idPostfix, help) {
\r
54 idPostfix = idPostfix || "";
\r
56 var hooks = this.hooks = new Markdown.HookCollection();
\r
57 hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed
\r
58 hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text
\r
59 hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates
\r
60 * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
\r
61 * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
\r
64 this.getConverter = function () { return markdownConverter; }
\r
69 this.run = function () {
\r
71 return; // already initialized
\r
73 panels = new PanelCollection(idPostfix);
\r
74 var commandManager = new CommandManager(hooks);
\r
75 var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); });
\r
76 var undoManager, uiManager;
\r
78 if (!/\?noundo/.test(doc.location.href)) {
\r
79 undoManager = new UndoManager(function () {
\r
80 previewManager.refresh();
\r
81 if (uiManager) // not available on the first call
\r
82 uiManager.setUndoRedoButtonStates();
\r
86 uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help);
\r
87 uiManager.setUndoRedoButtonStates();
\r
89 var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };
\r
96 // before: contains all the text in the input box BEFORE the selection.
\r
97 // after: contains all the text in the input box AFTER the selection.
\r
98 function Chunks() { }
\r
100 // startRegex: a regular expression to find the start tag
\r
101 // endRegex: a regular expresssion to find the end tag
\r
102 Chunks.prototype.findTags = function (startRegex, endRegex) {
\r
104 var chunkObj = this;
\r
109 regex = util.extendRegExp(startRegex, "", "$");
\r
111 this.before = this.before.replace(regex,
\r
113 chunkObj.startTag = chunkObj.startTag + match;
\r
117 regex = util.extendRegExp(startRegex, "^", "");
\r
119 this.selection = this.selection.replace(regex,
\r
121 chunkObj.startTag = chunkObj.startTag + match;
\r
128 regex = util.extendRegExp(endRegex, "", "$");
\r
130 this.selection = this.selection.replace(regex,
\r
132 chunkObj.endTag = match + chunkObj.endTag;
\r
136 regex = util.extendRegExp(endRegex, "^", "");
\r
138 this.after = this.after.replace(regex,
\r
140 chunkObj.endTag = match + chunkObj.endTag;
\r
146 // If remove is false, the whitespace is transferred
\r
147 // to the before/after regions.
\r
149 // If remove is true, the whitespace disappears.
\r
150 Chunks.prototype.trimWhitespace = function (remove) {
\r
151 var beforeReplacer, afterReplacer, that = this;
\r
153 beforeReplacer = afterReplacer = "";
\r
155 beforeReplacer = function (s) { that.before += s; return ""; }
\r
156 afterReplacer = function (s) { that.after = s + that.after; return ""; }
\r
159 this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer);
\r
163 Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) {
\r
165 if (nLinesBefore === undefined) {
\r
169 if (nLinesAfter === undefined) {
\r
177 var replacementText;
\r
179 // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985
\r
180 if (navigator.userAgent.match(/Chrome/)) {
\r
184 this.selection = this.selection.replace(/(^\n*)/, "");
\r
186 this.startTag = this.startTag + re.$1;
\r
188 this.selection = this.selection.replace(/(\n*$)/, "");
\r
189 this.endTag = this.endTag + re.$1;
\r
190 this.startTag = this.startTag.replace(/(^\n*)/, "");
\r
191 this.before = this.before + re.$1;
\r
192 this.endTag = this.endTag.replace(/(\n*$)/, "");
\r
193 this.after = this.after + re.$1;
\r
197 regexText = replacementText = "";
\r
199 while (nLinesBefore--) {
\r
200 regexText += "\\n?";
\r
201 replacementText += "\n";
\r
204 if (findExtraNewlines) {
\r
205 regexText = "\\n*";
\r
207 this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
\r
212 regexText = replacementText = "";
\r
214 while (nLinesAfter--) {
\r
215 regexText += "\\n?";
\r
216 replacementText += "\n";
\r
218 if (findExtraNewlines) {
\r
219 regexText = "\\n*";
\r
222 this.after = this.after.replace(new re(regexText, ""), replacementText);
\r
228 // A collection of the important regions on the page.
\r
229 // Cached so we don't have to keep traversing the DOM.
\r
230 // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around
\r
232 // Internet explorer has problems with CSS sprite buttons that use HTML
\r
233 // lists. When you click on the background image "button", IE will
\r
234 // select the non-existent link text and discard the selection in the
\r
235 // textarea. The solution to this is to cache the textarea selection
\r
236 // on the button's mousedown event and set a flag. In the part of the
\r
237 // code where we need to grab the selection, we check for the flag
\r
238 // and, if it's set, use the cached area instead of querying the
\r
241 // This ONLY affects Internet Explorer (tested on versions 6, 7
\r
242 // and 8) and ONLY on button clicks. Keyboard shortcuts work
\r
243 // normally since the focus never leaves the textarea.
\r
244 function PanelCollection(postfix) {
\r
245 this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
\r
246 this.preview = doc.getElementById("wmd-preview" + postfix);
\r
247 this.input = doc.getElementById("wmd-input" + postfix);
\r
250 // Returns true if the DOM element is visible, false if it's hidden.
\r
251 // Checks if display is anything other than none.
\r
252 util.isVisible = function (elem) {
\r
254 if (window.getComputedStyle) {
\r
256 return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none";
\r
258 else if (elem.currentStyle) {
\r
260 return elem.currentStyle["display"] !== "none";
\r
265 // Adds a listener callback to a DOM element which is fired on a specified
\r
267 util.addEvent = function (elem, event, listener) {
\r
268 if (elem.attachEvent) {
\r
269 // IE only. The "on" is mandatory.
\r
270 elem.attachEvent("on" + event, listener);
\r
274 elem.addEventListener(event, listener, false);
\r
279 // Removes a listener callback from a DOM element which is fired on a specified
\r
281 util.removeEvent = function (elem, event, listener) {
\r
282 if (elem.detachEvent) {
\r
283 // IE only. The "on" is mandatory.
\r
284 elem.detachEvent("on" + event, listener);
\r
288 elem.removeEventListener(event, listener, false);
\r
292 // Converts \r\n and \r to \n.
\r
293 util.fixEolChars = function (text) {
\r
294 text = text.replace(/\r\n/g, "\n");
\r
295 text = text.replace(/\r/g, "\n");
\r
299 // Extends a regular expression. Returns a new RegExp
\r
300 // using pre + regex + post as the expression.
\r
301 // Used in a few functions where we have a base
\r
302 // expression and we want to pre- or append some
\r
303 // conditions to it (e.g. adding "$" to the end).
\r
304 // The flags are unchanged.
\r
306 // regex is a RegExp, pre and post are strings.
\r
307 util.extendRegExp = function (regex, pre, post) {
\r
309 if (pre === null || pre === undefined) {
\r
312 if (post === null || post === undefined) {
\r
316 var pattern = regex.toString();
\r
319 // Replace the flags with empty space and store them.
\r
320 pattern = pattern.replace(/\/([gim]*)$/, "");
\r
323 // Remove the slash delimiters on the regular expression.
\r
324 pattern = pattern.replace(/(^\/|\/$)/g, "");
\r
325 pattern = pre + pattern + post;
\r
327 return new re(pattern, flags);
\r
331 // The assignment in the while loop makes jslint cranky.
\r
332 // I'll change it to a better loop later.
\r
333 position.getTop = function (elem, isInner) {
\r
334 var result = elem.offsetTop;
\r
336 while (elem = elem.offsetParent) {
\r
337 result += elem.offsetTop;
\r
343 position.getHeight = function (elem) {
\r
344 return elem.offsetHeight || elem.scrollHeight;
\r
347 position.getWidth = function (elem) {
\r
348 return elem.offsetWidth || elem.scrollWidth;
\r
351 position.getPageSize = function () {
\r
353 var scrollWidth, scrollHeight;
\r
354 var innerWidth, innerHeight;
\r
356 // It's not very clear which blocks work with which browsers.
\r
357 if (self.innerHeight && self.scrollMaxY) {
\r
358 scrollWidth = doc.body.scrollWidth;
\r
359 scrollHeight = self.innerHeight + self.scrollMaxY;
\r
361 else if (doc.body.scrollHeight > doc.body.offsetHeight) {
\r
362 scrollWidth = doc.body.scrollWidth;
\r
363 scrollHeight = doc.body.scrollHeight;
\r
366 scrollWidth = doc.body.offsetWidth;
\r
367 scrollHeight = doc.body.offsetHeight;
\r
370 if (self.innerHeight) {
\r
372 innerWidth = self.innerWidth;
\r
373 innerHeight = self.innerHeight;
\r
375 else if (doc.documentElement && doc.documentElement.clientHeight) {
\r
376 // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
\r
377 innerWidth = doc.documentElement.clientWidth;
\r
378 innerHeight = doc.documentElement.clientHeight;
\r
380 else if (doc.body) {
\r
381 // Other versions of IE
\r
382 innerWidth = doc.body.clientWidth;
\r
383 innerHeight = doc.body.clientHeight;
\r
386 var maxWidth = Math.max(scrollWidth, innerWidth);
\r
387 var maxHeight = Math.max(scrollHeight, innerHeight);
\r
388 return [maxWidth, maxHeight, innerWidth, innerHeight];
\r
391 // Handles pushing and popping TextareaStates for undo/redo commands.
\r
392 // I should rename the stack variables to list.
\r
393 function UndoManager(callback, panels) {
\r
395 var undoObj = this;
\r
396 var undoStack = []; // A stack of undo states
\r
397 var stackPtr = 0; // The index of the current state
\r
399 var lastState; // The last state
\r
400 var timer; // The setTimeout handle for cancelling the timer
\r
403 // Set the mode for later logic steps.
\r
404 var setMode = function (newMode, noSave) {
\r
405 if (mode != newMode) {
\r
412 if (!uaSniffed.isIE || mode != "moving") {
\r
413 timer = top.setTimeout(refreshState, 1);
\r
416 inputStateObj = null;
\r
420 var refreshState = function (isInitialState) {
\r
421 inputStateObj = new TextareaState(panels, isInitialState);
\r
425 this.setCommandMode = function () {
\r
428 timer = top.setTimeout(refreshState, 0);
\r
431 this.canUndo = function () {
\r
432 return stackPtr > 1;
\r
435 this.canRedo = function () {
\r
436 if (undoStack[stackPtr + 1]) {
\r
442 // Removes the last state and restores it.
\r
443 this.undo = function () {
\r
445 if (undoObj.canUndo()) {
\r
447 // What about setting state -1 to null or checking for undefined?
\r
448 lastState.restore();
\r
452 undoStack[stackPtr] = new TextareaState(panels);
\r
453 undoStack[--stackPtr].restore();
\r
462 panels.input.focus();
\r
467 this.redo = function () {
\r
469 if (undoObj.canRedo()) {
\r
471 undoStack[++stackPtr].restore();
\r
479 panels.input.focus();
\r
483 // Push the input area state to the stack.
\r
484 var saveState = function () {
\r
485 var currState = inputStateObj || new TextareaState(panels);
\r
490 if (mode == "moving") {
\r
492 lastState = currState;
\r
497 if (undoStack[stackPtr - 1].text != lastState.text) {
\r
498 undoStack[stackPtr++] = lastState;
\r
502 undoStack[stackPtr++] = currState;
\r
503 undoStack[stackPtr + 1] = null;
\r
509 var handleCtrlYZ = function (event) {
\r
511 var handled = false;
\r
513 if (event.ctrlKey || event.metaKey) {
\r
515 // IE and Opera do not support charCode.
\r
516 var keyCode = event.charCode || event.keyCode;
\r
517 var keyCodeChar = String.fromCharCode(keyCode);
\r
519 switch (keyCodeChar) {
\r
527 if (!event.shiftKey) {
\r
539 if (event.preventDefault) {
\r
540 event.preventDefault();
\r
543 top.event.returnValue = false;
\r
549 // Set the mode depending on what is going on in the input area.
\r
550 var handleModeChange = function (event) {
\r
552 if (!event.ctrlKey && !event.metaKey) {
\r
554 var keyCode = event.keyCode;
\r
556 if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
\r
557 // 33 - 40: page up/dn and arrow keys
\r
558 // 63232 - 63235: page up/dn and arrow keys on safari
\r
561 else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
\r
565 setMode("deleting");
\r
567 else if (keyCode == 13) {
\r
569 setMode("newlines");
\r
571 else if (keyCode == 27) {
\r
575 else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
\r
576 // 16-20 are shift, etc.
\r
577 // 91: left window key
\r
578 // I think this might be a little messed up since there are
\r
579 // a lot of nonprinting keys above 20.
\r
585 var setEventHandlers = function () {
\r
586 util.addEvent(panels.input, "keypress", function (event) {
\r
589 if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) {
\r
590 event.preventDefault();
\r
594 var handlePaste = function () {
\r
595 if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) {
\r
596 if (timer == undefined) {
\r
604 util.addEvent(panels.input, "keydown", handleCtrlYZ);
\r
605 util.addEvent(panels.input, "keydown", handleModeChange);
\r
606 util.addEvent(panels.input, "mousedown", function () {
\r
610 panels.input.onpaste = handlePaste;
\r
611 panels.input.ondrop = handlePaste;
\r
614 var init = function () {
\r
615 setEventHandlers();
\r
616 refreshState(true);
\r
623 // end of UndoManager
\r
625 // The input textarea state/contents.
\r
626 // This is used to implement undo/redo by the undo manager.
\r
627 function TextareaState(panels, isInitialState) {
\r
630 var stateObj = this;
\r
631 var inputArea = panels.input;
\r
632 this.init = function () {
\r
633 if (!util.isVisible(inputArea)) {
\r
636 if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box
\r
640 this.setInputAreaSelectionStartEnd();
\r
641 this.scrollTop = inputArea.scrollTop;
\r
642 if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
\r
643 this.text = inputArea.value;
\r
648 // Sets the selected text in the input box after we've performed an
\r
650 this.setInputAreaSelection = function () {
\r
652 if (!util.isVisible(inputArea)) {
\r
656 if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) {
\r
659 inputArea.selectionStart = stateObj.start;
\r
660 inputArea.selectionEnd = stateObj.end;
\r
661 inputArea.scrollTop = stateObj.scrollTop;
\r
663 else if (doc.selection) {
\r
665 if (doc.activeElement && doc.activeElement !== inputArea) {
\r
670 var range = inputArea.createTextRange();
\r
671 range.moveStart("character", -inputArea.value.length);
\r
672 range.moveEnd("character", -inputArea.value.length);
\r
673 range.moveEnd("character", stateObj.end);
\r
674 range.moveStart("character", stateObj.start);
\r
679 this.setInputAreaSelectionStartEnd = function () {
\r
681 if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) {
\r
683 stateObj.start = inputArea.selectionStart;
\r
684 stateObj.end = inputArea.selectionEnd;
\r
686 else if (doc.selection) {
\r
688 stateObj.text = util.fixEolChars(inputArea.value);
\r
690 // IE loses the selection in the textarea when buttons are
\r
691 // clicked. On IE we cache the selection. Here, if something is cached,
\r
693 var range = panels.ieCachedRange || doc.selection.createRange();
\r
695 var fixedRange = util.fixEolChars(range.text);
\r
696 var marker = "\x07";
\r
697 var markedRange = marker + fixedRange + marker;
\r
698 range.text = markedRange;
\r
699 var inputText = util.fixEolChars(inputArea.value);
\r
701 range.moveStart("character", -markedRange.length);
\r
702 range.text = fixedRange;
\r
704 stateObj.start = inputText.indexOf(marker);
\r
705 stateObj.end = inputText.lastIndexOf(marker) - marker.length;
\r
707 var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
\r
710 range.moveStart("character", -fixedRange.length);
\r
712 fixedRange += "\n";
\r
715 range.text = fixedRange;
\r
718 if (panels.ieCachedRange)
\r
719 stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange
\r
721 panels.ieCachedRange = null;
\r
723 this.setInputAreaSelection();
\r
727 // Restore this state into the input area.
\r
728 this.restore = function () {
\r
730 if (stateObj.text != undefined && stateObj.text != inputArea.value) {
\r
731 inputArea.value = stateObj.text;
\r
733 this.setInputAreaSelection();
\r
734 inputArea.scrollTop = stateObj.scrollTop;
\r
737 // Gets a collection of HTML chunks from the inptut textarea.
\r
738 this.getChunks = function () {
\r
740 var chunk = new Chunks();
\r
741 chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
\r
742 chunk.startTag = "";
\r
743 chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
\r
745 chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
\r
746 chunk.scrollTop = stateObj.scrollTop;
\r
751 // Sets the TextareaState properties given a chunk of markdown.
\r
752 this.setChunks = function (chunk) {
\r
754 chunk.before = chunk.before + chunk.startTag;
\r
755 chunk.after = chunk.endTag + chunk.after;
\r
757 this.start = chunk.before.length;
\r
758 this.end = chunk.before.length + chunk.selection.length;
\r
759 this.text = chunk.before + chunk.selection + chunk.after;
\r
760 this.scrollTop = chunk.scrollTop;
\r
765 function PreviewManager(converter, panels, previewRefreshCallback) {
\r
767 var managerObj = this;
\r
771 var maxDelay = 3000;
\r
772 var startType = "delayed"; // The other legal value is "manual"
\r
774 // Adds event listeners to elements
\r
775 var setupEvents = function (inputElem, listener) {
\r
777 util.addEvent(inputElem, "input", listener);
\r
778 inputElem.onpaste = listener;
\r
779 inputElem.ondrop = listener;
\r
781 util.addEvent(inputElem, "keypress", listener);
\r
782 util.addEvent(inputElem, "keydown", listener);
\r
785 var getDocScrollTop = function () {
\r
789 if (top.innerHeight) {
\r
790 result = top.pageYOffset;
\r
793 if (doc.documentElement && doc.documentElement.scrollTop) {
\r
794 result = doc.documentElement.scrollTop;
\r
798 result = doc.body.scrollTop;
\r
804 var makePreviewHtml = function () {
\r
806 // If there is no registered preview panel
\r
807 // there is nothing to do.
\r
808 if (!panels.preview)
\r
812 var text = panels.input.value;
\r
813 if (text && text == oldInputText) {
\r
814 return; // Input text hasn't changed.
\r
817 oldInputText = text;
\r
820 var prevTime = new Date().getTime();
\r
822 text = converter.makeHtml(text);
\r
824 // Calculate the processing time of the HTML creation.
\r
825 // It's used as the delay time in the event listener.
\r
826 var currTime = new Date().getTime();
\r
827 elapsedTime = currTime - prevTime;
\r
829 pushPreviewHtml(text);
\r
832 // setTimeout is already used. Used as an event listener.
\r
833 var applyTimeout = function () {
\r
836 top.clearTimeout(timeout);
\r
837 timeout = undefined;
\r
840 if (startType !== "manual") {
\r
844 if (startType === "delayed") {
\r
845 delay = elapsedTime;
\r
848 if (delay > maxDelay) {
\r
851 timeout = top.setTimeout(makePreviewHtml, delay);
\r
855 var getScaleFactor = function (panel) {
\r
856 if (panel.scrollHeight <= panel.clientHeight) {
\r
859 return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
\r
862 var setPanelScrollTops = function () {
\r
863 if (panels.preview) {
\r
864 panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);
\r
868 this.refresh = function (requiresRefresh) {
\r
870 if (requiresRefresh) {
\r
879 this.processingTime = function () {
\r
880 return elapsedTime;
\r
883 var isFirstTimeFilled = true;
\r
885 // IE doesn't let you use innerHTML if the element is contained somewhere in a table
\r
886 // (which is the case for inline editing) -- in that case, detach the element, set the
\r
887 // value, and reattach. Yes, that *is* ridiculous.
\r
888 var ieSafePreviewSet = function (text) {
\r
889 var preview = panels.preview;
\r
890 var parent = preview.parentNode;
\r
891 var sibling = preview.nextSibling;
\r
892 parent.removeChild(preview);
\r
893 preview.innerHTML = text;
\r
895 parent.appendChild(preview);
\r
897 parent.insertBefore(preview, sibling);
\r
900 var nonSuckyBrowserPreviewSet = function (text) {
\r
901 panels.preview.innerHTML = text;
\r
906 var previewSet = function (text) {
\r
908 return previewSetter(text);
\r
911 nonSuckyBrowserPreviewSet(text);
\r
912 previewSetter = nonSuckyBrowserPreviewSet;
\r
914 previewSetter = ieSafePreviewSet;
\r
915 previewSetter(text);
\r
919 var pushPreviewHtml = function (text) {
\r
921 var emptyTop = position.getTop(panels.input) - getDocScrollTop();
\r
923 if (panels.preview) {
\r
925 previewRefreshCallback();
\r
928 setPanelScrollTops();
\r
930 if (isFirstTimeFilled) {
\r
931 isFirstTimeFilled = false;
\r
935 var fullTop = position.getTop(panels.input) - getDocScrollTop();
\r
937 if (uaSniffed.isIE) {
\r
938 top.setTimeout(function () {
\r
939 top.scrollBy(0, fullTop - emptyTop);
\r
943 top.scrollBy(0, fullTop - emptyTop);
\r
947 var init = function () {
\r
949 setupEvents(panels.input, applyTimeout);
\r
952 if (panels.preview) {
\r
953 panels.preview.scrollTop = 0;
\r
960 // Creates the background behind the hyperlink text entry box.
\r
961 // And download dialog
\r
962 // Most of this has been moved to CSS but the div creation and
\r
963 // browser-specific hacks remain here.
\r
964 ui.createBackground = function () {
\r
966 var background = doc.createElement("div");
\r
967 background.className = "wmd-prompt-background";
\r
968 style = background.style;
\r
969 style.position = "absolute";
\r
972 style.zIndex = "1000";
\r
974 if (uaSniffed.isIE) {
\r
975 style.filter = "alpha(opacity=50)";
\r
978 style.opacity = "0.5";
\r
981 var pageSize = position.getPageSize();
\r
982 style.height = pageSize[1] + "px";
\r
984 if (uaSniffed.isIE) {
\r
985 style.left = doc.documentElement.scrollLeft;
\r
986 style.width = doc.documentElement.clientWidth;
\r
990 style.width = "100%";
\r
993 doc.body.appendChild(background);
\r
997 // This simulates a modal dialog box and asks for the URL when you
\r
998 // click the hyperlink or image buttons.
\r
1000 // text: The html for the input box.
\r
1001 // defaultInputText: The default value that appears in the input box.
\r
1002 // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
\r
1003 // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
\r
1005 ui.prompt = function (text, defaultInputText, callback) {
\r
1007 // These variables need to be declared at this level since they are used
\r
1008 // in multiple functions.
\r
1009 var dialog; // The dialog box.
\r
1010 var input; // The text box where you enter the hyperlink.
\r
1013 if (defaultInputText === undefined) {
\r
1014 defaultInputText = "";
\r
1017 // Used as a keydown event handler. Esc dismisses the prompt.
\r
1018 // Key code 27 is ESC.
\r
1019 var checkEscape = function (key) {
\r
1020 var code = (key.charCode || key.keyCode);
\r
1021 if (code === 27) {
\r
1026 // Dismisses the hyperlink input box.
\r
1027 // isCancel is true if we don't care about the input text.
\r
1028 // isCancel is false if we are going to keep the text.
\r
1029 var close = function (isCancel) {
\r
1030 util.removeEvent(doc.body, "keydown", checkEscape);
\r
1031 var text = input.value;
\r
1037 // Fixes common pasting errors.
\r
1038 text = text.replace('http://http://', 'http://');
\r
1039 text = text.replace('http://https://', 'https://');
\r
1040 text = text.replace('http://ftp://', 'ftp://');
\r
1042 if (text.indexOf('http://') === -1 && text.indexOf('ftp://') === -1 && text.indexOf('https://') === -1) {
\r
1043 text = 'http://' + text;
\r
1047 dialog.parentNode.removeChild(dialog);
\r
1055 // Create the text input box form/window.
\r
1056 var createDialog = function () {
\r
1058 // The main dialog box.
\r
1059 dialog = doc.createElement("div");
\r
1060 dialog.className = "wmd-prompt-dialog";
\r
1061 dialog.style.padding = "10px;";
\r
1062 dialog.style.position = "fixed";
\r
1063 dialog.style.width = "400px";
\r
1064 dialog.style.zIndex = "1001";
\r
1066 // The dialog text.
\r
1067 var question = doc.createElement("div");
\r
1068 question.innerHTML = text;
\r
1069 question.style.padding = "5px";
\r
1070 dialog.appendChild(question);
\r
1072 // The web form container for the text box and buttons.
\r
1073 var form = doc.createElement("form");
\r
1074 form.onsubmit = function () { return close(false); };
\r
1075 style = form.style;
\r
1076 style.padding = "0";
\r
1077 style.margin = "0";
\r
1078 style.cssFloat = "left";
\r
1079 style.width = "100%";
\r
1080 style.textAlign = "center";
\r
1081 style.position = "relative";
\r
1082 dialog.appendChild(form);
\r
1084 // The input text box
\r
1085 input = doc.createElement("input");
\r
1086 input.type = "text";
\r
1087 input.value = defaultInputText;
\r
1088 style = input.style;
\r
1089 style.display = "block";
\r
1090 style.width = "80%";
\r
1091 style.marginLeft = style.marginRight = "auto";
\r
1092 form.appendChild(input);
\r
1095 var okButton = doc.createElement("input");
\r
1096 okButton.type = "button";
\r
1097 okButton.onclick = function () { return close(false); };
\r
1098 okButton.value = "OK";
\r
1099 style = okButton.style;
\r
1100 style.margin = "10px";
\r
1101 style.display = "inline";
\r
1102 style.width = "7em";
\r
1105 // The cancel button
\r
1106 var cancelButton = doc.createElement("input");
\r
1107 cancelButton.type = "button";
\r
1108 cancelButton.onclick = function () { return close(true); };
\r
1109 cancelButton.value = "Cancel";
\r
1110 style = cancelButton.style;
\r
1111 style.margin = "10px";
\r
1112 style.display = "inline";
\r
1113 style.width = "7em";
\r
1115 form.appendChild(okButton);
\r
1116 form.appendChild(cancelButton);
\r
1118 util.addEvent(doc.body, "keydown", checkEscape);
\r
1119 dialog.style.top = "50%";
\r
1120 dialog.style.left = "50%";
\r
1121 dialog.style.display = "block";
\r
1122 if (uaSniffed.isIE_5or6) {
\r
1123 dialog.style.position = "absolute";
\r
1124 dialog.style.top = doc.documentElement.scrollTop + 200 + "px";
\r
1125 dialog.style.left = "50%";
\r
1127 doc.body.appendChild(dialog);
\r
1129 // This has to be done AFTER adding the dialog to the form if you
\r
1130 // want it to be centered.
\r
1131 dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";
\r
1132 dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";
\r
1136 // Why is this in a zero-length timeout?
\r
1137 // Is it working around a browser bug?
\r
1138 top.setTimeout(function () {
\r
1142 var defTextLen = defaultInputText.length;
\r
1143 if (input.selectionStart !== undefined) {
\r
1144 input.selectionStart = 0;
\r
1145 input.selectionEnd = defTextLen;
\r
1147 else if (input.createTextRange) {
\r
1148 var range = input.createTextRange();
\r
1149 range.collapse(false);
\r
1150 range.moveStart("character", -defTextLen);
\r
1151 range.moveEnd("character", defTextLen);
\r
1159 function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) {
\r
1161 var inputBox = panels.input,
\r
1162 buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
\r
1164 makeSpritedButtonRow();
\r
1166 var keyEvent = "keydown";
\r
1167 if (uaSniffed.isOpera) {
\r
1168 keyEvent = "keypress";
\r
1171 util.addEvent(inputBox, keyEvent, function (key) {
\r
1173 // Check to see if we have a button key and, if so execute the callback.
\r
1174 if ((key.ctrlKey || key.metaKey) && !key.altKey) {
\r
1176 var keyCode = key.charCode || key.keyCode;
\r
1177 var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
\r
1179 switch (keyCodeStr) {
\r
1181 doClick(buttons.bold);
\r
1184 doClick(buttons.italic);
\r
1187 doClick(buttons.link);
\r
1190 doClick(buttons.quote);
\r
1193 doClick(buttons.code);
\r
1196 doClick(buttons.image);
\r
1199 doClick(buttons.olist);
\r
1202 doClick(buttons.ulist);
\r
1205 doClick(buttons.heading);
\r
1208 doClick(buttons.hr);
\r
1211 doClick(buttons.redo);
\r
1214 if (key.shiftKey) {
\r
1215 doClick(buttons.redo);
\r
1218 doClick(buttons.undo);
\r
1226 if (key.preventDefault) {
\r
1227 key.preventDefault();
\r
1231 top.event.returnValue = false;
\r
1236 // Auto-indent on shift-enter
\r
1237 util.addEvent(inputBox, "keyup", function (key) {
\r
1238 if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
\r
1239 var keyCode = key.charCode || key.keyCode;
\r
1240 // Character 13 is Enter
\r
1241 if (keyCode === 13) {
\r
1243 fakeButton.textOp = bindCommand("doAutoindent");
\r
1244 doClick(fakeButton);
\r
1249 // special handler because IE clears the context of the textbox on ESC
\r
1250 if (uaSniffed.isIE) {
\r
1251 util.addEvent(inputBox, "keydown", function (key) {
\r
1252 var code = key.keyCode;
\r
1253 if (code === 27) {
\r
1260 // Perform the button's action.
\r
1261 function doClick(button) {
\r
1265 if (button.textOp) {
\r
1267 if (undoManager) {
\r
1268 undoManager.setCommandMode();
\r
1271 var state = new TextareaState(panels);
\r
1277 var chunks = state.getChunks();
\r
1279 // Some commands launch a "modal" prompt dialog. Javascript
\r
1280 // can't really make a modal dialog box and the WMD code
\r
1281 // will continue to execute while the dialog is displayed.
\r
1282 // This prevents the dialog pattern I'm used to and means
\r
1283 // I can't do something like this:
\r
1285 // var link = CreateLinkDialog();
\r
1286 // makeMarkdownLink(link);
\r
1288 // Instead of this straightforward method of handling a
\r
1289 // dialog I have to pass any code which would execute
\r
1290 // after the dialog is dismissed (e.g. link creation)
\r
1291 // in a function parameter.
\r
1293 // Yes this is awkward and I think it sucks, but there's
\r
1294 // no real workaround. Only the image and link code
\r
1295 // create dialogs and require the function pointers.
\r
1296 var fixupInputArea = function () {
\r
1301 state.setChunks(chunks);
\r
1305 previewManager.refresh();
\r
1308 var noCleanup = button.textOp(chunks, fixupInputArea);
\r
1316 if (button.execute) {
\r
1317 button.execute(undoManager);
\r
1321 function setupButton(button, isEnabled) {
\r
1323 var normalYShift = "0px";
\r
1324 var disabledYShift = "-20px";
\r
1325 var highlightYShift = "-40px";
\r
1326 var image = button.getElementsByTagName("span")[0];
\r
1328 image.style.backgroundPosition = button.XShift + " " + normalYShift;
\r
1329 button.onmouseover = function () {
\r
1330 image.style.backgroundPosition = this.XShift + " " + highlightYShift;
\r
1333 button.onmouseout = function () {
\r
1334 image.style.backgroundPosition = this.XShift + " " + normalYShift;
\r
1337 // IE tries to select the background image "button" text (it's
\r
1338 // implemented in a list item) so we have to cache the selection
\r
1340 if (uaSniffed.isIE) {
\r
1341 button.onmousedown = function () {
\r
1342 if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection
\r
1345 panels.ieCachedRange = document.selection.createRange();
\r
1346 panels.ieCachedScrollTop = panels.input.scrollTop;
\r
1350 if (!button.isHelp) {
\r
1351 button.onclick = function () {
\r
1352 if (this.onmouseout) {
\r
1353 this.onmouseout();
\r
1361 image.style.backgroundPosition = button.XShift + " " + disabledYShift;
\r
1362 button.onmouseover = button.onmouseout = button.onclick = function () { };
\r
1366 function bindCommand(method) {
\r
1367 if (typeof method === "string")
\r
1368 method = commandManager[method];
\r
1369 return function () { method.apply(commandManager, arguments); }
\r
1372 function makeSpritedButtonRow() {
\r
1374 var buttonBar = panels.buttonBar;
\r
1376 var normalYShift = "0px";
\r
1377 var disabledYShift = "-20px";
\r
1378 var highlightYShift = "-40px";
\r
1380 var buttonRow = document.createElement("ul");
\r
1381 buttonRow.id = "wmd-button-row" + postfix;
\r
1382 buttonRow.className = 'wmd-button-row';
\r
1383 buttonRow = buttonBar.appendChild(buttonRow);
\r
1384 var xPosition = 0;
\r
1385 var makeButton = function (id, title, XShift, textOp) {
\r
1386 var button = document.createElement("li");
\r
1387 button.className = "wmd-button";
\r
1388 button.style.left = xPosition + "px";
\r
1390 var buttonImage = document.createElement("span");
\r
1391 button.id = id + postfix;
\r
1392 button.appendChild(buttonImage);
\r
1393 button.title = title;
\r
1394 button.XShift = XShift;
\r
1396 button.textOp = textOp;
\r
1397 setupButton(button, true);
\r
1398 buttonRow.appendChild(button);
\r
1401 var makeSpacer = function (num) {
\r
1402 var spacer = document.createElement("li");
\r
1403 spacer.className = "wmd-spacer wmd-spacer" + num;
\r
1404 spacer.id = "wmd-spacer" + num + postfix;
\r
1405 buttonRow.appendChild(spacer);
\r
1409 buttons.bold = makeButton("wmd-bold-button", "Strong <strong> Ctrl+B", "0px", bindCommand("doBold"));
\r
1410 buttons.italic = makeButton("wmd-italic-button", "Emphasis <em> Ctrl+I", "-20px", bindCommand("doItalic"));
\r
1412 buttons.link = makeButton("wmd-link-button", "Hyperlink <a> Ctrl+L", "-40px", bindCommand(function (chunk, postProcessing) {
\r
1413 return this.doLinkOrImage(chunk, postProcessing, false);
\r
1415 buttons.quote = makeButton("wmd-quote-button", "Blockquote <blockquote> Ctrl+Q", "-60px", bindCommand("doBlockquote"));
\r
1416 buttons.code = makeButton("wmd-code-button", "Code Sample <pre><code> Ctrl+K", "-80px", bindCommand("doCode"));
\r
1417 buttons.image = makeButton("wmd-image-button", "Image <img> Ctrl+G", "-100px", bindCommand(function (chunk, postProcessing) {
\r
1418 return this.doLinkOrImage(chunk, postProcessing, true);
\r
1421 buttons.olist = makeButton("wmd-olist-button", "Numbered List <ol> Ctrl+O", "-120px", bindCommand(function (chunk, postProcessing) {
\r
1422 this.doList(chunk, postProcessing, true);
\r
1424 buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List <ul> Ctrl+U", "-140px", bindCommand(function (chunk, postProcessing) {
\r
1425 this.doList(chunk, postProcessing, false);
\r
1427 buttons.heading = makeButton("wmd-heading-button", "Heading <h1>/<h2> Ctrl+H", "-160px", bindCommand("doHeading"));
\r
1428 buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule <hr> Ctrl+R", "-180px", bindCommand("doHorizontalRule"));
\r
1430 buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "-200px", null);
\r
1431 buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
\r
1433 var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
\r
1435 "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms
\r
1437 buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null);
\r
1438 buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
\r
1440 if (helpOptions) {
\r
1441 var helpButton = document.createElement("li");
\r
1442 var helpButtonImage = document.createElement("span");
\r
1443 helpButton.appendChild(helpButtonImage);
\r
1444 helpButton.className = "wmd-button wmd-help-button";
\r
1445 helpButton.id = "wmd-help-button" + postfix;
\r
1446 helpButton.XShift = "-240px";
\r
1447 helpButton.isHelp = true;
\r
1448 helpButton.style.right = "0px";
\r
1449 helpButton.title = helpOptions.title || defaultHelpHoverTitle;
\r
1450 helpButton.onclick = helpOptions.handler;
\r
1452 setupButton(helpButton, true);
\r
1453 buttonRow.appendChild(helpButton);
\r
1454 buttons.help = helpButton;
\r
1457 setUndoRedoButtonStates();
\r
1460 function setUndoRedoButtonStates() {
\r
1461 if (undoManager) {
\r
1462 setupButton(buttons.undo, undoManager.canUndo());
\r
1463 setupButton(buttons.redo, undoManager.canRedo());
\r
1467 this.setUndoRedoButtonStates = setUndoRedoButtonStates;
\r
1471 function CommandManager(pluginHooks) {
\r
1472 this.hooks = pluginHooks;
\r
1475 var commandProto = CommandManager.prototype;
\r
1477 // The markdown symbols - 4 spaces = code, > = blockquote, etc.
\r
1478 commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
\r
1480 // Remove markdown symbols from the chunk selection.
\r
1481 commandProto.unwrap = function (chunk) {
\r
1482 var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");
\r
1483 chunk.selection = chunk.selection.replace(txt, "$1 $2");
\r
1486 commandProto.wrap = function (chunk, len) {
\r
1487 this.unwrap(chunk);
\r
1488 var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm");
\r
1490 chunk.selection = chunk.selection.replace(regex, function (line, marked) {
\r
1491 if (new re("^" + this.prefixes, "").test(line)) {
\r
1494 return marked + "\n";
\r
1497 chunk.selection = chunk.selection.replace(/\s+$/, "");
\r
1500 commandProto.doBold = function (chunk, postProcessing) {
\r
1501 return this.doBorI(chunk, postProcessing, 2, "strong text");
\r
1504 commandProto.doItalic = function (chunk, postProcessing) {
\r
1505 return this.doBorI(chunk, postProcessing, 1, "emphasized text");
\r
1508 // chunk: The selected region that will be enclosed with */**
\r
1509 // nStars: 1 for italics, 2 for bold
\r
1510 // insertText: If you just click the button without highlighting text, this gets inserted
\r
1511 commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {
\r
1513 // Get rid of whitespace and fixup newlines.
\r
1514 chunk.trimWhitespace();
\r
1515 chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
\r
1517 // Look for stars before and after. Is the chunk already marked up?
\r
1518 // note that these regex matches cannot fail
\r
1519 var starsBefore = /(\**$)/.exec(chunk.before)[0];
\r
1520 var starsAfter = /(^\**)/.exec(chunk.after)[0];
\r
1522 var prevStars = Math.min(starsBefore.length, starsAfter.length);
\r
1524 // Remove stars if we have to since the button acts as a toggle.
\r
1525 if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
\r
1526 chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
\r
1527 chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
\r
1529 else if (!chunk.selection && starsAfter) {
\r
1530 // It's not really clear why this code is necessary. It just moves
\r
1531 // some arbitrary stuff around.
\r
1532 chunk.after = chunk.after.replace(/^([*_]*)/, "");
\r
1533 chunk.before = chunk.before.replace(/(\s?)$/, "");
\r
1534 var whitespace = re.$1;
\r
1535 chunk.before = chunk.before + starsAfter + whitespace;
\r
1539 // In most cases, if you don't have any selected text and click the button
\r
1540 // you'll get a selected, marked up region with the default text inserted.
\r
1541 if (!chunk.selection && !starsAfter) {
\r
1542 chunk.selection = insertText;
\r
1545 // Add the true markup.
\r
1546 var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
\r
1547 chunk.before = chunk.before + markup;
\r
1548 chunk.after = markup + chunk.after;
\r
1554 commandProto.stripLinkDefs = function (text, defsToAdd) {
\r
1556 text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,
\r
1557 function (totalMatch, id, link, newlines, title) {
\r
1558 defsToAdd[id] = totalMatch.replace(/\s*$/, "");
\r
1560 // Strip the title and return that separately.
\r
1561 defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
\r
1562 return newlines + title;
\r
1570 commandProto.addLinkDef = function (chunk, linkDef) {
\r
1572 var refNumber = 0; // The current reference number
\r
1573 var defsToAdd = {}; //
\r
1574 // Start with a clean slate by removing all previous link definitions.
\r
1575 chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);
\r
1576 chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);
\r
1577 chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);
\r
1580 var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
\r
1582 var addDefNumber = function (def) {
\r
1584 def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:");
\r
1585 defs += "\n" + def;
\r
1589 // a) the recursive call to getLink cannot go infinite, because by definition
\r
1590 // of regex, inner is always a proper substring of wholeMatch, and
\r
1591 // b) more than one level of nesting is neither supported by the regex
\r
1592 // nor making a lot of sense (the only use case for nesting is a linked image)
\r
1593 var getLink = function (wholeMatch, before, inner, afterInner, id, end) {
\r
1594 inner = inner.replace(regex, getLink);
\r
1595 if (defsToAdd[id]) {
\r
1596 addDefNumber(defsToAdd[id]);
\r
1597 return before + inner + afterInner + refNumber + end;
\r
1599 return wholeMatch;
\r
1602 chunk.before = chunk.before.replace(regex, getLink);
\r
1605 addDefNumber(linkDef);
\r
1608 chunk.selection = chunk.selection.replace(regex, getLink);
\r
1611 var refOut = refNumber;
\r
1613 chunk.after = chunk.after.replace(regex, getLink);
\r
1615 if (chunk.after) {
\r
1616 chunk.after = chunk.after.replace(/\n*$/, "");
\r
1618 if (!chunk.after) {
\r
1619 chunk.selection = chunk.selection.replace(/\n*$/, "");
\r
1622 chunk.after += "\n\n" + defs;
\r
1627 // takes the line as entered into the add link/as image dialog and makes
\r
1628 // sure the URL and the optinal title are "nice".
\r
1629 function properlyEncoded(linkdef) {
\r
1630 return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
\r
1631 link = link.replace(/\?.*$/, function (querypart) {
\r
1632 return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical
\r
1634 link = decodeURIComponent(link); // unencode first, to prevent double encoding
\r
1635 link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
\r
1636 link = link.replace(/\?.*$/, function (querypart) {
\r
1637 return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded
\r
1640 title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");
\r
1641 title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(/</g, "<").replace(/>/g, ">");
\r
1643 return title ? link + ' "' + title + '"' : link;
\r
1647 commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
\r
1649 chunk.trimWhitespace();
\r
1650 chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
\r
1653 if (chunk.endTag.length > 1) {
\r
1655 chunk.startTag = chunk.startTag.replace(/!?\[/, "");
\r
1656 chunk.endTag = "";
\r
1657 this.addLinkDef(chunk, null);
\r
1662 if (/\n\n/.test(chunk.selection)) {
\r
1663 this.addLinkDef(chunk, null);
\r
1667 // The function to be executed when you enter a link and press OK or Cancel.
\r
1668 // Marks up the link and adds the ref.
\r
1669 var linkEnteredCallback = function (link) {
\r
1671 background.parentNode.removeChild(background);
\r
1673 if (link !== null) {
\r
1675 chunk.startTag = chunk.endTag = "";
\r
1676 var linkDef = " [999]: " + properlyEncoded(link);
\r
1678 var num = that.addLinkDef(chunk, linkDef);
\r
1679 chunk.startTag = isImage ? "![" : "[";
\r
1680 chunk.endTag = "][" + num + "]";
\r
1682 if (!chunk.selection) {
\r
1684 chunk.selection = "enter image description here";
\r
1687 chunk.selection = "enter link description here";
\r
1694 background = ui.createBackground();
\r
1697 if (!this.hooks.insertImageDialog(linkEnteredCallback))
\r
1698 ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback);
\r
1701 ui.prompt(linkDialogText, linkDefaultText, linkEnteredCallback);
\r
1707 // When making a list, hitting shift-enter will put your cursor on the next line
\r
1708 // at the current indent level.
\r
1709 commandProto.doAutoindent = function (chunk, postProcessing) {
\r
1711 var commandMgr = this;
\r
1713 chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
\r
1714 chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
\r
1715 chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
\r
1717 if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
\r
1718 if (commandMgr.doList) {
\r
1719 commandMgr.doList(chunk);
\r
1722 if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
\r
1723 if (commandMgr.doBlockquote) {
\r
1724 commandMgr.doBlockquote(chunk);
\r
1727 if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
\r
1728 if (commandMgr.doCode) {
\r
1729 commandMgr.doCode(chunk);
\r
1734 commandProto.doBlockquote = function (chunk, postProcessing) {
\r
1736 chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
\r
1737 function (totalMatch, newlinesBefore, text, newlinesAfter) {
\r
1738 chunk.before += newlinesBefore;
\r
1739 chunk.after = newlinesAfter + chunk.after;
\r
1743 chunk.before = chunk.before.replace(/(>[ \t]*)$/,
\r
1744 function (totalMatch, blankLine) {
\r
1745 chunk.selection = blankLine + chunk.selection;
\r
1749 chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");
\r
1750 chunk.selection = chunk.selection || "Blockquote";
\r
1752 // The original code uses a regular expression to find out how much of the
\r
1753 // text *directly before* the selection already was a blockquote:
\r
1756 if (chunk.before) {
\r
1757 chunk.before = chunk.before.replace(/\n?$/, "\n");
\r
1759 chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
\r
1760 function (totalMatch) {
\r
1761 chunk.startTag = totalMatch;
\r
1766 // This comes down to:
\r
1767 // Go backwards as many lines a possible, such that each line
\r
1768 // a) starts with ">", or
\r
1769 // b) is almost empty, except for whitespace, or
\r
1770 // c) is preceeded by an unbroken chain of non-empty lines
\r
1771 // leading up to a line that starts with ">" and at least one more character
\r
1772 // and in addition
\r
1773 // d) at least one line fulfills a)
\r
1775 // Since this is essentially a backwards-moving regex, it's susceptible to
\r
1776 // catstrophic backtracking and can cause the browser to hang;
\r
1777 // see e.g. http://meta.stackoverflow.com/questions/9807.
\r
1779 // Hence we replaced this by a simple state machine that just goes through the
\r
1780 // lines and checks for a), b), and c).
\r
1785 if (chunk.before) {
\r
1786 var lines = chunk.before.replace(/\n$/, "").split("\n");
\r
1787 var inChain = false;
\r
1788 for (var i = 0; i < lines.length; i++) {
\r
1791 inChain = inChain && line.length > 0; // c) any non-empty line continues the chain
\r
1792 if (/^>/.test(line)) { // a)
\r
1794 if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain
\r
1796 } else if (/^[ \t]*$/.test(line)) { // b)
\r
1799 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
\r
1802 match += line + "\n";
\r
1804 leftOver += match + line;
\r
1808 if (!/(^|\n)>/.test(match)) { // d)
\r
1809 leftOver += match;
\r
1814 chunk.startTag = match;
\r
1815 chunk.before = leftOver;
\r
1819 if (chunk.after) {
\r
1820 chunk.after = chunk.after.replace(/^\n?/, "\n");
\r
1823 chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
\r
1824 function (totalMatch) {
\r
1825 chunk.endTag = totalMatch;
\r
1830 var replaceBlanksInTags = function (useBracket) {
\r
1832 var replacement = useBracket ? "> " : "";
\r
1834 if (chunk.startTag) {
\r
1835 chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
\r
1836 function (totalMatch, markdown) {
\r
1837 return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
\r
1840 if (chunk.endTag) {
\r
1841 chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
\r
1842 function (totalMatch, markdown) {
\r
1843 return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
\r
1848 if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {
\r
1849 this.wrap(chunk, SETTINGS.lineLength - 2);
\r
1850 chunk.selection = chunk.selection.replace(/^/gm, "> ");
\r
1851 replaceBlanksInTags(true);
\r
1852 chunk.skipLines();
\r
1854 chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
\r
1855 this.unwrap(chunk);
\r
1856 replaceBlanksInTags(false);
\r
1858 if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {
\r
1859 chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
\r
1862 if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {
\r
1863 chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");
\r
1867 chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);
\r
1869 if (!/\n/.test(chunk.selection)) {
\r
1870 chunk.selection = chunk.selection.replace(/^(> *)/,
\r
1871 function (wholeMatch, blanks) {
\r
1872 chunk.startTag += blanks;
\r
1878 commandProto.doCode = function (chunk, postProcessing) {
\r
1880 var hasTextBefore = /\S[ ]*$/.test(chunk.before);
\r
1881 var hasTextAfter = /^[ ]*\S/.test(chunk.after);
\r
1883 // Use 'four space' markdown if the selection is on its own
\r
1884 // line or is multiline.
\r
1885 if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
\r
1887 chunk.before = chunk.before.replace(/[ ]{4}$/,
\r
1888 function (totalMatch) {
\r
1889 chunk.selection = totalMatch + chunk.selection;
\r
1893 var nLinesBack = 1;
\r
1894 var nLinesForward = 1;
\r
1896 if (/\n(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
\r
1899 if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
\r
1900 nLinesForward = 0;
\r
1903 chunk.skipLines(nLinesBack, nLinesForward);
\r
1905 if (!chunk.selection) {
\r
1906 chunk.startTag = " ";
\r
1907 chunk.selection = "enter code here";
\r
1910 if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
\r
1911 chunk.selection = chunk.selection.replace(/^/gm, " ");
\r
1914 chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
\r
1919 // Use backticks (`) to delimit the code block.
\r
1921 chunk.trimWhitespace();
\r
1922 chunk.findTags(/`/, /`/);
\r
1924 if (!chunk.startTag && !chunk.endTag) {
\r
1925 chunk.startTag = chunk.endTag = "`";
\r
1926 if (!chunk.selection) {
\r
1927 chunk.selection = "enter code here";
\r
1930 else if (chunk.endTag && !chunk.startTag) {
\r
1931 chunk.before += chunk.endTag;
\r
1932 chunk.endTag = "";
\r
1935 chunk.startTag = chunk.endTag = "";
\r
1940 commandProto.doList = function (chunk, postProcessing, isNumberedList) {
\r
1942 // These are identical except at the very beginning and end.
\r
1943 // Should probably use the regex extension function to make this clearer.
\r
1944 var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
\r
1945 var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
\r
1947 // The default bullet is a dash but others are possible.
\r
1948 // This has nothing to do with the particular HTML bullet,
\r
1949 // it's just a markdown bullet.
\r
1952 // The number in a numbered list.
\r
1955 // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
\r
1956 var getItemPrefix = function () {
\r
1958 if (isNumberedList) {
\r
1959 prefix = " " + num + ". ";
\r
1963 prefix = " " + bullet + " ";
\r
1968 // Fixes the prefixes of the other list items.
\r
1969 var getPrefixedItem = function (itemText) {
\r
1971 // The numbering flag is unset when called by autoindent.
\r
1972 if (isNumberedList === undefined) {
\r
1973 isNumberedList = /^\s*\d/.test(itemText);
\r
1976 // Renumber/bullet the list element.
\r
1977 itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
\r
1979 return getItemPrefix();
\r
1985 chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
\r
1987 if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {
\r
1988 chunk.before += chunk.startTag;
\r
1989 chunk.startTag = "";
\r
1992 if (chunk.startTag) {
\r
1994 var hasDigits = /\d+[.]/.test(chunk.startTag);
\r
1995 chunk.startTag = "";
\r
1996 chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
\r
1997 this.unwrap(chunk);
\r
1998 chunk.skipLines();
\r
2001 // Have to renumber the bullet points if this is a numbered list.
\r
2002 chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
\r
2004 if (isNumberedList == hasDigits) {
\r
2011 chunk.before = chunk.before.replace(previousItemsRegex,
\r
2012 function (itemText) {
\r
2013 if (/^\s*([*+-])/.test(itemText)) {
\r
2016 nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
\r
2017 return getPrefixedItem(itemText);
\r
2020 if (!chunk.selection) {
\r
2021 chunk.selection = "List item";
\r
2024 var prefix = getItemPrefix();
\r
2026 var nLinesDown = 1;
\r
2028 chunk.after = chunk.after.replace(nextItemsRegex,
\r
2029 function (itemText) {
\r
2030 nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
\r
2031 return getPrefixedItem(itemText);
\r
2034 chunk.trimWhitespace(true);
\r
2035 chunk.skipLines(nLinesUp, nLinesDown, true);
\r
2036 chunk.startTag = prefix;
\r
2037 var spaces = prefix.replace(/./g, " ");
\r
2038 this.wrap(chunk, SETTINGS.lineLength - spaces.length);
\r
2039 chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
\r
2043 commandProto.doHeading = function (chunk, postProcessing) {
\r
2045 // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
\r
2046 chunk.selection = chunk.selection.replace(/\s+/g, " ");
\r
2047 chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
\r
2049 // If we clicked the button with no selected text, we just
\r
2050 // make a level 2 hash header around some default text.
\r
2051 if (!chunk.selection) {
\r
2052 chunk.startTag = "## ";
\r
2053 chunk.selection = "Heading";
\r
2054 chunk.endTag = " ##";
\r
2058 var headerLevel = 0; // The existing header level of the selected text.
\r
2060 // Remove any existing hash heading markdown and save the header level.
\r
2061 chunk.findTags(/#+[ ]*/, /[ ]*#+/);
\r
2062 if (/#+/.test(chunk.startTag)) {
\r
2063 headerLevel = re.lastMatch.length;
\r
2065 chunk.startTag = chunk.endTag = "";
\r
2067 // Try to get the current header level by looking for - and = in the line
\r
2068 // below the selection.
\r
2069 chunk.findTags(null, /\s?(-+|=+)/);
\r
2070 if (/=+/.test(chunk.endTag)) {
\r
2073 if (/-+/.test(chunk.endTag)) {
\r
2077 // Skip to the next line so we can create the header markdown.
\r
2078 chunk.startTag = chunk.endTag = "";
\r
2079 chunk.skipLines(1, 1);
\r
2081 // We make a level 2 header if there is no current header.
\r
2082 // If there is a header level, we substract one from the header level.
\r
2083 // If it's already a level 1 header, it's removed.
\r
2084 var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
\r
2086 if (headerLevelToCreate > 0) {
\r
2088 // The button only creates level 1 and 2 underline headers.
\r
2089 // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner?
\r
2090 var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
\r
2091 var len = chunk.selection.length;
\r
2092 if (len > SETTINGS.lineLength) {
\r
2093 len = SETTINGS.lineLength;
\r
2095 chunk.endTag = "\n";
\r
2097 chunk.endTag += headerChar;
\r
2102 commandProto.doHorizontalRule = function (chunk, postProcessing) {
\r
2103 chunk.startTag = "----------\n";
\r
2104 chunk.selection = "";
\r
2105 chunk.skipLines(2, 1, true);
\r