├── LICENSE.txt ├── Markdown.Converter.js ├── Markdown.Editor.js ├── Markdown.Sanitizer.js ├── README.txt ├── demo ├── browser │ ├── demo.css │ └── demo.html └── node │ └── demo.js ├── local └── Markdown.local.fr.js ├── node-pagedown.js ├── package.json ├── resources └── wmd-buttons.psd └── wmd-buttons.png /LICENSE.txt: -------------------------------------------------------------------------------- 1 | A javascript port of Markdown, as used on Stack Overflow 2 | and the rest of Stack Exchange network. 3 | 4 | Largely based on showdown.js by John Fraser (Attacklab). 5 | 6 | Original Markdown Copyright (c) 2004-2005 John Gruber 7 | 8 | 9 | 10 | Original Showdown code copyright (c) 2007 John Fraser 11 | 12 | Modifications and bugfixes (c) 2009 Dana Robinson 13 | Modifications and bugfixes (c) 2009-2014 Stack Exchange Inc. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | 33 | -------------------------------------------------------------------------------- /Markdown.Converter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Markdown; 3 | 4 | if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module 5 | Markdown = exports; 6 | else 7 | Markdown = {}; 8 | 9 | // The following text is included for historical reasons, but should 10 | // be taken with a pinch of salt; it's not all true anymore. 11 | 12 | // 13 | // Wherever possible, Showdown is a straight, line-by-line port 14 | // of the Perl version of Markdown. 15 | // 16 | // This is not a normal parser design; it's basically just a 17 | // series of string substitutions. It's hard to read and 18 | // maintain this way, but keeping Showdown close to the original 19 | // design makes it easier to port new features. 20 | // 21 | // More importantly, Showdown behaves like markdown.pl in most 22 | // edge cases. So web applications can do client-side preview 23 | // in Javascript, and then build identical HTML on the server. 24 | // 25 | // This port needs the new RegExp functionality of ECMA 262, 26 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 27 | // should do fine. Even with the new regular expression features, 28 | // We do a lot of work to emulate Perl's regex functionality. 29 | // The tricky changes in this file mostly have the "attacklab:" 30 | // label. Major or self-explanatory changes don't. 31 | // 32 | // Smart diff tools like Araxis Merge will be able to match up 33 | // this file with markdown.pl in a useful way. A little tweaking 34 | // helps: in a copy of markdown.pl, replace "#" with "//" and 35 | // replace "$text" with "text". Be sure to ignore whitespace 36 | // and line endings. 37 | // 38 | 39 | 40 | // 41 | // Usage: 42 | // 43 | // var text = "Markdown *rocks*."; 44 | // 45 | // var converter = new Markdown.Converter(); 46 | // var html = converter.makeHtml(text); 47 | // 48 | // alert(html); 49 | // 50 | // Note: move the sample code to the bottom of this 51 | // file before uncommenting it. 52 | // 53 | 54 | (function () { 55 | 56 | function identity(x) { return x; } 57 | function returnFalse(x) { return false; } 58 | 59 | function HookCollection() { } 60 | 61 | HookCollection.prototype = { 62 | 63 | chain: function (hookname, func) { 64 | var original = this[hookname]; 65 | if (!original) 66 | throw new Error("unknown hook " + hookname); 67 | 68 | if (original === identity) 69 | this[hookname] = func; 70 | else 71 | this[hookname] = function (text) { 72 | var args = Array.prototype.slice.call(arguments, 0); 73 | args[0] = original.apply(null, args); 74 | return func.apply(null, args); 75 | }; 76 | }, 77 | set: function (hookname, func) { 78 | if (!this[hookname]) 79 | throw new Error("unknown hook " + hookname); 80 | this[hookname] = func; 81 | }, 82 | addNoop: function (hookname) { 83 | this[hookname] = identity; 84 | }, 85 | addFalse: function (hookname) { 86 | this[hookname] = returnFalse; 87 | } 88 | }; 89 | 90 | Markdown.HookCollection = HookCollection; 91 | 92 | // g_urls and g_titles allow arbitrary user-entered strings as keys. This 93 | // caused an exception (and hence stopped the rendering) when the user entered 94 | // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this 95 | // (since no builtin property starts with "s_"). See 96 | // http://meta.stackexchange.com/questions/64655/strange-wmd-bug 97 | // (granted, switching from Array() to Object() alone would have left only __proto__ 98 | // to be a problem) 99 | function SaveHash() { } 100 | SaveHash.prototype = { 101 | set: function (key, value) { 102 | this["s_" + key] = value; 103 | }, 104 | get: function (key) { 105 | return this["s_" + key]; 106 | } 107 | }; 108 | 109 | Markdown.Converter = function (OPTIONS) { 110 | var pluginHooks = this.hooks = new HookCollection(); 111 | 112 | // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link 113 | pluginHooks.addNoop("plainLinkText"); 114 | 115 | // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked 116 | pluginHooks.addNoop("preConversion"); 117 | 118 | // called with the text once all normalizations have been completed (tabs to spaces, line endings, etc.), but before any conversions have 119 | pluginHooks.addNoop("postNormalization"); 120 | 121 | // Called with the text before / after creating block elements like code blocks and lists. Note that this is called recursively 122 | // with inner content, e.g. it's called with the full text, and then only with the content of a blockquote. The inner 123 | // call will receive outdented text. 124 | pluginHooks.addNoop("preBlockGamut"); 125 | pluginHooks.addNoop("postBlockGamut"); 126 | 127 | // called with the text of a single block element before / after the span-level conversions (bold, code spans, etc.) have been made 128 | pluginHooks.addNoop("preSpanGamut"); 129 | pluginHooks.addNoop("postSpanGamut"); 130 | 131 | // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml 132 | pluginHooks.addNoop("postConversion"); 133 | 134 | // 135 | // Private state of the converter instance: 136 | // 137 | 138 | // Global hashes, used by various utility routines 139 | var g_urls; 140 | var g_titles; 141 | var g_html_blocks; 142 | 143 | // Used to track when we're inside an ordered or unordered list 144 | // (see _ProcessListItems() for details): 145 | var g_list_level; 146 | 147 | OPTIONS = OPTIONS || {}; 148 | var asciify = identity, deasciify = identity; 149 | if (OPTIONS.nonAsciiLetters) { 150 | 151 | /* In JavaScript regular expressions, \w only denotes [a-zA-Z0-9_]. 152 | * That's why there's inconsistent handling e.g. with intra-word bolding 153 | * of Japanese words. That's why we do the following if OPTIONS.nonAsciiLetters 154 | * is true: 155 | * 156 | * Before doing bold and italics, we find every instance 157 | * of a unicode word character in the Markdown source that is not 158 | * matched by \w, and the letter "Q". We take the character's code point 159 | * and encode it in base 51, using the "digits" 160 | * 161 | * A, B, ..., P, R, ..., Y, Z, a, b, ..., y, z 162 | * 163 | * delimiting it with "Q" on both sides. For example, the source 164 | * 165 | * > In Chinese, the smurfs are called 藍精靈, meaning "blue spirits". 166 | * 167 | * turns into 168 | * 169 | * > In Chinese, the smurfs are called QNIhQQMOIQQOuUQ, meaning "blue spirits". 170 | * 171 | * Since everything that is a letter in Unicode is now a letter (or 172 | * several letters) in ASCII, \w and \b should always do the right thing. 173 | * 174 | * After the bold/italic conversion, we decode again; since "Q" was encoded 175 | * alongside all non-ascii characters (as "QBfQ"), and the conversion 176 | * will not generate "Q", the only instances of that letter should be our 177 | * encoded characters. And since the conversion will not break words, the 178 | * "Q...Q" should all still be in one piece. 179 | * 180 | * We're using "Q" as the delimiter because it's probably one of the 181 | * rarest characters, and also because I can't think of any special behavior 182 | * that would ever be triggered by this letter (to use a silly example, if we 183 | * delimited with "H" on the left and "P" on the right, then "Ψ" would be 184 | * encoded as "HTTP", which may cause special behavior). The latter would not 185 | * actually be a huge issue for bold/italic, but may be if we later use it 186 | * in other places as well. 187 | * */ 188 | (function () { 189 | var lettersThatJavaScriptDoesNotKnowAndQ = /[Q\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/g; 190 | var cp_Q = "Q".charCodeAt(0); 191 | var cp_A = "A".charCodeAt(0); 192 | var cp_Z = "Z".charCodeAt(0); 193 | var dist_Za = "a".charCodeAt(0) - cp_Z - 1; 194 | 195 | asciify = function(text) { 196 | return text.replace(lettersThatJavaScriptDoesNotKnowAndQ, function (m) { 197 | var c = m.charCodeAt(0); 198 | var s = ""; 199 | var v; 200 | while (c > 0) { 201 | v = (c % 51) + cp_A; 202 | if (v >= cp_Q) 203 | v++; 204 | if (v > cp_Z) 205 | v += dist_Za; 206 | s = String.fromCharCode(v) + s; 207 | c = c / 51 | 0; 208 | } 209 | return "Q" + s + "Q"; 210 | }) 211 | }; 212 | 213 | deasciify = function(text) { 214 | return text.replace(/Q([A-PR-Za-z]{1,3})Q/g, function (m, s) { 215 | var c = 0; 216 | var v; 217 | for (var i = 0; i < s.length; i++) { 218 | v = s.charCodeAt(i); 219 | if (v > cp_Z) 220 | v -= dist_Za; 221 | if (v > cp_Q) 222 | v--; 223 | v -= cp_A; 224 | c = (c * 51) + v; 225 | } 226 | return String.fromCharCode(c); 227 | }) 228 | } 229 | })(); 230 | } 231 | 232 | var _DoItalicsAndBold = OPTIONS.asteriskIntraWordEmphasis ? _DoItalicsAndBold_AllowIntrawordWithAsterisk : _DoItalicsAndBoldStrict; 233 | 234 | this.makeHtml = function (text) { 235 | 236 | // 237 | // Main function. The order in which other subs are called here is 238 | // essential. Link and image substitutions need to happen before 239 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 240 | // and tags get encoded. 241 | // 242 | 243 | // This will only happen if makeHtml on the same converter instance is called from a plugin hook. 244 | // Don't do that. 245 | if (g_urls) 246 | throw new Error("Recursive call to converter.makeHtml"); 247 | 248 | // Create the private state objects. 249 | g_urls = new SaveHash(); 250 | g_titles = new SaveHash(); 251 | g_html_blocks = []; 252 | g_list_level = 0; 253 | 254 | text = pluginHooks.preConversion(text); 255 | 256 | // attacklab: Replace ~ with ~T 257 | // This lets us use tilde as an escape char to avoid md5 hashes 258 | // The choice of character is arbitray; anything that isn't 259 | // magic in Markdown will work. 260 | text = text.replace(/~/g, "~T"); 261 | 262 | // attacklab: Replace $ with ~D 263 | // RegExp interprets $ as a special character 264 | // when it's in a replacement string 265 | text = text.replace(/\$/g, "~D"); 266 | 267 | // Standardize line endings 268 | text = text.replace(/\r\n/g, "\n"); // DOS to Unix 269 | text = text.replace(/\r/g, "\n"); // Mac to Unix 270 | 271 | // Make sure text begins and ends with a couple of newlines: 272 | text = "\n\n" + text + "\n\n"; 273 | 274 | // Convert all tabs to spaces. 275 | text = _Detab(text); 276 | 277 | // Strip any lines consisting only of spaces and tabs. 278 | // This makes subsequent regexen easier to write, because we can 279 | // match consecutive blank lines with /\n+/ instead of something 280 | // contorted like /[ \t]*\n+/ . 281 | text = text.replace(/^[ \t]+$/mg, ""); 282 | 283 | text = pluginHooks.postNormalization(text); 284 | 285 | // Turn block-level HTML blocks into hash entries 286 | text = _HashHTMLBlocks(text); 287 | 288 | // Strip link definitions, store in hashes. 289 | text = _StripLinkDefinitions(text); 290 | 291 | text = _RunBlockGamut(text); 292 | 293 | text = _UnescapeSpecialChars(text); 294 | 295 | // attacklab: Restore dollar signs 296 | text = text.replace(/~D/g, "$$"); 297 | 298 | // attacklab: Restore tildes 299 | text = text.replace(/~T/g, "~"); 300 | 301 | text = pluginHooks.postConversion(text); 302 | 303 | g_html_blocks = g_titles = g_urls = null; 304 | 305 | return text; 306 | }; 307 | 308 | function _StripLinkDefinitions(text) { 309 | // 310 | // Strips link definitions from text, stores the URLs and titles in 311 | // hash references. 312 | // 313 | 314 | // Link defs are in the form: ^[id]: url "optional title" 315 | 316 | /* 317 | text = text.replace(/ 318 | ^[ ]{0,3}\[([^\[\]]+)\]: // id = $1 attacklab: g_tab_width - 1 319 | [ \t]* 320 | \n? // maybe *one* newline 321 | [ \t]* 322 | ? // url = $2 323 | (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below 324 | [ \t]* 325 | \n? // maybe one newline 326 | [ \t]* 327 | ( // (potential) title = $3 328 | (\n*) // any lines skipped = $4 attacklab: lookbehind removed 329 | [ \t]+ 330 | ["(] 331 | (.+?) // title = $5 332 | [")] 333 | [ \t]* 334 | )? // title is optional 335 | (\n+) // subsequent newlines = $6, capturing because they must be put back if the potential title isn't an actual title 336 | /gm, function(){...}); 337 | */ 338 | 339 | text = text.replace(/^[ ]{0,3}\[([^\[\]]+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(\n+)/gm, 340 | function (wholeMatch, m1, m2, m3, m4, m5, m6) { 341 | m1 = m1.toLowerCase(); 342 | g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive 343 | if (m4) { 344 | // Oops, found blank lines, so it's not a title. 345 | // Put back the parenthetical statement we stole. 346 | return m3 + m6; 347 | } else if (m5) { 348 | g_titles.set(m1, m5.replace(/"/g, """)); 349 | } 350 | 351 | // Completely remove the definition from the text 352 | return ""; 353 | } 354 | ); 355 | 356 | return text; 357 | } 358 | 359 | function _HashHTMLBlocks(text) { 360 | 361 | // Hashify HTML blocks: 362 | // We only want to do this for block-level HTML tags, such as headers, 363 | // lists, and tables. That's because we still want to wrap

s around 364 | // "paragraphs" that are wrapped in non-block-level tags, such as anchors, 365 | // phrase emphasis, and spans. The list of tags we're looking for is 366 | // hard-coded: 367 | var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" 368 | var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" 369 | 370 | // First, look for nested blocks, e.g.: 371 | //

372 | //
373 | // tags for inner block must be indented. 374 | //
375 | //
376 | // 377 | // The outermost tags must start at the left margin for this to match, and 378 | // the inner nested divs must be indented. 379 | // We need to do this before the next, more liberal match, because the next 380 | // match will start at the first `
` and stop at the first `
`. 381 | 382 | // attacklab: This regex can be expensive when it fails. 383 | 384 | /* 385 | text = text.replace(/ 386 | ( // save in $1 387 | ^ // start of line (with /m) 388 | <($block_tags_a) // start tag = $2 389 | \b // word break 390 | // attacklab: hack around khtml/pcre bug... 391 | [^\r]*?\n // any number of lines, minimally matching 392 | // the matching end tag 393 | [ \t]* // trailing spaces/tabs 394 | (?=\n+) // followed by a newline 395 | ) // attacklab: there are sentinel newlines at end of document 396 | /gm,function(){...}}; 397 | */ 398 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashMatch); 399 | 400 | // 401 | // Now match more liberally, simply from `\n` to `\n` 402 | // 403 | 404 | /* 405 | text = text.replace(/ 406 | ( // save in $1 407 | ^ // start of line (with /m) 408 | <($block_tags_b) // start tag = $2 409 | \b // word break 410 | // attacklab: hack around khtml/pcre bug... 411 | [^\r]*? // any number of lines, minimally matching 412 | .* // the matching end tag 413 | [ \t]* // trailing spaces/tabs 414 | (?=\n+) // followed by a newline 415 | ) // attacklab: there are sentinel newlines at end of document 416 | /gm,function(){...}}; 417 | */ 418 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashMatch); 419 | 420 | // Special case just for
. It was easier to make a special case than 421 | // to make the other regex more complicated. 422 | 423 | /* 424 | text = text.replace(/ 425 | \n // Starting after a blank line 426 | [ ]{0,3} 427 | ( // save in $1 428 | (<(hr) // start tag = $2 429 | \b // word break 430 | ([^<>])*? 431 | \/?>) // the matching end tag 432 | [ \t]* 433 | (?=\n{2,}) // followed by a blank line 434 | ) 435 | /g,hashMatch); 436 | */ 437 | text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashMatch); 438 | 439 | // Special case for standalone HTML comments: 440 | 441 | /* 442 | text = text.replace(/ 443 | \n\n // Starting after a blank line 444 | [ ]{0,3} // attacklab: g_tab_width - 1 445 | ( // save in $1 446 | -]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackexchange.com/q/95256 448 | > 449 | [ \t]* 450 | (?=\n{2,}) // followed by a blank line 451 | ) 452 | /g,hashMatch); 453 | */ 454 | text = text.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashMatch); 455 | 456 | // PHP and ASP-style processor instructions ( and <%...%>) 457 | 458 | /* 459 | text = text.replace(/ 460 | (?: 461 | \n\n // Starting after a blank line 462 | ) 463 | ( // save in $1 464 | [ ]{0,3} // attacklab: g_tab_width - 1 465 | (?: 466 | <([?%]) // $2 467 | [^\r]*? 468 | \2> 469 | ) 470 | [ \t]* 471 | (?=\n{2,}) // followed by a blank line 472 | ) 473 | /g,hashMatch); 474 | */ 475 | text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashMatch); 476 | 477 | return text; 478 | } 479 | 480 | function hashBlock(text) { 481 | text = text.replace(/(^\n+|\n+$)/g, ""); 482 | // Replace the element text with a marker ("~KxK" where x is its key) 483 | return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n"; 484 | } 485 | 486 | function hashMatch(wholeMatch, m1) { 487 | return hashBlock(m1); 488 | } 489 | 490 | var blockGamutHookCallback = function (t) { return _RunBlockGamut(t); } 491 | 492 | function _RunBlockGamut(text, doNotUnhash, doNotCreateParagraphs) { 493 | // 494 | // These are all the transformations that form block-level 495 | // tags like paragraphs, headers, and list items. 496 | // 497 | 498 | text = pluginHooks.preBlockGamut(text, blockGamutHookCallback); 499 | 500 | text = _DoHeaders(text); 501 | 502 | // Do Horizontal Rules: 503 | var replacement = "
\n"; 504 | text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement); 505 | text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement); 506 | text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement); 507 | 508 | text = _DoLists(text); 509 | text = _DoCodeBlocks(text); 510 | text = _DoBlockQuotes(text); 511 | 512 | text = pluginHooks.postBlockGamut(text, blockGamutHookCallback); 513 | 514 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 515 | // was to escape raw HTML in the original Markdown source. This time, 516 | // we're escaping the markup we've just created, so that we don't wrap 517 | //

tags around block-level tags. 518 | text = _HashHTMLBlocks(text); 519 | 520 | text = _FormParagraphs(text, doNotUnhash, doNotCreateParagraphs); 521 | 522 | return text; 523 | } 524 | 525 | function _RunSpanGamut(text) { 526 | // 527 | // These are all the transformations that occur *within* block-level 528 | // tags like paragraphs, headers, and list items. 529 | // 530 | 531 | text = pluginHooks.preSpanGamut(text); 532 | 533 | text = _DoCodeSpans(text); 534 | text = _EscapeSpecialCharsWithinTagAttributes(text); 535 | text = _EncodeBackslashEscapes(text); 536 | 537 | // Process anchor and image tags. Images must come first, 538 | // because ![foo][f] looks like an anchor. 539 | text = _DoImages(text); 540 | text = _DoAnchors(text); 541 | 542 | // Make links out of things like `` 543 | // Must come after _DoAnchors(), because you can use < and > 544 | // delimiters in inline links like [this](). 545 | text = _DoAutoLinks(text); 546 | 547 | text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now 548 | 549 | text = _EncodeAmpsAndAngles(text); 550 | text = _DoItalicsAndBold(text); 551 | 552 | // Do hard breaks: 553 | text = text.replace(/ +\n/g, "
\n"); 554 | 555 | text = pluginHooks.postSpanGamut(text); 556 | 557 | return text; 558 | } 559 | 560 | function _EscapeSpecialCharsWithinTagAttributes(text) { 561 | // 562 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they 563 | // don't conflict with their use in Markdown for code, italics and strong. 564 | // 565 | 566 | // Build a regex to find HTML tags and comments. See Friedl's 567 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. 568 | 569 | // SE: changed the comment part of the regex 570 | 571 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi; 572 | 573 | text = text.replace(regex, function (wholeMatch) { 574 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`"); 575 | tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackexchange.com/questions/95987 576 | return tag; 577 | }); 578 | 579 | return text; 580 | } 581 | 582 | function _DoAnchors(text) { 583 | 584 | if (text.indexOf("[") === -1) 585 | return text; 586 | 587 | // 588 | // Turn Markdown link shortcuts into XHTML
tags. 589 | // 590 | // 591 | // First, handle reference-style links: [link text] [id] 592 | // 593 | 594 | /* 595 | text = text.replace(/ 596 | ( // wrap whole match in $1 597 | \[ 598 | ( 599 | (?: 600 | \[[^\]]*\] // allow brackets nested one level 601 | | 602 | [^\[] // or anything else 603 | )* 604 | ) 605 | \] 606 | 607 | [ ]? // one optional space 608 | (?:\n[ ]*)? // one optional newline followed by spaces 609 | 610 | \[ 611 | (.*?) // id = $3 612 | \] 613 | ) 614 | ()()()() // pad remaining backreferences 615 | /g, writeAnchorTag); 616 | */ 617 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag); 618 | 619 | // 620 | // Next, inline-style links: [link text](url "optional title") 621 | // 622 | 623 | /* 624 | text = text.replace(/ 625 | ( // wrap whole match in $1 626 | \[ 627 | ( 628 | (?: 629 | \[[^\]]*\] // allow brackets nested one level 630 | | 631 | [^\[\]] // or anything else 632 | )* 633 | ) 634 | \] 635 | \( // literal paren 636 | [ \t]* 637 | () // no id, so leave $3 empty 638 | ? 645 | [ \t]* 646 | ( // $5 647 | (['"]) // quote char = $6 648 | (.*?) // Title = $7 649 | \6 // matching quote 650 | [ \t]* // ignore any spaces/tabs between closing quote and ) 651 | )? // title is optional 652 | \) 653 | ) 654 | /g, writeAnchorTag); 655 | */ 656 | 657 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag); 658 | 659 | // 660 | // Last, handle reference-style shortcuts: [link text] 661 | // These must come last in case you've also got [link test][1] 662 | // or [link test](/foo) 663 | // 664 | 665 | /* 666 | text = text.replace(/ 667 | ( // wrap whole match in $1 668 | \[ 669 | ([^\[\]]+) // link text = $2; can't contain '[' or ']' 670 | \] 671 | ) 672 | ()()()()() // pad rest of backreferences 673 | /g, writeAnchorTag); 674 | */ 675 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); 676 | 677 | return text; 678 | } 679 | 680 | function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) { 681 | if (m7 == undefined) m7 = ""; 682 | var whole_match = m1; 683 | var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs 684 | var link_id = m3.toLowerCase(); 685 | var url = m4; 686 | var title = m7; 687 | 688 | if (url == "") { 689 | if (link_id == "") { 690 | // lower-case and turn embedded newlines into spaces 691 | link_id = link_text.toLowerCase().replace(/ ?\n/g, " "); 692 | } 693 | url = "#" + link_id; 694 | 695 | if (g_urls.get(link_id) != undefined) { 696 | url = g_urls.get(link_id); 697 | if (g_titles.get(link_id) != undefined) { 698 | title = g_titles.get(link_id); 699 | } 700 | } 701 | else { 702 | if (whole_match.search(/\(\s*\)$/m) > -1) { 703 | // Special case for explicit empty url 704 | url = ""; 705 | } else { 706 | return whole_match; 707 | } 708 | } 709 | } 710 | url = attributeSafeUrl(url); 711 | 712 | var result = ""; 721 | 722 | return result; 723 | } 724 | 725 | function _DoImages(text) { 726 | 727 | if (text.indexOf("![") === -1) 728 | return text; 729 | 730 | // 731 | // Turn Markdown image shortcuts into tags. 732 | // 733 | 734 | // 735 | // First, handle reference-style labeled images: ![alt text][id] 736 | // 737 | 738 | /* 739 | text = text.replace(/ 740 | ( // wrap whole match in $1 741 | !\[ 742 | (.*?) // alt text = $2 743 | \] 744 | 745 | [ ]? // one optional space 746 | (?:\n[ ]*)? // one optional newline followed by spaces 747 | 748 | \[ 749 | (.*?) // id = $3 750 | \] 751 | ) 752 | ()()()() // pad rest of backreferences 753 | /g, writeImageTag); 754 | */ 755 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag); 756 | 757 | // 758 | // Next, handle inline images: ![alt text](url "optional title") 759 | // Don't forget: encode * and _ 760 | 761 | /* 762 | text = text.replace(/ 763 | ( // wrap whole match in $1 764 | !\[ 765 | (.*?) // alt text = $2 766 | \] 767 | \s? // One optional whitespace character 768 | \( // literal paren 769 | [ \t]* 770 | () // no id, so leave $3 empty 771 | ? // src url = $4 772 | [ \t]* 773 | ( // $5 774 | (['"]) // quote char = $6 775 | (.*?) // title = $7 776 | \6 // matching quote 777 | [ \t]* 778 | )? // title is optional 779 | \) 780 | ) 781 | /g, writeImageTag); 782 | */ 783 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag); 784 | 785 | return text; 786 | } 787 | 788 | function attributeEncode(text) { 789 | // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title) 790 | // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it) 791 | return text.replace(/>/g, ">").replace(/" + _RunSpanGamut(m1) + "\n\n"; } 850 | ); 851 | 852 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, 853 | function (matchFound, m1) { return "

" + _RunSpanGamut(m1) + "

\n\n"; } 854 | ); 855 | 856 | // atx-style headers: 857 | // # Header 1 858 | // ## Header 2 859 | // ## Header 2 with closing hashes ## 860 | // ... 861 | // ###### Header 6 862 | // 863 | 864 | /* 865 | text = text.replace(/ 866 | ^(\#{1,6}) // $1 = string of #'s 867 | [ \t]* 868 | (.+?) // $2 = Header text 869 | [ \t]* 870 | \#* // optional closing #'s (not counted) 871 | \n+ 872 | /gm, function() {...}); 873 | */ 874 | 875 | text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, 876 | function (wholeMatch, m1, m2) { 877 | var h_level = m1.length; 878 | return "" + _RunSpanGamut(m2) + "\n\n"; 879 | } 880 | ); 881 | 882 | return text; 883 | } 884 | 885 | function _DoLists(text, isInsideParagraphlessListItem) { 886 | // 887 | // Form HTML ordered (numbered) and unordered (bulleted) lists. 888 | // 889 | 890 | // attacklab: add sentinel to hack around khtml/safari bug: 891 | // http://bugs.webkit.org/show_bug.cgi?id=11231 892 | text += "~0"; 893 | 894 | // Re-usable pattern to match any entirel ul or ol list: 895 | 896 | /* 897 | var whole_list = / 898 | ( // $1 = whole list 899 | ( // $2 900 | [ ]{0,3} // attacklab: g_tab_width - 1 901 | ([*+-]|\d+[.]) // $3 = first list item marker 902 | [ \t]+ 903 | ) 904 | [^\r]+? 905 | ( // $4 906 | ~0 // sentinel for workaround; should be $ 907 | | 908 | \n{2,} 909 | (?=\S) 910 | (?! // Negative lookahead for another list item marker 911 | [ \t]* 912 | (?:[*+-]|\d+[.])[ \t]+ 913 | ) 914 | ) 915 | ) 916 | /g 917 | */ 918 | var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 919 | if (g_list_level) { 920 | text = text.replace(whole_list, function (wholeMatch, m1, m2) { 921 | var list = m1; 922 | var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; 923 | var first_number; 924 | if (list_type === "ol") 925 | first_number = parseInt(m2, 10) 926 | 927 | var result = _ProcessListItems(list, list_type, isInsideParagraphlessListItem); 928 | 929 | // Trim any trailing whitespace, to put the closing `` 930 | // up on the preceding line, to get it past the current stupid 931 | // HTML block parser. This is a hack to work around the terrible 932 | // hack that is the HTML block parser. 933 | result = result.replace(/\s+$/, ""); 934 | var opening = "<" + list_type; 935 | if (first_number && first_number !== 1) 936 | opening += " start=\"" + first_number + "\""; 937 | result = opening + ">" + result + "\n"; 938 | return result; 939 | }); 940 | } else { 941 | whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; 942 | text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { 943 | var runup = m1; 944 | var list = m2; 945 | 946 | var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; 947 | 948 | var first_number; 949 | if (list_type === "ol") 950 | first_number = parseInt(m3, 10) 951 | 952 | var result = _ProcessListItems(list, list_type); 953 | var opening = "<" + list_type; 954 | if (first_number && first_number !== 1) 955 | opening += " start=\"" + first_number + "\""; 956 | 957 | result = runup + opening + ">\n" + result + "\n"; 958 | return result; 959 | }); 960 | } 961 | 962 | // attacklab: strip sentinel 963 | text = text.replace(/~0/, ""); 964 | 965 | return text; 966 | } 967 | 968 | var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" }; 969 | 970 | function _ProcessListItems(list_str, list_type, isInsideParagraphlessListItem) { 971 | // 972 | // Process the contents of a single ordered or unordered list, splitting it 973 | // into individual list items. 974 | // 975 | // list_type is either "ul" or "ol". 976 | 977 | // The $g_list_level global keeps track of when we're inside a list. 978 | // Each time we enter a list, we increment it; when we leave a list, 979 | // we decrement. If it's zero, we're not in a list anymore. 980 | // 981 | // We do this because when we're not inside a list, we want to treat 982 | // something like this: 983 | // 984 | // I recommend upgrading to version 985 | // 8. Oops, now this line is treated 986 | // as a sub-list. 987 | // 988 | // As a single paragraph, despite the fact that the second line starts 989 | // with a digit-period-space sequence. 990 | // 991 | // Whereas when we're inside a list (or sub-list), that line will be 992 | // treated as the start of a sub-list. What a kludge, huh? This is 993 | // an aspect of Markdown's syntax that's hard to parse perfectly 994 | // without resorting to mind-reading. Perhaps the solution is to 995 | // change the syntax rules such that sub-lists must start with a 996 | // starting cardinal number; e.g. "1." or "a.". 997 | 998 | g_list_level++; 999 | 1000 | // trim trailing blank lines: 1001 | list_str = list_str.replace(/\n{2,}$/, "\n"); 1002 | 1003 | // attacklab: add sentinel to emulate \z 1004 | list_str += "~0"; 1005 | 1006 | // In the original attacklab showdown, list_type was not given to this function, and anything 1007 | // that matched /[*+-]|\d+[.]/ would just create the next
  • , causing this mismatch: 1008 | // 1009 | // Markdown rendered by WMD rendered by MarkdownSharp 1010 | // ------------------------------------------------------------------ 1011 | // 1. first 1. first 1. first 1012 | // 2. second 2. second 2. second 1013 | // - third 3. third * third 1014 | // 1015 | // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx, 1016 | // with {MARKER} being one of \d+[.] or [*+-], depending on list_type: 1017 | 1018 | /* 1019 | list_str = list_str.replace(/ 1020 | (^[ \t]*) // leading whitespace = $1 1021 | ({MARKER}) [ \t]+ // list marker = $2 1022 | ([^\r]+? // list item text = $3 1023 | (\n+) 1024 | ) 1025 | (?= 1026 | (~0 | \2 ({MARKER}) [ \t]+) 1027 | ) 1028 | /gm, function(){...}); 1029 | */ 1030 | 1031 | var marker = _listItemMarkers[list_type]; 1032 | var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm"); 1033 | var last_item_had_a_double_newline = false; 1034 | list_str = list_str.replace(re, 1035 | function (wholeMatch, m1, m2, m3) { 1036 | var item = m3; 1037 | var leading_space = m1; 1038 | var ends_with_double_newline = /\n\n$/.test(item); 1039 | var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1; 1040 | 1041 | var loose = contains_double_newline || last_item_had_a_double_newline; 1042 | item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true, /* doNotCreateParagraphs = */ !loose); 1043 | 1044 | last_item_had_a_double_newline = ends_with_double_newline; 1045 | return "
  • " + item + "
  • \n"; 1046 | } 1047 | ); 1048 | 1049 | // attacklab: strip sentinel 1050 | list_str = list_str.replace(/~0/g, ""); 1051 | 1052 | g_list_level--; 1053 | return list_str; 1054 | } 1055 | 1056 | function _DoCodeBlocks(text) { 1057 | // 1058 | // Process Markdown `
    ` blocks.
    1059 |             //  
    1060 | 
    1061 |             /*
    1062 |             text = text.replace(/
    1063 |                 (?:\n\n|^)
    1064 |                 (                               // $1 = the code block -- one or more lines, starting with a space/tab
    1065 |                     (?:
    1066 |                         (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
    1067 |                         .*\n+
    1068 |                     )+
    1069 |                 )
    1070 |                 (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
    1071 |             /g ,function(){...});
    1072 |             */
    1073 | 
    1074 |             // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
    1075 |             text += "~0";
    1076 | 
    1077 |             text = text.replace(/(?:\n\n|^\n?)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
    1078 |                 function (wholeMatch, m1, m2) {
    1079 |                     var codeblock = m1;
    1080 |                     var nextChar = m2;
    1081 | 
    1082 |                     codeblock = _EncodeCode(_Outdent(codeblock));
    1083 |                     codeblock = _Detab(codeblock);
    1084 |                     codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
    1085 |                     codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
    1086 | 
    1087 |                     codeblock = "
    " + codeblock + "\n
    "; 1088 | 1089 | return "\n\n" + codeblock + "\n\n" + nextChar; 1090 | } 1091 | ); 1092 | 1093 | // attacklab: strip sentinel 1094 | text = text.replace(/~0/, ""); 1095 | 1096 | return text; 1097 | } 1098 | 1099 | function _DoCodeSpans(text) { 1100 | // 1101 | // * Backtick quotes are used for spans. 1102 | // 1103 | // * You can use multiple backticks as the delimiters if you want to 1104 | // include literal backticks in the code span. So, this input: 1105 | // 1106 | // Just type ``foo `bar` baz`` at the prompt. 1107 | // 1108 | // Will translate to: 1109 | // 1110 | //

    Just type foo `bar` baz at the prompt.

    1111 | // 1112 | // There's no arbitrary limit to the number of backticks you 1113 | // can use as delimters. If you need three consecutive backticks 1114 | // in your code, use four for delimiters, etc. 1115 | // 1116 | // * You can use spaces to get literal backticks at the edges: 1117 | // 1118 | // ... type `` `bar` `` ... 1119 | // 1120 | // Turns to: 1121 | // 1122 | // ... type `bar` ... 1123 | // 1124 | 1125 | /* 1126 | text = text.replace(/ 1127 | (^|[^\\`]) // Character before opening ` can't be a backslash or backtick 1128 | (`+) // $2 = Opening run of ` 1129 | (?!`) // and no more backticks -- match the full run 1130 | ( // $3 = The code block 1131 | [^\r]*? 1132 | [^`] // attacklab: work around lack of lookbehind 1133 | ) 1134 | \2 // Matching closer 1135 | (?!`) 1136 | /gm, function(){...}); 1137 | */ 1138 | 1139 | text = text.replace(/(^|[^\\`])(`+)(?!`)([^\r]*?[^`])\2(?!`)/gm, 1140 | function (wholeMatch, m1, m2, m3, m4) { 1141 | var c = m3; 1142 | c = c.replace(/^([ \t]*)/g, ""); // leading whitespace 1143 | c = c.replace(/[ \t]*$/g, ""); // trailing whitespace 1144 | c = _EncodeCode(c); 1145 | c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs. 1146 | return m1 + "" + c + ""; 1147 | } 1148 | ); 1149 | 1150 | return text; 1151 | } 1152 | 1153 | function _EncodeCode(text) { 1154 | // 1155 | // Encode/escape certain characters inside Markdown code runs. 1156 | // The point is that in code, these characters are literals, 1157 | // and lose their special Markdown meanings. 1158 | // 1159 | // Encode all ampersands; HTML entities are not 1160 | // entities within a Markdown code span. 1161 | text = text.replace(/&/g, "&"); 1162 | 1163 | // Do the angle bracket song and dance: 1164 | text = text.replace(//g, ">"); 1166 | 1167 | // Now, escape characters that are magic in Markdown: 1168 | text = escapeCharacters(text, "\*_{}[]\\", false); 1169 | 1170 | // jj the line above breaks this: 1171 | //--- 1172 | 1173 | //* Item 1174 | 1175 | // 1. Subitem 1176 | 1177 | // special char: * 1178 | //--- 1179 | 1180 | return text; 1181 | } 1182 | 1183 | function _DoItalicsAndBoldStrict(text) { 1184 | 1185 | if (text.indexOf("*") === -1 && text.indexOf("_") === - 1) 1186 | return text; 1187 | 1188 | text = asciify(text); 1189 | 1190 | // must go first: 1191 | 1192 | // (^|[\W_]) Start with a non-letter or beginning of string. Store in \1. 1193 | // (?:(?!\1)|(?=^)) Either the next character is *not* the same as the previous, 1194 | // or we started at the end of the string (in which case the previous 1195 | // group had zero width, so we're still there). Because the next 1196 | // character is the marker, this means that if there are e.g. multiple 1197 | // underscores in a row, we can only match the left-most ones (which 1198 | // prevents foo___bar__ from getting bolded) 1199 | // (\*|_) The marker character itself, asterisk or underscore. Store in \2. 1200 | // \2 The marker again, since bold needs two. 1201 | // (?=\S) The first bolded character cannot be a space. 1202 | // ([^\r]*?\S) The actual bolded string. At least one character, and it cannot *end* 1203 | // with a space either. Note that like in many other places, [^\r] is 1204 | // just a workaround for JS' lack of single-line regexes; it's equivalent 1205 | // to a . in an /s regex, because the string cannot contain any \r (they 1206 | // are removed in the normalizing step). 1207 | // \2\2 The marker character, twice -- end of bold. 1208 | // (?!\2) Not followed by another marker character (ensuring that we match the 1209 | // rightmost two in a longer row)... 1210 | // (?=[\W_]|$) ...but by any other non-word character or the end of string. 1211 | text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)\2(?=\S)([^\r]*?\S)\2\2(?!\2)(?=[\W_]|$)/g, 1212 | "$1$3"); 1213 | 1214 | // This is almost identical to the regex, except 1) there's obviously just one marker 1215 | // character, and 2) the italicized string cannot contain the marker character. 1216 | text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)(?=\S)((?:(?!\2)[^\r])*?\S)\2(?!\2)(?=[\W_]|$)/g, 1217 | "$1$3"); 1218 | 1219 | return deasciify(text); 1220 | } 1221 | 1222 | function _DoItalicsAndBold_AllowIntrawordWithAsterisk(text) { 1223 | 1224 | if (text.indexOf("*") === -1 && text.indexOf("_") === - 1) 1225 | return text; 1226 | 1227 | text = asciify(text); 1228 | 1229 | // must go first: 1230 | // (?=[^\r][*_]|[*_]) Optimization only, to find potentially relevant text portions faster. Minimally slower in Chrome, but much faster in IE. 1231 | // ( Store in \1. This is the last character before the delimiter 1232 | // ^ Either we're at the start of the string (i.e. there is no last character)... 1233 | // | ... or we allow one of the following: 1234 | // (?= (lookahead; we're not capturing this, just listing legal possibilities) 1235 | // \W__ If the delimiter is __, then this last character must be non-word non-underscore (extra-word emphasis only) 1236 | // | 1237 | // (?!\*)[\W_]\*\* If the delimiter is **, then this last character can be non-word non-asterisk (extra-word emphasis)... 1238 | // | 1239 | // \w\*\*\w ...or it can be word/underscore, but only if the first bolded character is such a character as well (intra-word emphasis) 1240 | // ) 1241 | // [^\r] actually capture the character (can't use `.` since it could be \n) 1242 | // ) 1243 | // (\*\*|__) Store in \2: the actual delimiter 1244 | // (?!\2) not followed by the delimiter again (at most one more asterisk/underscore is allowed) 1245 | // (?=\S) the first bolded character can't be a space 1246 | // ( Store in \3: the bolded string 1247 | // 1248 | // (?:| Look at all bolded characters except for the last one. Either that's empty, meaning only a single character was bolded... 1249 | // [^\r]*? ... otherwise take arbitrary characters, minimally matching; that's all bolded characters except for the last *two* 1250 | // (?!\2) the last two characters cannot be the delimiter itself (because that would mean four underscores/asterisks in a row) 1251 | // [^\r] capture the next-to-last bolded character 1252 | // ) 1253 | // (?= lookahead at the very last bolded char and what comes after 1254 | // \S_ for underscore-bolding, it can be any non-space 1255 | // | 1256 | // \w for asterisk-bolding (otherwise the previous alternative would've matched, since \w implies \S), either the last char is word/underscore... 1257 | // | 1258 | // \S\*\*(?:[\W_]|$) ... or it's any other non-space, but in that case the character *after* the delimiter may not be a word character 1259 | // ) 1260 | // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases) 1261 | // ) 1262 | // (?= lookahead; list the legal possibilities for the closing delimiter and its following character 1263 | // __(?:\W|$) for underscore-bolding, the following character (if any) must be non-word non-underscore 1264 | // | 1265 | // \*\*(?:[^*]|$) for asterisk-bolding, any non-asterisk is allowed (note we already ensured above that it's not a word character if the last bolded character wasn't one) 1266 | // ) 1267 | // \2 actually capture the closing delimiter (and make sure that it matches the opening one) 1268 | 1269 | text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W__|(?!\*)[\W_]\*\*|\w\*\*\w)[^\r])(\*\*|__)(?!\2)(?=\S)((?:|[^\r]*?(?!\2)[^\r])(?=\S_|\w|\S\*\*(?:[\W_]|$)).)(?=__(?:\W|$)|\*\*(?:[^*]|$))\2/g, 1270 | "$1$3"); 1271 | 1272 | // now : 1273 | // (?=[^\r][*_]|[*_]) Optimization, see above. 1274 | // ( Store in \1. This is the last character before the delimiter 1275 | // ^ Either we're at the start of the string (i.e. there is no last character)... 1276 | // | ... or we allow one of the following: 1277 | // (?= (lookahead; we're not capturing this, just listing legal possibilities) 1278 | // \W_ If the delimiter is _, then this last character must be non-word non-underscore (extra-word emphasis only) 1279 | // | 1280 | // (?!\*) otherwise, we list two possiblities for * as the delimiter; in either case, the last characters cannot be an asterisk itself 1281 | // (?: 1282 | // [\W_]\* this last character can be non-word (extra-word emphasis)... 1283 | // | 1284 | // \D\*(?=\w)\D ...or it can be word (otherwise the first alternative would've matched), but only if 1285 | // a) the first italicized character is such a character as well (intra-word emphasis), and 1286 | // b) neither character on either side of the asterisk is a digit 1287 | // ) 1288 | // ) 1289 | // [^\r] actually capture the character (can't use `.` since it could be \n) 1290 | // ) 1291 | // (\*|_) Store in \2: the actual delimiter 1292 | // (?!\2\2\2) not followed by more than two more instances of the delimiter 1293 | // (?=\S) the first italicized character can't be a space 1294 | // ( Store in \3: the italicized string 1295 | // (?:(?!\2)[^\r])*? arbitrary characters except for the delimiter itself, minimally matching 1296 | // (?= lookahead at the very last italicized char and what comes after 1297 | // [^\s_]_ for underscore-italicizing, it can be any non-space non-underscore 1298 | // | 1299 | // (?=\w)\D\*\D for asterisk-italicizing, either the last char is word/underscore *and* neither character on either side of the asterisk is a digit... 1300 | // | 1301 | // [^\s*]\*(?:[\W_]|$) ... or that last char is any other non-space non-asterisk, but then the character after the delimiter (if any) must be non-word 1302 | // ) 1303 | // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases) 1304 | // ) 1305 | // (?= lookahead; list the legal possibilities for the closing delimiter and its following character 1306 | // _(?:\W|$) for underscore-italicizing, the following character (if any) must be non-word non-underscore 1307 | // | 1308 | // \*(?:[^*]|$) for asterisk-italicizing, any non-asterisk is allowed; all other restrictions have already been ensured in the previous lookahead 1309 | // ) 1310 | // \2 actually capture the closing delimiter (and make sure that it matches the opening one) 1311 | 1312 | text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W_|(?!\*)(?:[\W_]\*|\D\*(?=\w)\D))[^\r])(\*|_)(?!\2\2\2)(?=\S)((?:(?!\2)[^\r])*?(?=[^\s_]_|(?=\w)\D\*\D|[^\s*]\*(?:[\W_]|$)).)(?=_(?:\W|$)|\*(?:[^*]|$))\2/g, 1313 | "$1$3"); 1314 | 1315 | return deasciify(text); 1316 | } 1317 | 1318 | 1319 | function _DoBlockQuotes(text) { 1320 | 1321 | /* 1322 | text = text.replace(/ 1323 | ( // Wrap whole match in $1 1324 | ( 1325 | ^[ \t]*>[ \t]? // '>' at the start of a line 1326 | .+\n // rest of the first line 1327 | (.+\n)* // subsequent consecutive lines 1328 | \n* // blanks 1329 | )+ 1330 | ) 1331 | /gm, function(){...}); 1332 | */ 1333 | 1334 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, 1335 | function (wholeMatch, m1) { 1336 | var bq = m1; 1337 | 1338 | // attacklab: hack around Konqueror 3.5.4 bug: 1339 | // "----------bug".replace(/^-/g,"") == "bug" 1340 | 1341 | bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting 1342 | 1343 | // attacklab: clean up hack 1344 | bq = bq.replace(/~0/g, ""); 1345 | 1346 | bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines 1347 | bq = _RunBlockGamut(bq); // recurse 1348 | 1349 | bq = bq.replace(/(^|\n)/g, "$1 "); 1350 | // These leading spaces screw with
     content, so we need to fix that:
    1351 |                     bq = bq.replace(
    1352 |                             /(\s*
    [^\r]+?<\/pre>)/gm,
    1353 |                         function (wholeMatch, m1) {
    1354 |                             var pre = m1;
    1355 |                             // attacklab: hack around Konqueror 3.5.4 bug:
    1356 |                             pre = pre.replace(/^  /mg, "~0");
    1357 |                             pre = pre.replace(/~0/g, "");
    1358 |                             return pre;
    1359 |                         });
    1360 | 
    1361 |                     return hashBlock("
    \n" + bq + "\n
    "); 1362 | } 1363 | ); 1364 | return text; 1365 | } 1366 | 1367 | function _FormParagraphs(text, doNotUnhash, doNotCreateParagraphs) { 1368 | // 1369 | // Params: 1370 | // $text - string to process with html

    tags 1371 | // 1372 | 1373 | // Strip leading and trailing lines: 1374 | text = text.replace(/^\n+/g, ""); 1375 | text = text.replace(/\n+$/g, ""); 1376 | 1377 | var grafs = text.split(/\n{2,}/g); 1378 | var grafsOut = []; 1379 | 1380 | var markerRe = /~K(\d+)K/; 1381 | 1382 | // 1383 | // Wrap

    tags. 1384 | // 1385 | var end = grafs.length; 1386 | for (var i = 0; i < end; i++) { 1387 | var str = grafs[i]; 1388 | 1389 | // if this is an HTML marker, copy it 1390 | if (markerRe.test(str)) { 1391 | grafsOut.push(str); 1392 | } 1393 | else if (/\S/.test(str)) { 1394 | str = _RunSpanGamut(str); 1395 | str = str.replace(/^([ \t]*)/g, doNotCreateParagraphs ? "" : "

    "); 1396 | if (!doNotCreateParagraphs) 1397 | str += "

    " 1398 | grafsOut.push(str); 1399 | } 1400 | 1401 | } 1402 | // 1403 | // Unhashify HTML blocks 1404 | // 1405 | if (!doNotUnhash) { 1406 | end = grafsOut.length; 1407 | for (var i = 0; i < end; i++) { 1408 | var foundAny = true; 1409 | while (foundAny) { // we may need several runs, since the data may be nested 1410 | foundAny = false; 1411 | grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { 1412 | foundAny = true; 1413 | return g_html_blocks[id]; 1414 | }); 1415 | } 1416 | } 1417 | } 1418 | return grafsOut.join("\n\n"); 1419 | } 1420 | 1421 | function _EncodeAmpsAndAngles(text) { 1422 | // Smart processing for ampersands and angle brackets that need to be encoded. 1423 | 1424 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1425 | // http://bumppo.net/projects/amputator/ 1426 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); 1427 | 1428 | // Encode naked <'s 1429 | text = text.replace(/<(?![a-z\/?!]|~D)/gi, "<"); 1430 | 1431 | return text; 1432 | } 1433 | 1434 | function _EncodeBackslashEscapes(text) { 1435 | // 1436 | // Parameter: String. 1437 | // Returns: The string, with after processing the following backslash 1438 | // escape sequences. 1439 | // 1440 | 1441 | // attacklab: The polite way to do this is with the new 1442 | // escapeCharacters() function: 1443 | // 1444 | // text = escapeCharacters(text,"\\",true); 1445 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1446 | // 1447 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1448 | // as an optimization for Firefox. This function gets called a LOT. 1449 | 1450 | text = text.replace(/\\(\\)/g, escapeCharacters_callback); 1451 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); 1452 | return text; 1453 | } 1454 | 1455 | var charInsideUrl = "[-A-Z0-9+&@#/%?=~_|[\\]()!:,.;]", 1456 | charEndingUrl = "[-A-Z0-9+&@#/%=~_|[\\])]", 1457 | autoLinkRegex = new RegExp("(=\"|<)?\\b(https?|ftp)(://" + charInsideUrl + "*" + charEndingUrl + ")(?=$|\\W)", "gi"), 1458 | endCharRegex = new RegExp(charEndingUrl, "i"); 1459 | 1460 | function handleTrailingParens(wholeMatch, lookbehind, protocol, link) { 1461 | if (lookbehind) 1462 | return wholeMatch; 1463 | if (link.charAt(link.length - 1) !== ")") 1464 | return "<" + protocol + link + ">"; 1465 | var parens = link.match(/[()]/g); 1466 | var level = 0; 1467 | for (var i = 0; i < parens.length; i++) { 1468 | if (parens[i] === "(") { 1469 | if (level <= 0) 1470 | level = 1; 1471 | else 1472 | level++; 1473 | } 1474 | else { 1475 | level--; 1476 | } 1477 | } 1478 | var tail = ""; 1479 | if (level < 0) { 1480 | var re = new RegExp("\\){1," + (-level) + "}$"); 1481 | link = link.replace(re, function (trailingParens) { 1482 | tail = trailingParens; 1483 | return ""; 1484 | }); 1485 | } 1486 | if (tail) { 1487 | var lastChar = link.charAt(link.length - 1); 1488 | if (!endCharRegex.test(lastChar)) { 1489 | tail = lastChar + tail; 1490 | link = link.substr(0, link.length - 1); 1491 | } 1492 | } 1493 | return "<" + protocol + link + ">" + tail; 1494 | } 1495 | 1496 | function _DoAutoLinks(text) { 1497 | 1498 | // note that at this point, all other URL in the text are already hyperlinked as
    1499 | // *except* for the case 1500 | 1501 | // automatically add < and > around unadorned raw hyperlinks 1502 | // must be preceded by a non-word character (and not by =" or <) and followed by non-word/EOF character 1503 | // simulating the lookbehind in a consuming way is okay here, since a URL can neither and with a " nor 1504 | // with a <, so there is no risk of overlapping matches. 1505 | text = text.replace(autoLinkRegex, handleTrailingParens); 1506 | 1507 | // autolink anything like 1508 | 1509 | 1510 | var replacer = function (wholematch, m1) { 1511 | var url = attributeSafeUrl(m1); 1512 | 1513 | return "" + pluginHooks.plainLinkText(m1) + ""; 1514 | }; 1515 | text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); 1516 | 1517 | // Email addresses: 1518 | /* 1519 | text = text.replace(/ 1520 | < 1521 | (?:mailto:)? 1522 | ( 1523 | [-.\w]+ 1524 | \@ 1525 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1526 | ) 1527 | > 1528 | /gi, _DoAutoLinks_callback()); 1529 | */ 1530 | 1531 | /* disabling email autolinking, since we don't do that on the server, either 1532 | text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, 1533 | function(wholeMatch,m1) { 1534 | return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); 1535 | } 1536 | ); 1537 | */ 1538 | return text; 1539 | } 1540 | 1541 | function _UnescapeSpecialChars(text) { 1542 | // 1543 | // Swap back in all the special characters we've hidden. 1544 | // 1545 | text = text.replace(/~E(\d+)E/g, 1546 | function (wholeMatch, m1) { 1547 | var charCodeToReplace = parseInt(m1); 1548 | return String.fromCharCode(charCodeToReplace); 1549 | } 1550 | ); 1551 | return text; 1552 | } 1553 | 1554 | function _Outdent(text) { 1555 | // 1556 | // Remove one level of line-leading tabs or spaces 1557 | // 1558 | 1559 | // attacklab: hack around Konqueror 3.5.4 bug: 1560 | // "----------bug".replace(/^-/g,"") == "bug" 1561 | 1562 | text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width 1563 | 1564 | // attacklab: clean up hack 1565 | text = text.replace(/~0/g, "") 1566 | 1567 | return text; 1568 | } 1569 | 1570 | function _Detab(text) { 1571 | if (!/\t/.test(text)) 1572 | return text; 1573 | 1574 | var spaces = [" ", " ", " ", " "], 1575 | skew = 0, 1576 | v; 1577 | 1578 | return text.replace(/[\n\t]/g, function (match, offset) { 1579 | if (match === "\n") { 1580 | skew = offset + 1; 1581 | return match; 1582 | } 1583 | v = (offset - skew) % 4; 1584 | skew = offset + 1; 1585 | return spaces[v]; 1586 | }); 1587 | } 1588 | 1589 | // 1590 | // attacklab: Utility functions 1591 | // 1592 | 1593 | function attributeSafeUrl(url) { 1594 | url = attributeEncode(url); 1595 | url = escapeCharacters(url, "*_:()[]") 1596 | return url; 1597 | } 1598 | 1599 | function escapeCharacters(text, charsToEscape, afterBackslash) { 1600 | // First we have to escape the escape characters so that 1601 | // we can build a character class out of them 1602 | var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; 1603 | 1604 | if (afterBackslash) { 1605 | regexString = "\\\\" + regexString; 1606 | } 1607 | 1608 | var regex = new RegExp(regexString, "g"); 1609 | text = text.replace(regex, escapeCharacters_callback); 1610 | 1611 | return text; 1612 | } 1613 | 1614 | 1615 | function escapeCharacters_callback(wholeMatch, m1) { 1616 | var charCodeToEscape = m1.charCodeAt(0); 1617 | return "~E" + charCodeToEscape + "E"; 1618 | } 1619 | 1620 | }; // end of the Markdown.Converter constructor 1621 | 1622 | })(); -------------------------------------------------------------------------------- /Markdown.Editor.js: -------------------------------------------------------------------------------- 1 | // needs Markdown.Converter.js at the moment 2 | 3 | (function () { 4 | 5 | var util = {}, 6 | position = {}, 7 | ui = {}, 8 | doc = window.document, 9 | re = window.RegExp, 10 | nav = window.navigator, 11 | SETTINGS = { lineLength: 72 }, 12 | 13 | // Used to work around some browser bugs where we can't use feature testing. 14 | uaSniffed = { 15 | isIE: /msie/.test(nav.userAgent.toLowerCase()), 16 | isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()), 17 | isOpera: /opera/.test(nav.userAgent.toLowerCase()) 18 | }; 19 | 20 | var defaultsStrings = { 21 | bold: "Strong Ctrl+B", 22 | boldexample: "strong text", 23 | 24 | italic: "Emphasis Ctrl+I", 25 | italicexample: "emphasized text", 26 | 27 | link: "Hyperlink Ctrl+L", 28 | linkdescription: "enter link description here", 29 | linkdialog: "

    Insert Hyperlink

    http://example.com/ \"optional title\"

    ", 30 | 31 | quote: "Blockquote
    Ctrl+Q", 32 | quoteexample: "Blockquote", 33 | 34 | code: "Code Sample
     Ctrl+K",
      35 |         codeexample: "enter code here",
      36 | 
      37 |         image: "Image  Ctrl+G",
      38 |         imagedescription: "enter image description here",
      39 |         imagedialog: "

    Insert Image

    http://example.com/images/diagram.jpg \"optional title\"

    Need
    free image hosting?

    ", 40 | 41 | olist: "Numbered List
      Ctrl+O", 42 | ulist: "Bulleted List
        Ctrl+U", 43 | litem: "List item", 44 | 45 | heading: "Heading

        /

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



      • "; 62 | var match; 63 | var tagpaired = []; 64 | var tagremove = []; 65 | var needsRemoval = false; 66 | 67 | // loop through matched tags in forward order 68 | for (var ctag = 0; ctag < tagcount; ctag++) { 69 | tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1"); 70 | // skip any already paired tags 71 | // and skip tags in our ignore list; assume they're self-closed 72 | if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1) 73 | continue; 74 | 75 | tag = tags[ctag]; 76 | match = -1; 77 | 78 | if (!/^<\//.test(tag)) { 79 | // this is an opening tag 80 | // search forwards (next tags), look for closing tags 81 | for (var ntag = ctag + 1; ntag < tagcount; ntag++) { 82 | if (!tagpaired[ntag] && tags[ntag] == "") { 83 | match = ntag; 84 | break; 85 | } 86 | } 87 | } 88 | 89 | if (match == -1) 90 | needsRemoval = tagremove[ctag] = true; // mark for removal 91 | else 92 | tagpaired[match] = true; // mark paired 93 | } 94 | 95 | if (!needsRemoval) 96 | return html; 97 | 98 | // delete all orphaned tags from the string 99 | 100 | var ctag = 0; 101 | html = html.replace(re, function (match) { 102 | var res = tagremove[ctag] ? "" : match; 103 | ctag++; 104 | return res; 105 | }); 106 | return html; 107 | } 108 | })(); 109 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ujifgc/pagedown/39d7468a82f7462177df2870449135cf315f8d25/README.txt -------------------------------------------------------------------------------- /demo/browser/demo.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | background-color: White; 4 | font-family: sans-serif; 5 | } 6 | 7 | blockquote { 8 | border-left: 2px dotted #888; 9 | padding-left: 5px; 10 | background: #d0f0ff; 11 | } 12 | 13 | .wmd-panel 14 | { 15 | margin-left: 25%; 16 | margin-right: 25%; 17 | width: 50%; 18 | min-width: 500px; 19 | } 20 | 21 | .wmd-button-bar 22 | { 23 | width: 100%; 24 | background-color: Silver; 25 | } 26 | 27 | .wmd-input 28 | { 29 | height: 300px; 30 | width: 100%; 31 | background-color: Gainsboro; 32 | border: 1px solid DarkGray; 33 | } 34 | 35 | .wmd-preview 36 | { 37 | background-color: #c0e0ff; 38 | } 39 | 40 | .wmd-button-row 41 | { 42 | position: relative; 43 | margin-left: 5px; 44 | margin-right: 5px; 45 | margin-bottom: 5px; 46 | margin-top: 10px; 47 | padding: 0px; 48 | height: 20px; 49 | } 50 | 51 | .wmd-spacer 52 | { 53 | width: 1px; 54 | height: 20px; 55 | margin-left: 14px; 56 | 57 | position: absolute; 58 | background-color: Silver; 59 | display: inline-block; 60 | list-style: none; 61 | } 62 | 63 | .wmd-button { 64 | width: 20px; 65 | height: 20px; 66 | padding-left: 2px; 67 | padding-right: 3px; 68 | position: absolute; 69 | display: inline-block; 70 | list-style: none; 71 | cursor: pointer; 72 | } 73 | 74 | .wmd-button > span { 75 | background-image: url(../../wmd-buttons.png); 76 | background-repeat: no-repeat; 77 | background-position: 0px 0px; 78 | width: 20px; 79 | height: 20px; 80 | display: inline-block; 81 | } 82 | 83 | .wmd-spacer1 84 | { 85 | left: 50px; 86 | } 87 | .wmd-spacer2 88 | { 89 | left: 175px; 90 | } 91 | .wmd-spacer3 92 | { 93 | left: 300px; 94 | } 95 | 96 | 97 | 98 | 99 | .wmd-prompt-background 100 | { 101 | background-color: Black; 102 | } 103 | 104 | .wmd-prompt-dialog 105 | { 106 | border: 1px solid #999999; 107 | background-color: #F5F5F5; 108 | } 109 | 110 | .wmd-prompt-dialog > div { 111 | font-size: 0.8em; 112 | font-family: arial, helvetica, sans-serif; 113 | } 114 | 115 | 116 | .wmd-prompt-dialog > form > input[type="text"] { 117 | border: 1px solid #999999; 118 | color: black; 119 | } 120 | 121 | .wmd-prompt-dialog > form > input[type="button"]{ 122 | border: 1px solid #888888; 123 | font-family: trebuchet MS, helvetica, sans-serif; 124 | font-size: 0.8em; 125 | font-weight: bold; 126 | } 127 | -------------------------------------------------------------------------------- /demo/browser/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PageDown Demo Page 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
        17 |
        18 | 36 |
        37 |
        38 | 39 |

        40 | 41 |
        42 |
        43 | 62 |
        63 |
        64 | 65 | 66 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /demo/node/demo.js: -------------------------------------------------------------------------------- 1 | // NOTE: This is just a demo -- in a production environment, 2 | // be sure to spend a few more thoughts on sanitizing user input. 3 | // (also, you probably wouldn't use a get request) 4 | 5 | var http = require("http"), 6 | url = require("url"), 7 | querystring = require("querystring"), 8 | Converter = require("../../Markdown.Converter").Converter, 9 | getSanitizingConverter = require("../../Markdown.Sanitizer").getSanitizingConverter, 10 | conv = new Converter(), 11 | saneConv = getSanitizingConverter(); 12 | 13 | http.createServer(function (req, res) { 14 | 15 | var route = url.parse(req.url); 16 | if (route.pathname !== "/") { 17 | res.writeHead(404); 18 | res.end("Page not found"); 19 | return; 20 | } 21 | 22 | var query = querystring.parse(route.query); 23 | 24 | res.writeHead(200, { "Content-type": "text/html" }); 25 | res.write(""); 26 | 27 | var markdown = query.md || "## Hello!\n\nI'm walking\n\nVisit [Stack Overflow](http://stackoverflow.com)\n\nThis is never closed!"; 28 | 29 | res.write("

        Your output, sanitized:

        \n" + saneConv.makeHtml(markdown)) 30 | res.write("

        Your output, unsanitized:

        \n" + conv.makeHtml(markdown)) 31 | 32 | res.write( 33 | "

        Enter Markdown

        \n" + 34 | "
        " + 35 | "
        " + 38 | "" + 39 | "
        " 40 | ); 41 | 42 | res.end(""); 43 | 44 | }).listen(8000); 45 | -------------------------------------------------------------------------------- /local/Markdown.local.fr.js: -------------------------------------------------------------------------------- 1 | // Usage: 2 | // 3 | // var myConverter = new Markdown.Editor(myConverter, null, { strings: Markdown.local.fr }); 4 | 5 | (function () { 6 | Markdown.local = Markdown.local || {}; 7 | Markdown.local.fr = { 8 | bold: "Gras Ctrl+B", 9 | boldexample: "texte en gras", 10 | 11 | italic: "Italique Ctrl+I", 12 | italicexample: "texte en italique", 13 | 14 | link: "Hyperlien Ctrl+L", 15 | linkdescription: "description de l'hyperlien", 16 | linkdialog: "

        Insérer un hyperlien

        http://example.com/ \"titre optionnel\"

        ", 17 | 18 | quote: "Citation
        Ctrl+Q", 19 | quoteexample: "Citation", 20 | 21 | code: "Extrait de code
         Ctrl+K",
        22 |         codeexample: "votre extrait de code",
        23 | 
        24 |         image: "Image  Ctrl+G",
        25 |         imagedescription: "description de l'image",
        26 |         imagedialog: "

        Insérer une image

        http://example.com/images/diagram.jpg \"titre optionnel\"

        Vous chercher un hébergement d'image grauit ?

        ", 27 | 28 | olist: "Liste numérotée
          Ctrl+O", 29 | ulist: "Liste à point
            Ctrl+U", 30 | litem: "Elément de liste", 31 | 32 | heading: "Titre

            /

            Ctrl+H", 33 | headingexample: "Titre", 34 | 35 | hr: "Trait horizontal
            Ctrl+R", 36 | 37 | undo: "Annuler - Ctrl+Z", 38 | redo: "Refaire - Ctrl+Y", 39 | redomac: "Refaire - Ctrl+Shift+Z", 40 | 41 | help: "Aide sur Markdown" 42 | }; 43 | })(); -------------------------------------------------------------------------------- /node-pagedown.js: -------------------------------------------------------------------------------- 1 | exports.Converter = require("./Markdown.Converter").Converter; 2 | exports.getSanitizingConverter = require("./Markdown.Sanitizer").getSanitizingConverter; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pagedown", 3 | "version": "1.1.0", 4 | "description": "markdown converter, based on showdown", 5 | "repository": { "type": "hg", "url": "https://code.google.com/p/pagedown/" }, 6 | "keywords": ["markdown"], 7 | "license": "MIT", 8 | "files": ["Markdown.Converter.js", "Markdown.Sanitizer.js", "node-pagedown.js"], 9 | "main": "node-pagedown.js", 10 | "bugs": "http://code.google.com/p/pagedown/issues/list", 11 | "homepage": "http://code.google.com/p/pagedown/wiki/PageDown" 12 | } 13 | -------------------------------------------------------------------------------- /resources/wmd-buttons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ujifgc/pagedown/39d7468a82f7462177df2870449135cf315f8d25/resources/wmd-buttons.psd -------------------------------------------------------------------------------- /wmd-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ujifgc/pagedown/39d7468a82f7462177df2870449135cf315f8d25/wmd-buttons.png --------------------------------------------------------------------------------