" + c + "
";
}
);
@@ -1162,7 +1206,7 @@ else
text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&");
// Encode naked <'s
- text = text.replace(/<(?![a-z\/?\$!])/gi, "<");
+ text = text.replace(/<(?![a-z\/?!]|~D)/gi, "<");
return text;
}
@@ -1188,14 +1232,57 @@ else
return text;
}
+ var charInsideUrl = "[-A-Z0-9+&@#/%?=~_|[\\]()!:,.;]",
+ charEndingUrl = "[-A-Z0-9+&@#/%=~_|[\\])]",
+ autoLinkRegex = new RegExp("(=\"|<)?\\b(https?|ftp)(://" + charInsideUrl + "*" + charEndingUrl + ")(?=$|\\W)", "gi"),
+ endCharRegex = new RegExp(charEndingUrl, "i");
+
+ function handleTrailingParens(wholeMatch, lookbehind, protocol, link) {
+ if (lookbehind)
+ return wholeMatch;
+ if (link.charAt(link.length - 1) !== ")")
+ return "<" + protocol + link + ">";
+ var parens = link.match(/[()]/g);
+ var level = 0;
+ for (var i = 0; i < parens.length; i++) {
+ if (parens[i] === "(") {
+ if (level <= 0)
+ level = 1;
+ else
+ level++;
+ }
+ else {
+ level--;
+ }
+ }
+ var tail = "";
+ if (level < 0) {
+ var re = new RegExp("\\){1," + (-level) + "}$");
+ link = link.replace(re, function (trailingParens) {
+ tail = trailingParens;
+ return "";
+ });
+ }
+ if (tail) {
+ var lastChar = link.charAt(link.length - 1);
+ if (!endCharRegex.test(lastChar)) {
+ tail = lastChar + tail;
+ link = link.substr(0, link.length - 1);
+ }
+ }
+ return "<" + protocol + link + ">" + tail;
+ }
+
function _DoAutoLinks(text) {
// note that at this point, all other URL in the text are already hyperlinked as
// *except* for the Insert Hyperlink
http://example.com/ \"optional title\"
", + + quote: "BlockquoteCtrl+Q", + quoteexample: "Blockquote", + + code: "Code SampleCtrl+K", + codeexample: "enter code here", + + image: "Image
Ctrl+G", + imagedescription: "enter image description here", + imagedialog: "
Insert Image
http://example.com/images/diagram.jpg \"optional title\"
", + + olist: "Numbered List
Need free image hosting?Ctrl+O", + ulist: "Bulleted List
Ctrl+U", + litem: "List item", + + heading: "Heading
/
Ctrl+H", + headingexample: "Heading", + + hr: "Horizontal Rule
Ctrl+R", + + undo: "Undo - Ctrl+Z", + redo: "Redo - Ctrl+Y", + redomac: "Redo - Ctrl+Shift+Z", + + help: "Markdown Editing Help" + }; + // ------------------------------------------------------------------- // YOUR CHANGES GO HERE // - // I've tried to localize the things you are likely to change to + // I've tried to localize the things you are likely to change to // this area. // ------------------------------------------------------------------- - // The text that appears on the upper part of the dialog box when - // entering links. - var linkDialogText = "Insert Hyperlink
http://example.com/ \"optional title\"
"; - var imageDialogText = "Insert Image
http://example.com/images/diagram.jpg \"optional title\"
"; - // The default text that appears in the dialog input box when entering // links. var imageDefaultText = "http://"; var linkDefaultText = "http://"; - var defaultHelpHoverTitle = "Markdown Editing Help"; - // ------------------------------------------------------------------- // END OF YOUR CHANGES // ------------------------------------------------------------------- - // help, if given, should have a property "handler", the click handler for the help button, - // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help"). - // If help isn't given, not help button is created. + // options, if given, can have the following properties: + // options.helpButton = { handler: yourEventHandler } + // options.strings = { italicexample: "slanted text" } + // `yourEventHandler` is the click handler for the help button. + // If `options.helpButton` isn't given, not help button is created. + // `options.strings` can have any or all of the same properties as + // `defaultStrings` above, so you can just override some string displayed + // to the user on a case-by-case basis, or translate all strings to + // a different language. + // + // For backwards compatibility reasons, the `options` argument can also + // be just the `helpButton` object, and `strings.help` can also be set via + // `helpButton.title`. This should be considered legacy. // // The constructed editor object has the methods: // - getConverter() returns the markdown converter object that was passed to the constructor // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. - Markdown.Editor = function (markdownConverter, idPostfix, help) { + Markdown.Editor = function (markdownConverter, idPostfix, options) { + + options = options || {}; + + if (typeof options.handler === "function") { //backwards compatible behavior + options = { helpButton: options }; + } + options.strings = options.strings || {}; + if (options.helpButton) { + options.strings.help = options.strings.help || options.helpButton.title; + } + var getString = function (identifier) { return options.strings[identifier] || defaultsStrings[identifier]; } idPostfix = idPostfix || ""; @@ -71,7 +122,7 @@ return; // already initialized panels = new PanelCollection(idPostfix); - var commandManager = new CommandManager(hooks); + var commandManager = new CommandManager(hooks, getString); var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); }); var undoManager, uiManager; @@ -81,9 +132,14 @@ if (uiManager) // not available on the first call uiManager.setUndoRedoButtonStates(); }, panels); + this.textOperation = function (f) { + undoManager.setCommandMode(); + f(); + that.refreshPreview(); + } } - uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help); + uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString); uiManager.setUndoRedoButtonStates(); var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); }; @@ -155,7 +211,7 @@ beforeReplacer = function (s) { that.before += s; return ""; } afterReplacer = function (s) { that.after = s + that.after; return ""; } } - + this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); }; @@ -223,14 +279,14 @@ } }; - // end of Chunks + // end of Chunks // A collection of the important regions on the page. // Cached so we don't have to keep traversing the DOM. // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around // this issue: // Internet explorer has problems with CSS sprite buttons that use HTML - // lists. When you click on the background image "button", IE will + // lists. When you click on the background image "button", IE will // select the non-existent link text and discard the selection in the // textarea. The solution to this is to cache the textarea selection // on the button's mousedown event and set a flag. In the part of the @@ -317,8 +373,10 @@ var flags; // Replace the flags with empty space and store them. - pattern = pattern.replace(/\/([gim]*)$/, ""); - flags = re.$1; + pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) { + flags = flagsPart; + return ""; + }); // Remove the slash delimiters on the regular expression. pattern = pattern.replace(/(^\/|\/$)/g, ""); @@ -510,13 +568,13 @@ var handled = false; - if (event.ctrlKey || event.metaKey) { + if ((event.ctrlKey || event.metaKey) && !event.altKey) { // IE and Opera do not support charCode. var keyCode = event.charCode || event.keyCode; var keyCodeChar = String.fromCharCode(keyCode); - switch (keyCodeChar) { + switch (keyCodeChar.toLowerCase()) { case "y": undoObj.redo(); @@ -573,7 +631,7 @@ setMode("escape"); } else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) { - // 16-20 are shift, etc. + // 16-20 are shift, etc. // 91: left window key // I think this might be a little messed up since there are // a lot of nonprinting keys above 20. @@ -586,7 +644,7 @@ util.addEvent(panels.input, "keypress", function (event) { // keyCode 89: y // keyCode 90: z - if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) { + if ((event.ctrlKey || event.metaKey) && !event.altKey && (event.keyCode == 89 || event.keyCode == 90)) { event.preventDefault(); } }); @@ -717,7 +775,7 @@ if (panels.ieCachedRange) stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange - + panels.ieCachedRange = null; this.setInputAreaSelection(); @@ -963,9 +1021,11 @@ // browser-specific hacks remain here. ui.createBackground = function () { - var background = doc.createElement("div"); + var background = doc.createElement("div"), + style = background.style; + background.className = "wmd-prompt-background"; - style = background.style; + style.position = "absolute"; style.top = "0"; @@ -1035,13 +1095,9 @@ } else { // Fixes common pasting errors. - text = text.replace('http://http://', 'http://'); - text = text.replace('http://https://', 'https://'); - text = text.replace('http://ftp://', 'ftp://'); - - if (text.indexOf('http://') === -1 && text.indexOf('ftp://') === -1 && text.indexOf('https://') === -1) { + text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); + if (!/^(?:https?|ftp):\/\//.test(text)) text = 'http://' + text; - } } dialog.parentNode.removeChild(dialog); @@ -1070,9 +1126,9 @@ dialog.appendChild(question); // The web form container for the text box and buttons. - var form = doc.createElement("form"); + var form = doc.createElement("form"), + style = form.style; form.onsubmit = function () { return close(false); }; - style = form.style; style.padding = "0"; style.margin = "0"; style.cssFloat = "left"; @@ -1156,7 +1212,7 @@ }, 0); }; - function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) { + function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) { var inputBox = panels.input, buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements. @@ -1171,7 +1227,7 @@ util.addEvent(inputBox, keyEvent, function (key) { // Check to see if we have a button key and, if so execute the callback. - if ((key.ctrlKey || key.metaKey) && !key.altKey) { + if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { var keyCode = key.charCode || key.keyCode; var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); @@ -1239,7 +1295,7 @@ var keyCode = key.charCode || key.keyCode; // Character 13 is Enter if (keyCode === 13) { - fakeButton = {}; + var fakeButton = {}; fakeButton.textOp = bindCommand("doAutoindent"); doClick(fakeButton); } @@ -1284,7 +1340,7 @@ // // var link = CreateLinkDialog(); // makeMarkdownLink(link); - // + // // Instead of this straightforward method of handling a // dialog I have to pass any code which would execute // after the dialog is dismissed (e.g. link creation) @@ -1406,33 +1462,33 @@ xPosition += 25; } - buttons.bold = makeButton("wmd-bold-button", "Strong Ctrl+B", "0px", bindCommand("doBold")); - buttons.italic = makeButton("wmd-italic-button", "Emphasis Ctrl+I", "-20px", bindCommand("doItalic")); + buttons.bold = makeButton("wmd-bold-button", getString("bold"), "0px", bindCommand("doBold")); + buttons.italic = makeButton("wmd-italic-button", getString("italic"), "-20px", bindCommand("doItalic")); makeSpacer(1); - buttons.link = makeButton("wmd-link-button", "Hyperlink Ctrl+L", "-40px", bindCommand(function (chunk, postProcessing) { + buttons.link = makeButton("wmd-link-button", getString("link"), "-40px", bindCommand(function (chunk, postProcessing) { return this.doLinkOrImage(chunk, postProcessing, false); })); - buttons.quote = makeButton("wmd-quote-button", "Blockquote
Need free image hosting?Ctrl+Q", "-60px", bindCommand("doBlockquote")); - buttons.code = makeButton("wmd-code-button", "Code SampleCtrl+K", "-80px", bindCommand("doCode")); - buttons.image = makeButton("wmd-image-button", "Image
Ctrl+G", "-100px", bindCommand(function (chunk, postProcessing) { + buttons.quote = makeButton("wmd-quote-button", getString("quote"), "-60px", bindCommand("doBlockquote")); + buttons.code = makeButton("wmd-code-button", getString("code"), "-80px", bindCommand("doCode")); + buttons.image = makeButton("wmd-image-button", getString("image"), "-100px", bindCommand(function (chunk, postProcessing) { return this.doLinkOrImage(chunk, postProcessing, true); })); makeSpacer(2); - buttons.olist = makeButton("wmd-olist-button", "Numbered List
Ctrl+O", "-120px", bindCommand(function (chunk, postProcessing) { + buttons.olist = makeButton("wmd-olist-button", getString("olist"), "-120px", bindCommand(function (chunk, postProcessing) { this.doList(chunk, postProcessing, true); })); - buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List
Ctrl+U", "-140px", bindCommand(function (chunk, postProcessing) { + buttons.ulist = makeButton("wmd-ulist-button", getString("ulist"), "-140px", bindCommand(function (chunk, postProcessing) { this.doList(chunk, postProcessing, false); })); - buttons.heading = makeButton("wmd-heading-button", "Heading
/
Ctrl+H", "-160px", bindCommand("doHeading")); - buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule
Ctrl+R", "-180px", bindCommand("doHorizontalRule")); + buttons.heading = makeButton("wmd-heading-button", getString("heading"), "-160px", bindCommand("doHeading")); + buttons.hr = makeButton("wmd-hr-button", getString("hr"), "-180px", bindCommand("doHorizontalRule")); makeSpacer(3); - buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "-200px", null); + buttons.undo = makeButton("wmd-undo-button", getString("undo"), "-200px", null); buttons.undo.execute = function (manager) { if (manager) manager.undo(); }; var redoTitle = /win/.test(nav.platform.toLowerCase()) ? - "Redo - Ctrl+Y" : - "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms + getString("redo") : + getString("redomac"); // mac and other non-Windows platforms buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null); buttons.redo.execute = function (manager) { if (manager) manager.redo(); }; @@ -1446,7 +1502,7 @@ helpButton.XShift = "-240px"; helpButton.isHelp = true; helpButton.style.right = "0px"; - helpButton.title = helpOptions.title || defaultHelpHoverTitle; + helpButton.title = getString("help"); helpButton.onclick = helpOptions.handler; setupButton(helpButton, true); @@ -1468,8 +1524,9 @@ } - function CommandManager(pluginHooks) { + function CommandManager(pluginHooks, getString) { this.hooks = pluginHooks; + this.getString = getString; } var commandProto = CommandManager.prototype; @@ -1485,10 +1542,11 @@ commandProto.wrap = function (chunk, len) { this.unwrap(chunk); - var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"); + var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"), + that = this; chunk.selection = chunk.selection.replace(regex, function (line, marked) { - if (new re("^" + this.prefixes, "").test(line)) { + if (new re("^" + that.prefixes, "").test(line)) { return line; } return marked + "\n"; @@ -1498,11 +1556,11 @@ }; commandProto.doBold = function (chunk, postProcessing) { - return this.doBorI(chunk, postProcessing, 2, "strong text"); + return this.doBorI(chunk, postProcessing, 2, this.getString("boldexample")); }; commandProto.doItalic = function (chunk, postProcessing) { - return this.doBorI(chunk, postProcessing, 1, "emphasized text"); + return this.doBorI(chunk, postProcessing, 1, this.getString("italicexample")); }; // chunk: The selected region that will be enclosed with */** @@ -1638,7 +1696,7 @@ }); if (title) { title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, ""); - title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); + title = title.replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); } return title ? link + ' "' + title + '"' : link; }); @@ -1650,7 +1708,7 @@ chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); var background; - if (chunk.endTag.length > 1) { + if (chunk.endTag.length > 1 && chunk.startTag.length > 0) { chunk.startTag = chunk.startTag.replace(/!?\[/, ""); chunk.endTag = ""; @@ -1658,6 +1716,12 @@ } else { + + // We're moving start and end tag back into the selection, since (as we're in the else block) we're not + // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the + // link text. linkEnteredCallback takes care of escaping any brackets. + chunk.selection = chunk.startTag + chunk.selection + chunk.endTag; + chunk.startTag = chunk.endTag = ""; if (/\n\n/.test(chunk.selection)) { this.addLinkDef(chunk, null); @@ -1671,8 +1735,26 @@ background.parentNode.removeChild(background); if (link !== null) { - - chunk.startTag = chunk.endTag = ""; + // ( $1 + // [^\\] anything that's not a backslash + // (?:\\\\)* an even number (this includes zero) of backslashes + // ) + // (?= followed by + // [[\]] an opening or closing bracket + // ) + // + // In other words, a non-escaped bracket. These have to be escaped now to make sure they + // don't count as the end of the link or similar. + // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets), + // the bracket in one match may be the "not a backslash" character in the next match, so it + // should not be consumed by the first match. + // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the + // start of the string, so this also works if the selection begins with a bracket. We cannot solve + // this by anchoring with ^, because in the case that the selection starts with two brackets, this + // would mean a zero-width match at the start. Since zero-width matches advance the string position, + // the first bracket could then not act as the "not a backslash" for the second. + chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1); + var linkDef = " [999]: " + properlyEncoded(link); var num = that.addLinkDef(chunk, linkDef); @@ -1681,10 +1763,10 @@ if (!chunk.selection) { if (isImage) { - chunk.selection = "enter image description here"; + chunk.selection = that.getString("imagedescription"); } else { - chunk.selection = "enter link description here"; + chunk.selection = that.getString("linkdescription"); } } } @@ -1695,10 +1777,10 @@ if (isImage) { if (!this.hooks.insertImageDialog(linkEnteredCallback)) - ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback); + ui.prompt(this.getString("imagedialog"), imageDefaultText, linkEnteredCallback); } else { - ui.prompt(linkDialogText, linkDefaultText, linkEnteredCallback); + ui.prompt(this.getString("linkdialog"), linkDefaultText, linkEnteredCallback); } return true; } @@ -1708,11 +1790,24 @@ // at the current indent level. commandProto.doAutoindent = function (chunk, postProcessing) { - var commandMgr = this; + var commandMgr = this, + fakeSelection = false; chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n"); chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n"); chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n"); + + // There's no selection, end the cursor wasn't at the end of the line: + // The user wants to split the current list item / code line / blockquote line + // (for the latter it doesn't really matter) in two. Temporarily select the + // (rest of the) line to achieve this. + if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) { + chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) { + chunk.selection = wholeMatch; + return ""; + }); + fakeSelection = true; + } if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) { if (commandMgr.doList) { @@ -1729,6 +1824,11 @@ commandMgr.doCode(chunk); } } + + if (fakeSelection) { + chunk.after = chunk.selection + chunk.after; + chunk.selection = ""; + } }; commandProto.doBlockquote = function (chunk, postProcessing) { @@ -1747,7 +1847,7 @@ }); chunk.selection = chunk.selection.replace(/^(\s|>)+$/, ""); - chunk.selection = chunk.selection || "Blockquote"; + chunk.selection = chunk.selection || this.getString("quoteexample"); // The original code uses a regular expression to find out how much of the // text *directly before* the selection already was a blockquote: @@ -1893,7 +1993,7 @@ var nLinesBack = 1; var nLinesForward = 1; - if (/\n(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { nLinesBack = 0; } if (/^\n(\t|[ ]{4,})/.test(chunk.after)) { @@ -1904,14 +2004,17 @@ if (!chunk.selection) { chunk.startTag = " "; - chunk.selection = "enter code here"; + chunk.selection = this.getString("codeexample"); } else { if (/^[ ]{0,3}\S/m.test(chunk.selection)) { - chunk.selection = chunk.selection.replace(/^/gm, " "); + if (/\n/.test(chunk.selection)) + chunk.selection = chunk.selection.replace(/^/gm, " "); + else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior + chunk.before += " "; } else { - chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, ""); + chunk.selection = chunk.selection.replace(/^(?:[ ]{4}|[ ]{0,3}\t)/gm, ""); } } } @@ -1924,7 +2027,7 @@ if (!chunk.startTag && !chunk.endTag) { chunk.startTag = chunk.endTag = "`"; if (!chunk.selection) { - chunk.selection = "enter code here"; + chunk.selection = this.getString("codeexample"); } } else if (chunk.endTag && !chunk.startTag) { @@ -2018,7 +2121,7 @@ }); if (!chunk.selection) { - chunk.selection = "List item"; + chunk.selection = this.getString("litem"); } var prefix = getItemPrefix(); @@ -2050,7 +2153,7 @@ // make a level 2 hash header around some default text. if (!chunk.selection) { chunk.startTag = "## "; - chunk.selection = "Heading"; + chunk.selection = this.getString("headingexample"); chunk.endTag = " ##"; return; }