├── .github └── FUNDING.yml ├── Markdown.Converter.js ├── Markdown.Editor.js ├── Markdown.Extra.js ├── Markdown.Sanitizer.js ├── README.md ├── cmunrb.otf ├── cmunrm.otf ├── favicon.ico ├── index.html └── mathjax-editing_writing.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [josephernest] 2 | custom: https://afewthingz.com 3 | -------------------------------------------------------------------------------- /Markdown.Converter.js: -------------------------------------------------------------------------------- 1 | //modification JB: \pagebreak 2 | 3 | "use strict"; 4 | var Markdown; 5 | 6 | if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module 7 | Markdown = exports; 8 | else 9 | Markdown = {}; 10 | 11 | // The following text is included for historical reasons, but should 12 | // be taken with a pinch of salt; it's not all true anymore. 13 | 14 | // 15 | // Wherever possible, Showdown is a straight, line-by-line port 16 | // of the Perl version of Markdown. 17 | // 18 | // This is not a normal parser design; it's basically just a 19 | // series of string substitutions. It's hard to read and 20 | // maintain this way, but keeping Showdown close to the original 21 | // design makes it easier to port new features. 22 | // 23 | // More importantly, Showdown behaves like markdown.pl in most 24 | // edge cases. So web applications can do client-side preview 25 | // in Javascript, and then build identical HTML on the server. 26 | // 27 | // This port needs the new RegExp functionality of ECMA 262, 28 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 29 | // should do fine. Even with the new regular expression features, 30 | // We do a lot of work to emulate Perl's regex functionality. 31 | // The tricky changes in this file mostly have the "attacklab:" 32 | // label. Major or self-explanatory changes don't. 33 | // 34 | // Smart diff tools like Araxis Merge will be able to match up 35 | // this file with markdown.pl in a useful way. A little tweaking 36 | // helps: in a copy of markdown.pl, replace "#" with "//" and 37 | // replace "$text" with "text". Be sure to ignore whitespace 38 | // and line endings. 39 | // 40 | 41 | 42 | // 43 | // Usage: 44 | // 45 | // var text = "Markdown *rocks*."; 46 | // 47 | // var converter = new Markdown.Converter(); 48 | // var html = converter.makeHtml(text); 49 | // 50 | // alert(html); 51 | // 52 | // Note: move the sample code to the bottom of this 53 | // file before uncommenting it. 54 | // 55 | 56 | (function () { 57 | 58 | function identity(x) { return x; } 59 | function returnFalse(x) { return false; } 60 | 61 | function HookCollection() { } 62 | 63 | HookCollection.prototype = { 64 | 65 | chain: function (hookname, func) { 66 | var original = this[hookname]; 67 | if (!original) 68 | throw new Error("unknown hook " + hookname); 69 | 70 | if (original === identity) 71 | this[hookname] = func; 72 | else 73 | this[hookname] = function (text) { 74 | var args = Array.prototype.slice.call(arguments, 0); 75 | args[0] = original.apply(null, args); 76 | return func.apply(null, args); 77 | }; 78 | }, 79 | set: function (hookname, func) { 80 | if (!this[hookname]) 81 | throw new Error("unknown hook " + hookname); 82 | this[hookname] = func; 83 | }, 84 | addNoop: function (hookname) { 85 | this[hookname] = identity; 86 | }, 87 | addFalse: function (hookname) { 88 | this[hookname] = returnFalse; 89 | } 90 | }; 91 | 92 | Markdown.HookCollection = HookCollection; 93 | 94 | // g_urls and g_titles allow arbitrary user-entered strings as keys. This 95 | // caused an exception (and hence stopped the rendering) when the user entered 96 | // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this 97 | // (since no builtin property starts with "s_"). See 98 | // http://meta.stackexchange.com/questions/64655/strange-wmd-bug 99 | // (granted, switching from Array() to Object() alone would have left only __proto__ 100 | // to be a problem) 101 | function SaveHash() { } 102 | SaveHash.prototype = { 103 | set: function (key, value) { 104 | this["s_" + key] = value; 105 | }, 106 | get: function (key) { 107 | return this["s_" + key]; 108 | } 109 | }; 110 | 111 | Markdown.Converter = function (OPTIONS) { 112 | var pluginHooks = this.hooks = new HookCollection(); 113 | 114 | // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link 115 | pluginHooks.addNoop("plainLinkText"); 116 | 117 | // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked 118 | pluginHooks.addNoop("preConversion"); 119 | 120 | // called with the text once all normalizations have been completed (tabs to spaces, line endings, etc.), but before any conversions have 121 | pluginHooks.addNoop("postNormalization"); 122 | 123 | // Called with the text before / after creating block elements like code blocks and lists. Note that this is called recursively 124 | // with inner content, e.g. it's called with the full text, and then only with the content of a blockquote. The inner 125 | // call will receive outdented text. 126 | pluginHooks.addNoop("preBlockGamut"); 127 | pluginHooks.addNoop("postBlockGamut"); 128 | 129 | // called with the text of a single block element before / after the span-level conversions (bold, code spans, etc.) have been made 130 | pluginHooks.addNoop("preSpanGamut"); 131 | pluginHooks.addNoop("postSpanGamut"); 132 | 133 | // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml 134 | pluginHooks.addNoop("postConversion"); 135 | 136 | // 137 | // Private state of the converter instance: 138 | // 139 | 140 | // Global hashes, used by various utility routines 141 | var g_urls; 142 | var g_titles; 143 | var g_html_blocks; 144 | 145 | // Used to track when we're inside an ordered or unordered list 146 | // (see _ProcessListItems() for details): 147 | var g_list_level; 148 | 149 | OPTIONS = OPTIONS || {}; 150 | var asciify = identity, deasciify = identity; 151 | if (OPTIONS.nonAsciiLetters) { 152 | 153 | /* In JavaScript regular expressions, \w only denotes [a-zA-Z0-9_]. 154 | * That's why there's inconsistent handling e.g. with intra-word bolding 155 | * of Japanese words. That's why we do the following if OPTIONS.nonAsciiLetters 156 | * is true: 157 | * 158 | * Before doing bold and italics, we find every instance 159 | * of a unicode word character in the Markdown source that is not 160 | * matched by \w, and the letter "Q". We take the character's code point 161 | * and encode it in base 51, using the "digits" 162 | * 163 | * A, B, ..., P, R, ..., Y, Z, a, b, ..., y, z 164 | * 165 | * delimiting it with "Q" on both sides. For example, the source 166 | * 167 | * > In Chinese, the smurfs are called 藍精靈, meaning "blue spirits". 168 | * 169 | * turns into 170 | * 171 | * > In Chinese, the smurfs are called QNIhQQMOIQQOuUQ, meaning "blue spirits". 172 | * 173 | * Since everything that is a letter in Unicode is now a letter (or 174 | * several letters) in ASCII, \w and \b should always do the right thing. 175 | * 176 | * After the bold/italic conversion, we decode again; since "Q" was encoded 177 | * alongside all non-ascii characters (as "QBfQ"), and the conversion 178 | * will not generate "Q", the only instances of that letter should be our 179 | * encoded characters. And since the conversion will not break words, the 180 | * "Q...Q" should all still be in one piece. 181 | * 182 | * We're using "Q" as the delimiter because it's probably one of the 183 | * rarest characters, and also because I can't think of any special behavior 184 | * that would ever be triggered by this letter (to use a silly example, if we 185 | * delimited with "H" on the left and "P" on the right, then "Ψ" would be 186 | * encoded as "HTTP", which may cause special behavior). The latter would not 187 | * actually be a huge issue for bold/italic, but may be if we later use it 188 | * in other places as well. 189 | * */ 190 | (function () { 191 | 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; 192 | var cp_Q = "Q".charCodeAt(0); 193 | var cp_A = "A".charCodeAt(0); 194 | var cp_Z = "Z".charCodeAt(0); 195 | var dist_Za = "a".charCodeAt(0) - cp_Z - 1; 196 | 197 | asciify = function(text) { 198 | return text.replace(lettersThatJavaScriptDoesNotKnowAndQ, function (m) { 199 | var c = m.charCodeAt(0); 200 | var s = ""; 201 | var v; 202 | while (c > 0) { 203 | v = (c % 51) + cp_A; 204 | if (v >= cp_Q) 205 | v++; 206 | if (v > cp_Z) 207 | v += dist_Za; 208 | s = String.fromCharCode(v) + s; 209 | c = c / 51 | 0; 210 | } 211 | return "Q" + s + "Q"; 212 | }) 213 | }; 214 | 215 | deasciify = function(text) { 216 | return text.replace(/Q([A-PR-Za-z]{1,3})Q/g, function (m, s) { 217 | var c = 0; 218 | var v; 219 | for (var i = 0; i < s.length; i++) { 220 | v = s.charCodeAt(i); 221 | if (v > cp_Z) 222 | v -= dist_Za; 223 | if (v > cp_Q) 224 | v--; 225 | v -= cp_A; 226 | c = (c * 51) + v; 227 | } 228 | return String.fromCharCode(c); 229 | }) 230 | } 231 | })(); 232 | } 233 | 234 | var _DoItalicsAndBold = OPTIONS.asteriskIntraWordEmphasis ? _DoItalicsAndBold_AllowIntrawordWithAsterisk : _DoItalicsAndBoldStrict; 235 | 236 | this.makeHtml = function (text) { 237 | 238 | // 239 | // Main function. The order in which other subs are called here is 240 | // essential. Link and image substitutions need to happen before 241 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 242 | // and tags get encoded. 243 | // 244 | 245 | // This will only happen if makeHtml on the same converter instance is called from a plugin hook. 246 | // Don't do that. 247 | if (g_urls) 248 | throw new Error("Recursive call to converter.makeHtml"); 249 | 250 | // Create the private state objects. 251 | g_urls = new SaveHash(); 252 | g_titles = new SaveHash(); 253 | g_html_blocks = []; 254 | g_list_level = 0; 255 | 256 | text = pluginHooks.preConversion(text); 257 | 258 | // attacklab: Replace ~ with ~T 259 | // This lets us use tilde as an escape char to avoid md5 hashes 260 | // The choice of character is arbitray; anything that isn't 261 | // magic in Markdown will work. 262 | text = text.replace(/~/g, "~T"); 263 | 264 | // attacklab: Replace $ with ~D 265 | // RegExp interprets $ as a special character 266 | // when it's in a replacement string 267 | text = text.replace(/\$/g, "~D"); 268 | 269 | // Standardize line endings 270 | text = text.replace(/\r\n/g, "\n"); // DOS to Unix 271 | text = text.replace(/\r/g, "\n"); // Mac to Unix 272 | 273 | // Make sure text begins and ends with a couple of newlines: 274 | text = "\n\n" + text + "\n\n"; 275 | 276 | // Convert all tabs to spaces. 277 | text = _Detab(text); 278 | 279 | // Strip any lines consisting only of spaces and tabs. 280 | // This makes subsequent regexen easier to write, because we can 281 | // match consecutive blank lines with /\n+/ instead of something 282 | // contorted like /[ \t]*\n+/ . 283 | text = text.replace(/^[ \t]+$/mg, ""); 284 | 285 | text = pluginHooks.postNormalization(text); 286 | 287 | // Turn block-level HTML blocks into hash entries 288 | text = _HashHTMLBlocks(text); 289 | 290 | // Strip link definitions, store in hashes. 291 | text = _StripLinkDefinitions(text); 292 | 293 | text = _RunBlockGamut(text); 294 | 295 | text = _UnescapeSpecialChars(text); 296 | 297 | // attacklab: Restore dollar signs 298 | text = text.replace(/~D/g, "$$"); 299 | 300 | // attacklab: Restore tildes 301 | text = text.replace(/~T/g, "~"); 302 | 303 | text = pluginHooks.postConversion(text); 304 | 305 | text = text.replace(/\\pagebreak/g, '
'); 306 | 307 | g_html_blocks = g_titles = g_urls = null; 308 | 309 | return text; 310 | }; 311 | 312 | function _StripLinkDefinitions(text) { 313 | // 314 | // Strips link definitions from text, stores the URLs and titles in 315 | // hash references. 316 | // 317 | 318 | // Link defs are in the form: ^[id]: url "optional title" 319 | 320 | /* 321 | text = text.replace(/ 322 | ^[ ]{0,3}\[([^\[\]]+)\]: // id = $1 attacklab: g_tab_width - 1 323 | [ \t]* 324 | \n? // maybe *one* newline 325 | [ \t]* 326 | ? // url = $2 327 | (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below 328 | [ \t]* 329 | \n? // maybe one newline 330 | [ \t]* 331 | ( // (potential) title = $3 332 | (\n*) // any lines skipped = $4 attacklab: lookbehind removed 333 | [ \t]+ 334 | ["(] 335 | (.+?) // title = $5 336 | [")] 337 | [ \t]* 338 | )? // title is optional 339 | (?:\n+|$) 340 | /gm, function(){...}); 341 | */ 342 | 343 | text = text.replace(/^[ ]{0,3}\[([^\[\]]+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, 344 | function (wholeMatch, m1, m2, m3, m4, m5) { 345 | m1 = m1.toLowerCase(); 346 | g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive 347 | if (m4) { 348 | // Oops, found blank lines, so it's not a title. 349 | // Put back the parenthetical statement we stole. 350 | return m3; 351 | } else if (m5) { 352 | g_titles.set(m1, m5.replace(/"/g, """)); 353 | } 354 | 355 | // Completely remove the definition from the text 356 | return ""; 357 | } 358 | ); 359 | 360 | return text; 361 | } 362 | 363 | function _HashHTMLBlocks(text) { 364 | 365 | // Hashify HTML blocks: 366 | // We only want to do this for block-level HTML tags, such as headers, 367 | // lists, and tables. That's because we still want to wrap

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

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

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

" + _RunSpanGamut(m1) + "

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

    Just type foo `bar` baz at the prompt.

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

    tags 1381 | // 1382 | 1383 | // Strip leading and trailing lines: 1384 | text = text.replace(/^\n+/g, ""); 1385 | text = text.replace(/\n+$/g, ""); 1386 | 1387 | var grafs = text.split(/\n{2,}/g); 1388 | var grafsOut = []; 1389 | 1390 | var markerRe = /~K(\d+)K/; 1391 | 1392 | // 1393 | // Wrap

    tags. 1394 | // 1395 | var end = grafs.length; 1396 | for (var i = 0; i < end; i++) { 1397 | var str = grafs[i]; 1398 | 1399 | // if this is an HTML marker, copy it 1400 | if (markerRe.test(str)) { 1401 | grafsOut.push(str); 1402 | } 1403 | else if (/\S/.test(str)) { 1404 | str = _RunSpanGamut(str); 1405 | str = str.replace(/^([ \t]*)/g, "

    "); 1406 | str += "

    " 1407 | grafsOut.push(str); 1408 | } 1409 | 1410 | } 1411 | // 1412 | // Unhashify HTML blocks 1413 | // 1414 | if (!doNotUnhash) { 1415 | end = grafsOut.length; 1416 | for (var i = 0; i < end; i++) { 1417 | var foundAny = true; 1418 | while (foundAny) { // we may need several runs, since the data may be nested 1419 | foundAny = false; 1420 | grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { 1421 | foundAny = true; 1422 | return g_html_blocks[id]; 1423 | }); 1424 | } 1425 | } 1426 | } 1427 | return grafsOut.join("\n\n"); 1428 | } 1429 | 1430 | function _EncodeAmpsAndAngles(text) { 1431 | // Smart processing for ampersands and angle brackets that need to be encoded. 1432 | 1433 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1434 | // http://bumppo.net/projects/amputator/ 1435 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); 1436 | 1437 | // Encode naked <'s 1438 | text = text.replace(/<(?![a-z\/?!]|~D)/gi, "<"); 1439 | 1440 | return text; 1441 | } 1442 | 1443 | function _EncodeBackslashEscapes(text) { 1444 | // 1445 | // Parameter: String. 1446 | // Returns: The string, with after processing the following backslash 1447 | // escape sequences. 1448 | // 1449 | 1450 | // attacklab: The polite way to do this is with the new 1451 | // escapeCharacters() function: 1452 | // 1453 | // text = escapeCharacters(text,"\\",true); 1454 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1455 | // 1456 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1457 | // as an optimization for Firefox. This function gets called a LOT. 1458 | 1459 | text = text.replace(/\\(\\)/g, escapeCharacters_callback); 1460 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); 1461 | return text; 1462 | } 1463 | 1464 | var charInsideUrl = "[-A-Z0-9+&@#/%?=~_|[\\]()!:,.;]", 1465 | charEndingUrl = "[-A-Z0-9+&@#/%=~_|[\\])]", 1466 | autoLinkRegex = new RegExp("(=\"|<)?\\b(https?|ftp)(://" + charInsideUrl + "*" + charEndingUrl + ")(?=$|\\W)", "gi"), 1467 | endCharRegex = new RegExp(charEndingUrl, "i"); 1468 | 1469 | function handleTrailingParens(wholeMatch, lookbehind, protocol, link) { 1470 | if (lookbehind) 1471 | return wholeMatch; 1472 | if (link.charAt(link.length - 1) !== ")") 1473 | return "<" + protocol + link + ">"; 1474 | var parens = link.match(/[()]/g); 1475 | var level = 0; 1476 | for (var i = 0; i < parens.length; i++) { 1477 | if (parens[i] === "(") { 1478 | if (level <= 0) 1479 | level = 1; 1480 | else 1481 | level++; 1482 | } 1483 | else { 1484 | level--; 1485 | } 1486 | } 1487 | var tail = ""; 1488 | if (level < 0) { 1489 | var re = new RegExp("\\){1," + (-level) + "}$"); 1490 | link = link.replace(re, function (trailingParens) { 1491 | tail = trailingParens; 1492 | return ""; 1493 | }); 1494 | } 1495 | if (tail) { 1496 | var lastChar = link.charAt(link.length - 1); 1497 | if (!endCharRegex.test(lastChar)) { 1498 | tail = lastChar + tail; 1499 | link = link.substr(0, link.length - 1); 1500 | } 1501 | } 1502 | return "<" + protocol + link + ">" + tail; 1503 | } 1504 | 1505 | function _DoAutoLinks(text) { 1506 | 1507 | // note that at this point, all other URL in the text are already hyperlinked as
    1508 | // *except* for the case 1509 | 1510 | // automatically add < and > around unadorned raw hyperlinks 1511 | // must be preceded by a non-word character (and not by =" or <) and followed by non-word/EOF character 1512 | // simulating the lookbehind in a consuming way is okay here, since a URL can neither and with a " nor 1513 | // with a <, so there is no risk of overlapping matches. 1514 | text = text.replace(autoLinkRegex, handleTrailingParens); 1515 | 1516 | // autolink anything like 1517 | 1518 | 1519 | var replacer = function (wholematch, m1) { 1520 | var url = attributeSafeUrl(m1); 1521 | 1522 | return "" + pluginHooks.plainLinkText(m1) + ""; 1523 | }; 1524 | text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); 1525 | 1526 | // Email addresses: 1527 | /* 1528 | text = text.replace(/ 1529 | < 1530 | (?:mailto:)? 1531 | ( 1532 | [-.\w]+ 1533 | \@ 1534 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1535 | ) 1536 | > 1537 | /gi, _DoAutoLinks_callback()); 1538 | */ 1539 | 1540 | /* disabling email autolinking, since we don't do that on the server, either 1541 | text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, 1542 | function(wholeMatch,m1) { 1543 | return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); 1544 | } 1545 | ); 1546 | */ 1547 | return text; 1548 | } 1549 | 1550 | function _UnescapeSpecialChars(text) { 1551 | // 1552 | // Swap back in all the special characters we've hidden. 1553 | // 1554 | text = text.replace(/~E(\d+)E/g, 1555 | function (wholeMatch, m1) { 1556 | var charCodeToReplace = parseInt(m1); 1557 | return String.fromCharCode(charCodeToReplace); 1558 | } 1559 | ); 1560 | return text; 1561 | } 1562 | 1563 | function _Outdent(text) { 1564 | // 1565 | // Remove one level of line-leading tabs or spaces 1566 | // 1567 | 1568 | // attacklab: hack around Konqueror 3.5.4 bug: 1569 | // "----------bug".replace(/^-/g,"") == "bug" 1570 | 1571 | text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width 1572 | 1573 | // attacklab: clean up hack 1574 | text = text.replace(/~0/g, "") 1575 | 1576 | return text; 1577 | } 1578 | 1579 | function _Detab(text) { 1580 | if (!/\t/.test(text)) 1581 | return text; 1582 | 1583 | var spaces = [" ", " ", " ", " "], 1584 | skew = 0, 1585 | v; 1586 | 1587 | return text.replace(/[\n\t]/g, function (match, offset) { 1588 | if (match === "\n") { 1589 | skew = offset + 1; 1590 | return match; 1591 | } 1592 | v = (offset - skew) % 4; 1593 | skew = offset + 1; 1594 | return spaces[v]; 1595 | }); 1596 | } 1597 | 1598 | // 1599 | // attacklab: Utility functions 1600 | // 1601 | 1602 | function attributeSafeUrl(url) { 1603 | url = attributeEncode(url); 1604 | url = escapeCharacters(url, "*_:()[]") 1605 | return url; 1606 | } 1607 | 1608 | function escapeCharacters(text, charsToEscape, afterBackslash) { 1609 | // First we have to escape the escape characters so that 1610 | // we can build a character class out of them 1611 | var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; 1612 | 1613 | if (afterBackslash) { 1614 | regexString = "\\\\" + regexString; 1615 | } 1616 | 1617 | var regex = new RegExp(regexString, "g"); 1618 | text = text.replace(regex, escapeCharacters_callback); 1619 | 1620 | return text; 1621 | } 1622 | 1623 | 1624 | function escapeCharacters_callback(wholeMatch, m1) { 1625 | var charCodeToEscape = m1.charCodeAt(0); 1626 | return "~E" + charCodeToEscape + "E"; 1627 | } 1628 | 1629 | }; // end of the Markdown.Converter constructor 1630 | 1631 | })(); -------------------------------------------------------------------------------- /Markdown.Extra.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // A quick way to make sure we're only keeping span-level tags when we need to. 3 | // This isn't supposed to be foolproof. It's just a quick way to make sure we 4 | // keep all span-level tags returned by a pagedown converter. It should allow 5 | // all span-level tags through, with or without attributes. 6 | var inlineTags = new RegExp(['^(<\\/?(a|abbr|acronym|applet|area|b|basefont|', 7 | 'bdo|big|button|cite|code|del|dfn|em|figcaption|', 8 | 'font|i|iframe|img|input|ins|kbd|label|map|', 9 | 'mark|meter|object|param|progress|q|ruby|rp|rt|s|', 10 | 'samp|script|select|small|span|strike|strong|', 11 | 'sub|sup|textarea|time|tt|u|var|wbr)[^>]*>|', 12 | '<(br)\\s?\\/?>)$'].join(''), 'i'); 13 | 14 | /****************************************************************** 15 | * Utility Functions * 16 | *****************************************************************/ 17 | 18 | // patch for ie7 19 | if (!Array.indexOf) { 20 | Array.prototype.indexOf = function(obj) { 21 | for (var i = 0; i < this.length; i++) { 22 | if (this[i] == obj) { 23 | return i; 24 | } 25 | } 26 | return -1; 27 | }; 28 | } 29 | 30 | function trim(str) { 31 | return str.replace(/^\s+|\s+$/g, ''); 32 | } 33 | 34 | function rtrim(str) { 35 | return str.replace(/\s+$/g, ''); 36 | } 37 | 38 | // Remove one level of indentation from text. Indent is 4 spaces. 39 | function outdent(text) { 40 | return text.replace(new RegExp('^(\\t|[ ]{1,4})', 'gm'), ''); 41 | } 42 | 43 | function contains(str, substr) { 44 | return str.indexOf(substr) != -1; 45 | } 46 | 47 | // Sanitize html, removing tags that aren't in the whitelist 48 | function sanitizeHtml(html, whitelist) { 49 | return html.replace(/<[^>]*>?/gi, function(tag) { 50 | return tag.match(whitelist) ? tag : ''; 51 | }); 52 | } 53 | 54 | // Merge two arrays, keeping only unique elements. 55 | function union(x, y) { 56 | var obj = {}; 57 | for (var i = 0; i < x.length; i++) 58 | obj[x[i]] = x[i]; 59 | for (i = 0; i < y.length; i++) 60 | obj[y[i]] = y[i]; 61 | var res = []; 62 | for (var k in obj) { 63 | if (obj.hasOwnProperty(k)) 64 | res.push(obj[k]); 65 | } 66 | return res; 67 | } 68 | 69 | // JS regexes don't support \A or \Z, so we add sentinels, as Pagedown 70 | // does. In this case, we add the ascii codes for start of text (STX) and 71 | // end of text (ETX), an idea borrowed from: 72 | // https://github.com/tanakahisateru/js-markdown-extra 73 | function addAnchors(text) { 74 | if(text.charAt(0) != '\x02') 75 | text = '\x02' + text; 76 | if(text.charAt(text.length - 1) != '\x03') 77 | text = text + '\x03'; 78 | return text; 79 | } 80 | 81 | // Remove STX and ETX sentinels. 82 | function removeAnchors(text) { 83 | if(text.charAt(0) == '\x02') 84 | text = text.substr(1); 85 | if(text.charAt(text.length - 1) == '\x03') 86 | text = text.substr(0, text.length - 1); 87 | return text; 88 | } 89 | 90 | // Convert markdown within an element, retaining only span-level tags 91 | function convertSpans(text, extra) { 92 | return sanitizeHtml(convertAll(text, extra), inlineTags); 93 | } 94 | 95 | // Convert internal markdown using the stock pagedown converter 96 | function convertAll(text, extra) { 97 | var result = extra.blockGamutHookCallback(text); 98 | // We need to perform these operations since we skip the steps in the converter 99 | result = unescapeSpecialChars(result); 100 | result = result.replace(/~D/g, "$$").replace(/~T/g, "~"); 101 | result = extra.previousPostConversion(result); 102 | return result; 103 | } 104 | 105 | // Convert escaped special characters 106 | function processEscapesStep1(text) { 107 | // Markdown extra adds two escapable characters, `:` and `|` 108 | return text.replace(/\\\|/g, '~I').replace(/\\:/g, '~i'); 109 | } 110 | function processEscapesStep2(text) { 111 | return text.replace(/~I/g, '|').replace(/~i/g, ':'); 112 | } 113 | 114 | // Duplicated from PageDown converter 115 | function unescapeSpecialChars(text) { 116 | // Swap back in all the special characters we've hidden. 117 | text = text.replace(/~E(\d+)E/g, function(wholeMatch, m1) { 118 | var charCodeToReplace = parseInt(m1); 119 | return String.fromCharCode(charCodeToReplace); 120 | }); 121 | return text; 122 | } 123 | 124 | function slugify(text) { 125 | return text.toLowerCase() 126 | .replace(/\s+/g, '-') // Replace spaces with - 127 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 128 | .replace(/\-\-+/g, '-') // Replace multiple - with single - 129 | .replace(/^-+/, '') // Trim - from start of text 130 | .replace(/-+$/, ''); // Trim - from end of text 131 | } 132 | 133 | /***************************************************************************** 134 | * Markdown.Extra * 135 | ****************************************************************************/ 136 | 137 | Markdown.Extra = function() { 138 | // For converting internal markdown (in tables for instance). 139 | // This is necessary since these methods are meant to be called as 140 | // preConversion hooks, and the Markdown converter passed to init() 141 | // won't convert any markdown contained in the html tags we return. 142 | this.converter = null; 143 | 144 | // Stores html blocks we generate in hooks so that 145 | // they're not destroyed if the user is using a sanitizing converter 146 | this.hashBlocks = []; 147 | 148 | // Stores footnotes 149 | this.footnotes = {}; 150 | this.usedFootnotes = []; 151 | 152 | // Special attribute blocks for fenced code blocks and headers enabled. 153 | this.attributeBlocks = false; 154 | 155 | // Fenced code block options 156 | this.googleCodePrettify = false; 157 | this.highlightJs = false; 158 | 159 | // Table options 160 | this.tableClass = ''; 161 | 162 | this.tabWidth = 4; 163 | }; 164 | 165 | Markdown.Extra.init = function(converter, options) { 166 | // Each call to init creates a new instance of Markdown.Extra so it's 167 | // safe to have multiple converters, with different options, on a single page 168 | var extra = new Markdown.Extra(); 169 | var postNormalizationTransformations = []; 170 | var preBlockGamutTransformations = []; 171 | var postSpanGamutTransformations = []; 172 | var postConversionTransformations = ["unHashExtraBlocks"]; 173 | 174 | options = options || {}; 175 | options.extensions = options.extensions || ["all"]; 176 | if (contains(options.extensions, "all")) { 177 | options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "smartypants", "strikethrough", "newlines"]; 178 | } 179 | preBlockGamutTransformations.push("wrapHeaders"); 180 | if (contains(options.extensions, "attr_list")) { 181 | postNormalizationTransformations.push("hashFcbAttributeBlocks"); 182 | preBlockGamutTransformations.push("hashHeaderAttributeBlocks"); 183 | postConversionTransformations.push("applyAttributeBlocks"); 184 | extra.attributeBlocks = true; 185 | } 186 | if (contains(options.extensions, "fenced_code_gfm")) { 187 | // This step will convert fcb inside list items and blockquotes 188 | preBlockGamutTransformations.push("fencedCodeBlocks"); 189 | // This extra step is to prevent html blocks hashing and link definition/footnotes stripping inside fcb 190 | postNormalizationTransformations.push("fencedCodeBlocks"); 191 | } 192 | if (contains(options.extensions, "tables")) { 193 | preBlockGamutTransformations.push("tables"); 194 | } 195 | if (contains(options.extensions, "def_list")) { 196 | preBlockGamutTransformations.push("definitionLists"); 197 | } 198 | if (contains(options.extensions, "footnotes")) { 199 | postNormalizationTransformations.push("stripFootnoteDefinitions"); 200 | preBlockGamutTransformations.push("doFootnotes"); 201 | postConversionTransformations.push("printFootnotes"); 202 | } 203 | if (contains(options.extensions, "smartypants")) { 204 | postConversionTransformations.push("runSmartyPants"); 205 | } 206 | if (contains(options.extensions, "strikethrough")) { 207 | postSpanGamutTransformations.push("strikethrough"); 208 | } 209 | if (contains(options.extensions, "newlines")) { 210 | postSpanGamutTransformations.push("newlines"); 211 | } 212 | 213 | converter.hooks.chain("postNormalization", function(text) { 214 | return extra.doTransform(postNormalizationTransformations, text) + '\n'; 215 | }); 216 | 217 | converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) { 218 | // Keep a reference to the block gamut callback to run recursively 219 | extra.blockGamutHookCallback = blockGamutHookCallback; 220 | text = processEscapesStep1(text); 221 | text = extra.doTransform(preBlockGamutTransformations, text) + '\n'; 222 | text = processEscapesStep2(text); 223 | return text; 224 | }); 225 | 226 | converter.hooks.chain("postSpanGamut", function(text) { 227 | return extra.doTransform(postSpanGamutTransformations, text); 228 | }); 229 | 230 | // Keep a reference to the hook chain running before doPostConversion to apply on hashed extra blocks 231 | extra.previousPostConversion = converter.hooks.postConversion; 232 | converter.hooks.chain("postConversion", function(text) { 233 | text = extra.doTransform(postConversionTransformations, text); 234 | // Clear state vars that may use unnecessary memory 235 | extra.hashBlocks = []; 236 | extra.footnotes = {}; 237 | extra.usedFootnotes = []; 238 | return text; 239 | }); 240 | 241 | if ("highlighter" in options) { 242 | extra.googleCodePrettify = options.highlighter === 'prettify'; 243 | extra.highlightJs = options.highlighter === 'highlight'; 244 | } 245 | 246 | if ("table_class" in options) { 247 | extra.tableClass = options.table_class; 248 | } 249 | 250 | extra.converter = converter; 251 | 252 | // Caller usually won't need this, but it's handy for testing. 253 | return extra; 254 | }; 255 | 256 | // Do transformations 257 | Markdown.Extra.prototype.doTransform = function(transformations, text) { 258 | for(var i = 0; i < transformations.length; i++) 259 | text = this[transformations[i]](text); 260 | return text; 261 | }; 262 | 263 | // Return a placeholder containing a key, which is the block's index in the 264 | // hashBlocks array. We wrap our output in a

    tag here so Pagedown won't. 265 | Markdown.Extra.prototype.hashExtraBlock = function(block) { 266 | return '\n

    ~X' + (this.hashBlocks.push(block) - 1) + 'X

    \n'; 267 | }; 268 | Markdown.Extra.prototype.hashExtraInline = function(block) { 269 | return '~X' + (this.hashBlocks.push(block) - 1) + 'X'; 270 | }; 271 | 272 | // Replace placeholder blocks in `text` with their corresponding 273 | // html blocks in the hashBlocks array. 274 | Markdown.Extra.prototype.unHashExtraBlocks = function(text) { 275 | var self = this; 276 | function recursiveUnHash() { 277 | var hasHash = false; 278 | text = text.replace(/(?:

    )?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) { 279 | hasHash = true; 280 | var key = parseInt(m1, 10); 281 | return self.hashBlocks[key]; 282 | }); 283 | if(hasHash === true) { 284 | recursiveUnHash(); 285 | } 286 | } 287 | recursiveUnHash(); 288 | return text; 289 | }; 290 | 291 | // Wrap headers to make sure they won't be in def lists 292 | Markdown.Extra.prototype.wrapHeaders = function(text) { 293 | function wrap(text) { 294 | return '\n' + text + '\n'; 295 | } 296 | text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap); 297 | text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap); 298 | text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap); 299 | return text; 300 | }; 301 | 302 | 303 | /****************************************************************** 304 | * Attribute Blocks * 305 | *****************************************************************/ 306 | 307 | // TODO: use sentinels. Should we just add/remove them in doConversion? 308 | // TODO: better matches for id / class attributes 309 | var attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}"; 310 | var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm"); 311 | var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" + 312 | "(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead 313 | var fcbAttributes = new RegExp("^(```[^`\\n]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" + 314 | "(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm"); 315 | 316 | // Extract headers attribute blocks, move them above the element they will be 317 | // applied to, and hash them for later. 318 | Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) { 319 | 320 | var self = this; 321 | function attributeCallback(wholeMatch, pre, attr) { 322 | return '

    ~XX' + (self.hashBlocks.push(attr) - 1) + 'XX

    \n' + pre + "\n"; 323 | } 324 | 325 | text = text.replace(hdrAttributesA, attributeCallback); // ## headers 326 | text = text.replace(hdrAttributesB, attributeCallback); // underline headers 327 | return text; 328 | }; 329 | 330 | // Extract FCB attribute blocks, move them above the element they will be 331 | // applied to, and hash them for later. 332 | Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) { 333 | // TODO: use sentinels. Should we just add/remove them in doConversion? 334 | // TODO: better matches for id / class attributes 335 | 336 | var self = this; 337 | function attributeCallback(wholeMatch, pre, attr) { 338 | return '

    ~XX' + (self.hashBlocks.push(attr) - 1) + 'XX

    \n' + pre + "\n"; 339 | } 340 | 341 | return text.replace(fcbAttributes, attributeCallback); 342 | }; 343 | 344 | Markdown.Extra.prototype.applyAttributeBlocks = function(text) { 345 | var self = this; 346 | var blockRe = new RegExp('

    ~XX(\\d+)XX

    [\\s]*' + 347 | '(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?))', "gm"); 348 | text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) { 349 | if (!tag) // no following header or fenced code block. 350 | return ''; 351 | 352 | // get attributes list from hash 353 | var key = parseInt(k, 10); 354 | var attributes = self.hashBlocks[key]; 355 | 356 | // get id 357 | var id = attributes.match(/#[^\s#.]+/g) || []; 358 | var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : ''; 359 | 360 | // get classes and merge with existing classes 361 | var classes = attributes.match(/\.[^\s#.]+/g) || []; 362 | for (var i = 0; i < classes.length; i++) // Remove leading dot 363 | classes[i] = classes[i].substr(1, classes[i].length - 1); 364 | 365 | var classStr = ''; 366 | if (cls) 367 | classes = union(classes, [cls]); 368 | 369 | if (classes.length > 0) 370 | classStr = ' class="' + classes.join(' ') + '"'; 371 | 372 | return "<" + tag + idStr + classStr + rest; 373 | }); 374 | 375 | return text; 376 | }; 377 | 378 | /****************************************************************** 379 | * Tables * 380 | *****************************************************************/ 381 | 382 | // Find and convert Markdown Extra tables into html. 383 | Markdown.Extra.prototype.tables = function(text) { 384 | var self = this; 385 | 386 | var leadingPipe = new RegExp( 387 | ['^' , 388 | '[ ]{0,3}' , // Allowed whitespace 389 | '[|]' , // Initial pipe 390 | '(.+)\\n' , // $1: Header Row 391 | 392 | '[ ]{0,3}' , // Allowed whitespace 393 | '[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator 394 | 395 | '(' , // $3: Table Body 396 | '(?:[ ]*[|].*\\n?)*' , // Table rows 397 | ')', 398 | '(?:\\n|$)' // Stop at final newline 399 | ].join(''), 400 | 'gm' 401 | ); 402 | 403 | var noLeadingPipe = new RegExp( 404 | ['^' , 405 | '[ ]{0,3}' , // Allowed whitespace 406 | '(\\S.*[|].*)\\n' , // $1: Header Row 407 | 408 | '[ ]{0,3}' , // Allowed whitespace 409 | '([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator 410 | 411 | '(' , // $3: Table Body 412 | '(?:.*[|].*\\n?)*' , // Table rows 413 | ')' , 414 | '(?:\\n|$)' // Stop at final newline 415 | ].join(''), 416 | 'gm' 417 | ); 418 | 419 | text = text.replace(leadingPipe, doTable); 420 | text = text.replace(noLeadingPipe, doTable); 421 | 422 | // $1 = header, $2 = separator, $3 = body 423 | function doTable(match, header, separator, body, offset, string) { 424 | // remove any leading pipes and whitespace 425 | header = header.replace(/^ *[|]/m, ''); 426 | separator = separator.replace(/^ *[|]/m, ''); 427 | body = body.replace(/^ *[|]/gm, ''); 428 | 429 | // remove trailing pipes and whitespace 430 | header = header.replace(/[|] *$/m, ''); 431 | separator = separator.replace(/[|] *$/m, ''); 432 | body = body.replace(/[|] *$/gm, ''); 433 | 434 | // determine column alignments 435 | var alignspecs = separator.split(/ *[|] */); 436 | var align = []; 437 | for (var i = 0; i < alignspecs.length; i++) { 438 | var spec = alignspecs[i]; 439 | if (spec.match(/^ *-+: *$/m)) 440 | align[i] = ' align="right"'; 441 | else if (spec.match(/^ *:-+: *$/m)) 442 | align[i] = ' align="center"'; 443 | else if (spec.match(/^ *:-+ *$/m)) 444 | align[i] = ' align="left"'; 445 | else align[i] = ''; 446 | } 447 | 448 | // TODO: parse spans in header and rows before splitting, so that pipes 449 | // inside of tags are not interpreted as separators 450 | var headers = header.split(/ *[|] */); 451 | var colCount = headers.length; 452 | 453 | // build html 454 | var cls = self.tableClass ? ' class="' + self.tableClass + '"' : ''; 455 | var html = ['\n', '\n', '\n'].join(''); 456 | 457 | // build column headers. 458 | for (i = 0; i < colCount; i++) { 459 | var headerHtml = convertSpans(trim(headers[i]), self); 460 | html += [" ", headerHtml, "\n"].join(''); 461 | } 462 | html += "\n\n"; 463 | 464 | // build rows 465 | var rows = body.split('\n'); 466 | for (i = 0; i < rows.length; i++) { 467 | if (rows[i].match(/^\s*$/)) // can apply to final row 468 | continue; 469 | 470 | // ensure number of rowCells matches colCount 471 | var rowCells = rows[i].split(/ *[|] */); 472 | var lenDiff = colCount - rowCells.length; 473 | for (var j = 0; j < lenDiff; j++) 474 | rowCells.push(''); 475 | 476 | html += "\n"; 477 | for (j = 0; j < colCount; j++) { 478 | var colHtml = convertSpans(trim(rowCells[j]), self); 479 | html += [" ", colHtml, "\n"].join(''); 480 | } 481 | html += "\n"; 482 | } 483 | 484 | html += "\n"; 485 | 486 | // replace html with placeholder until postConversion step 487 | return self.hashExtraBlock(html); 488 | } 489 | 490 | return text; 491 | }; 492 | 493 | 494 | /****************************************************************** 495 | * Footnotes * 496 | *****************************************************************/ 497 | 498 | // Strip footnote, store in hashes. 499 | Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) { 500 | var self = this; 501 | 502 | text = text.replace( 503 | /\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g, 504 | function(wholeMatch, m1, m2) { 505 | m1 = slugify(m1); 506 | m2 += "\n"; 507 | m2 = m2.replace(/^[ ]{0,3}/g, ""); 508 | self.footnotes[m1] = m2; 509 | return "\n"; 510 | }); 511 | 512 | return text; 513 | }; 514 | 515 | 516 | // Find and convert footnotes references. 517 | Markdown.Extra.prototype.doFootnotes = function(text) { 518 | var self = this; 519 | if(self.isConvertingFootnote === true) { 520 | return text; 521 | } 522 | 523 | var footnoteCounter = 0; 524 | text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) { 525 | var id = slugify(m1); 526 | var footnote = self.footnotes[id]; 527 | if (footnote === undefined) { 528 | return wholeMatch; 529 | } 530 | footnoteCounter++; 531 | self.usedFootnotes.push(id); 532 | var html = '' + footnoteCounter 534 | + ''; 535 | return self.hashExtraInline(html); 536 | }); 537 | 538 | return text; 539 | }; 540 | 541 | // Print footnotes at the end of the document 542 | Markdown.Extra.prototype.printFootnotes = function(text) { 543 | var self = this; 544 | 545 | if (self.usedFootnotes.length === 0) { 546 | return text; 547 | } 548 | 549 | text += '\n\n
    \n
    \n
      \n\n'; 550 | for(var i=0; i' 559 | + formattedfootnote 560 | + ' \n\n'; 563 | } 564 | text += '
    \n
    '; 565 | return text; 566 | }; 567 | 568 | 569 | /****************************************************************** 570 | * Fenced Code Blocks (gfm) * 571 | ******************************************************************/ 572 | 573 | // Find and convert gfm-inspired fenced code blocks into html. 574 | Markdown.Extra.prototype.fencedCodeBlocks = function(text) { 575 | function encodeCode(code) { 576 | code = code.replace(/&/g, "&"); 577 | code = code.replace(//g, ">"); 579 | // These were escaped by PageDown before postNormalization 580 | code = code.replace(/~D/g, "$$"); 581 | code = code.replace(/~T/g, "~"); 582 | return code; 583 | } 584 | 585 | var self = this; 586 | text = text.replace(/(?:^|\n)```([^`\n]*)\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) { 587 | var language = trim(m1), codeblock = m2; 588 | 589 | // adhere to specified options 590 | var preclass = self.googleCodePrettify ? ' class="prettyprint"' : ''; 591 | var codeclass = ''; 592 | if (language) { 593 | if (self.googleCodePrettify || self.highlightJs) { 594 | // use html5 language- class names. supported by both prettify and highlight.js 595 | codeclass = ' class="language-' + language + '"'; 596 | } else { 597 | codeclass = ' class="' + language + '"'; 598 | } 599 | } 600 | 601 | var html = ['', 602 | encodeCode(codeblock), '
    '].join(''); 603 | 604 | // replace codeblock with placeholder until postConversion step 605 | return self.hashExtraBlock(html); 606 | }); 607 | 608 | return text; 609 | }; 610 | 611 | 612 | /****************************************************************** 613 | * SmartyPants * 614 | ******************************************************************/ 615 | 616 | Markdown.Extra.prototype.educatePants = function(text) { 617 | var self = this; 618 | var result = ''; 619 | var blockOffset = 0; 620 | // Here we parse HTML in a very bad manner 621 | text.replace(/(?:)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) { 622 | var token = text.substring(blockOffset, offset); 623 | result += self.applyPants(token); 624 | self.smartyPantsLastChar = result.substring(result.length - 1); 625 | blockOffset = offset + wholeMatch.length; 626 | if(!m1) { 627 | // Skip commentary 628 | result += wholeMatch; 629 | return; 630 | } 631 | // Skip special tags 632 | if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) { 633 | m4 = self.educatePants(m4); 634 | } 635 | else { 636 | self.smartyPantsLastChar = m4.substring(m4.length - 1); 637 | } 638 | result += m1 + m2 + m3 + m4 + m5; 639 | }); 640 | var lastToken = text.substring(blockOffset); 641 | result += self.applyPants(lastToken); 642 | self.smartyPantsLastChar = result.substring(result.length - 1); 643 | return result; 644 | }; 645 | 646 | function revertPants(wholeMatch, m1) { 647 | var blockText = m1; 648 | blockText = blockText.replace(/&\#8220;/g, "\""); 649 | blockText = blockText.replace(/&\#8221;/g, "\""); 650 | blockText = blockText.replace(/&\#8216;/g, "'"); 651 | blockText = blockText.replace(/&\#8217;/g, "'"); 652 | blockText = blockText.replace(/&\#8212;/g, "---"); 653 | blockText = blockText.replace(/&\#8211;/g, "--"); 654 | blockText = blockText.replace(/&\#8230;/g, "..."); 655 | return blockText; 656 | } 657 | 658 | Markdown.Extra.prototype.applyPants = function(text) { 659 | // Dashes 660 | text = text.replace(/---/g, "—").replace(/--/g, "–"); 661 | // Ellipses 662 | text = text.replace(/\.\.\./g, "…").replace(/\.\s\.\s\./g, "…"); 663 | // Backticks 664 | text = text.replace(/``/g, "“").replace (/''/g, "”"); 665 | 666 | if(/^'$/.test(text)) { 667 | // Special case: single-character ' token 668 | if(/\S/.test(this.smartyPantsLastChar)) { 669 | return "’"; 670 | } 671 | return "‘"; 672 | } 673 | if(/^"$/.test(text)) { 674 | // Special case: single-character " token 675 | if(/\S/.test(this.smartyPantsLastChar)) { 676 | return "”"; 677 | } 678 | return "“"; 679 | } 680 | 681 | // Special case if the very first character is a quote 682 | // followed by punctuation at a non-word-break. Close the quotes by brute force: 683 | text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "’"); 684 | text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "”"); 685 | 686 | // Special case for double sets of quotes, e.g.: 687 | //

    He said, "'Quoted' words in a larger quote."

    688 | text = text.replace(/"'(?=\w)/g, "“‘"); 689 | text = text.replace(/'"(?=\w)/g, "‘“"); 690 | 691 | // Special case for decade abbreviations (the '80s): 692 | text = text.replace(/'(?=\d{2}s)/g, "’"); 693 | 694 | // Get most opening single quotes: 695 | text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1‘"); 696 | 697 | // Single closing quotes: 698 | text = text.replace(/([^\s\[\{\(\-])'/g, "$1’"); 699 | text = text.replace(/'(?=\s|s\b)/g, "’"); 700 | 701 | // Any remaining single quotes should be opening ones: 702 | text = text.replace(/'/g, "‘"); 703 | 704 | // Get most opening double quotes: 705 | text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1“"); 706 | 707 | // Double closing quotes: 708 | text = text.replace(/([^\s\[\{\(\-])"/g, "$1”"); 709 | text = text.replace(/"(?=\s)/g, "”"); 710 | 711 | // Any remaining quotes should be opening ones. 712 | text = text.replace(/"/ig, "“"); 713 | return text; 714 | }; 715 | 716 | // Find and convert markdown extra definition lists into html. 717 | Markdown.Extra.prototype.runSmartyPants = function(text) { 718 | this.smartyPantsLastChar = ''; 719 | text = this.educatePants(text); 720 | // Clean everything inside html tags (some of them may have been converted due to our rough html parsing) 721 | text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants); 722 | return text; 723 | }; 724 | 725 | /****************************************************************** 726 | * Definition Lists * 727 | ******************************************************************/ 728 | 729 | // Find and convert markdown extra definition lists into html. 730 | Markdown.Extra.prototype.definitionLists = function(text) { 731 | var wholeList = new RegExp( 732 | ['(\\x02\\n?|\\n\\n)' , 733 | '(?:' , 734 | '(' , // $1 = whole list 735 | '(' , // $2 736 | '[ ]{0,3}' , 737 | '((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term 738 | '\\n?' , 739 | '[ ]{0,3}:[ ]+' , // colon starting definition 740 | ')' , 741 | '([\\s\\S]+?)' , 742 | '(' , // $4 743 | '(?=\\0x03)' , // \z 744 | '|' , 745 | '(?=' , 746 | '\\n{2,}' , 747 | '(?=\\S)' , 748 | '(?!' , // Negative lookahead for another term 749 | '[ ]{0,3}' , 750 | '(?:\\S.*\\n)+?' , // defined term 751 | '\\n?' , 752 | '[ ]{0,3}:[ ]+' , // colon starting definition 753 | ')' , 754 | '(?!' , // Negative lookahead for another definition 755 | '[ ]{0,3}:[ ]+' , // colon starting definition 756 | ')' , 757 | ')' , 758 | ')' , 759 | ')' , 760 | ')' 761 | ].join(''), 762 | 'gm' 763 | ); 764 | 765 | var self = this; 766 | text = addAnchors(text); 767 | 768 | text = text.replace(wholeList, function(match, pre, list) { 769 | var result = trim(self.processDefListItems(list)); 770 | result = "
    \n" + result + "\n
    "; 771 | return pre + self.hashExtraBlock(result) + "\n\n"; 772 | }); 773 | 774 | return removeAnchors(text); 775 | }; 776 | 777 | // Process the contents of a single definition list, splitting it 778 | // into individual term and definition list items. 779 | Markdown.Extra.prototype.processDefListItems = function(listStr) { 780 | var self = this; 781 | 782 | var dt = new RegExp( 783 | ['(\\x02\\n?|\\n\\n+)' , // leading line 784 | '(' , // definition terms = $1 785 | '[ ]{0,3}' , // leading whitespace 786 | '(?![:][ ]|[ ])' , // negative lookahead for a definition 787 | // mark (colon) or more whitespace 788 | '(?:\\S.*\\n)+?' , // actual term (not whitespace) 789 | ')' , 790 | '(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed 791 | ].join(''), // with a definition mark 792 | 'gm' 793 | ); 794 | 795 | var dd = new RegExp( 796 | ['\\n(\\n+)?' , // leading line = $1 797 | '(' , // marker space = $2 798 | '[ ]{0,3}' , // whitespace before colon 799 | '[:][ ]+' , // definition mark (colon) 800 | ')' , 801 | '([\\s\\S]+?)' , // definition text = $3 802 | '(?=\\n*' , // stop at next definition mark, 803 | '(?:' , // next term or end of text 804 | '\\n[ ]{0,3}[:][ ]|' , 805 | '
    |\\x03' , // \z 806 | ')' , 807 | ')' 808 | ].join(''), 809 | 'gm' 810 | ); 811 | 812 | listStr = addAnchors(listStr); 813 | // trim trailing blank lines: 814 | listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n"); 815 | 816 | // Process definition terms. 817 | listStr = listStr.replace(dt, function(match, pre, termsStr) { 818 | var terms = trim(termsStr).split("\n"); 819 | var text = ''; 820 | for (var i = 0; i < terms.length; i++) { 821 | var term = terms[i]; 822 | // process spans inside dt 823 | term = convertSpans(trim(term), self); 824 | text += "\n
    " + term + "
    "; 825 | } 826 | return text + "\n"; 827 | }); 828 | 829 | // Process actual definitions. 830 | listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) { 831 | if (leadingLine || def.match(/\n{2,}/)) { 832 | // replace marker with the appropriate whitespace indentation 833 | def = Array(markerSpace.length + 1).join(' ') + def; 834 | // process markdown inside definition 835 | // TODO?: currently doesn't apply extensions 836 | def = outdent(def) + "\n\n"; 837 | def = "\n" + convertAll(def, self) + "\n"; 838 | } else { 839 | // convert span-level markdown inside definition 840 | def = rtrim(def); 841 | def = convertSpans(outdent(def), self); 842 | } 843 | 844 | return "\n
    " + def + "
    \n"; 845 | }); 846 | 847 | return removeAnchors(listStr); 848 | }; 849 | 850 | 851 | /*********************************************************** 852 | * Strikethrough * 853 | ************************************************************/ 854 | 855 | Markdown.Extra.prototype.strikethrough = function(text) { 856 | // Pretty much duplicated from _DoItalicsAndBold 857 | return text.replace(/([\W_]|^)~T~T(?=\S)([^\r]*?\S[\*_]*)~T~T([\W_]|$)/g, 858 | "$1$2$3"); 859 | }; 860 | 861 | 862 | /*********************************************************** 863 | * New lines * 864 | ************************************************************/ 865 | 866 | Markdown.Extra.prototype.newlines = function(text) { 867 | // We have to ignore already converted newlines and line breaks in sub-list items 868 | return text.replace(/(<(?:br|\/li)>)?\n/g, function(wholeMatch, previousTag) { 869 | return previousTag ? wholeMatch : "
    \n"; 870 | }); 871 | }; 872 | 873 | })(); 874 | 875 | -------------------------------------------------------------------------------- /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.md: -------------------------------------------------------------------------------- 1 | Writing 2 | ======= 3 | 4 | **Writing** is a lightweight distraction-free text editor, in the browser. 5 | 6 | Live version: [Writing](https://josephernest.github.io/writing/). 7 | 8 | 9 | 10 | 11 | Installation 12 | ---- 13 | Just open `index.html` and that's it! There is no server code. Is that so simple? Yes! 14 | 15 | Usage 16 | ---- 17 | 18 | * CTRL + D: Toggle display mode 19 | 20 | * CTRL + P: Print or export as PDF 21 | 22 | * CTRL + S: Save source code as .MD file 23 | 24 | and a few other commands (change font, etc.) that can be found in: 25 | 26 | * CTRL+SHIFT+H or `?` bottom-left icon: Show help 27 | 28 | 29 | Why another Markdown editor? Why not just use StackEdit? 30 | ---- 31 | There are many online editors that support Markdown but: 32 | 33 | * half of them don't support LaTeX / MathJax (for math formulas) 34 | * some of them do, but have a **1-sec delay between keypress and display**, and I find this annoying, see e.g. [StackEdit](https://stackedit.io) 35 | * some of them have annoying flickering each time you write new text, once math is present on the page 36 | * most of them are not minimalist / distraction-free enough for me 37 | 38 | That's why I decided to make **Writing**: 39 | 40 | * open-source 41 | * no server needed, you can run it offline 42 | * fast rendering (no delay when writing / no flickering of math equations) 43 | * **just what you need: write, preview, save the code, print or save as PDF, and nothing else** 44 | * LPWP website, a.k.a. "Landing Page=Working Page", i.e. the first page that you visit on the website is the page *where things actually happen*, that means that there is no annoying welcome page or login page, etc. 45 | 46 | About 47 | ---- 48 | Author: Joseph Ernest ([@JosephErnest](https://twitter.com/JosephErnest)) 49 | 50 | Other projects: [BigPicture](http://bigpicture.bi), [bigpicture.js](https://github.com/josephernest/bigpicture.js), [AReallyBigPage](https://github.com/josephernest/AReallyBigPage), [SamplerBox](http://www.samplerbox.org), [Void](https://github.com/josephernest/void), [TalkTalkTalk](https://github.com/josephernest/TalkTalkTalk), [sdfgh](https://github.com/josephernest/sdfgh), [RaspFIP](https://github.com/josephernest/RaspFIP/), [Yopp](https://github.com/josephernest/Yopp), etc. 51 | 52 | Sponsoring and consulting 53 | ---- 54 | 55 | I am available for Python, Data science, ML, Automation consulting. Please contact me on https://afewthingz.com for freelancing requests. 56 | 57 | Do you want to support the development of my open-source projects? Please contact me! 58 | 59 | I am currently sponsored by [CodeSigningStore.com](https://codesigningstore.com/). Thank you to them for providing a DigiCert Code Signing Certificate and supporting open source software. 60 | 61 | License 62 | ---- 63 | MIT license 64 | 65 | Dependencies 66 | --- 67 | **Writing** uses [Pagedown](https://code.google.com/archive/p/pagedown/), [Pagedown Extra](https://github.com/jmcmanus/pagedown-extra), [MathJax](https://www.mathjax.org/), StackOverflow's [editor code](https://gist.github.com/gdalgas/a652bce3a173ddc59f66), and the [Computer Modern](http://cm-unicode.sourceforge.net/) font. 68 | 69 | *Note: Some of these libraries have been slightly modified (a few lines of code), to make it work all together, that's why they are included in this package.* 70 | 71 | ![](https://gget.it/pixel/writing.png) 72 | -------------------------------------------------------------------------------- /cmunrb.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephernest/writing/408224b077b9118eb8ade39ee83393e22e292d29/cmunrb.otf -------------------------------------------------------------------------------- /cmunrm.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephernest/writing/408224b077b9118eb8ade39ee83393e22e292d29/cmunrm.otf -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephernest/writing/408224b077b9118eb8ade39ee83393e22e292d29/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Writing 22 | 23 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
    91 | 92 |
    93 | 94 |
    95 |
    ?
    96 |
    97 |
    X
    98 |
        
     99 | Writing is a lightweight distraction-free text editor.
    100 | Write text on the left, and the result is displayed on the right.
    101 | 
    102 | Commands
    103 | --------
    104 | CTRL + D: toggle display mode (editor only, preview only or both-at-the-same-time)
    105 | CTRL + P: print or export as PDF
    106 | CTRL + S: save source code as .MD file
    107 | CTRL + SHIFT + O: open .MD file
    108 | 
    109 | CTRL + SHIFT + L: enable / disable LaTeX (i.e. math formulas)
    110 | CTRL + SHIFT + D: toggle dark mode
    111 | CTRL + SHIFT + R: toggle roman (LaTex-like) or sans-serif font
    112 | CTRL + SHIFT + H: show this help dialog
    113 | 
    114 | F11: full-screen (in most browsers)
    115 | 
    116 | Markdown syntax
    117 | ---------------
    118 | #Title
    119 | ##Subtitle
    120 | This is *italic* and this is **bold**.
    121 | This is a [link](http://www.example.com/) and this is an ![image](imagelink.jpg).
    122 | Write code with `...` or by adding a 4-whitespace indent to the paragraph.
    123 | > This is a quote.
    124 | 
    125 | LaTeX syntax
    126 | ------------
    127 | This formula $x^2+1$ will be displayed inline.
    128 | This formula $$x^2+1$$ will be displayed in a new paragraph.
    129 | 
    130 | Specific syntax
    131 | ---------------
    132 | \pagebreak will trigger a pagebreak when printing / exporting to PDF.
    133 | 
    134 | About
    135 | -----
    136 | Made by @JosephErnest
    137 | https://github.com/josephernest/writing
    138 | Uses Pagedown, Pagedown Extra, MathJax, StackOverflow's editor code and the Computer Modern font.
    139 | 
    140 |
    141 | 142 | 330 | 331 | 332 | 333 | 334 | 335 | -------------------------------------------------------------------------------- /mathjax-editing_writing.js: -------------------------------------------------------------------------------- 1 | // Comes from: http://dev.stackoverflow.com/content/js/mathjax-editing.js (MIT-License) 2 | // Version downloaded 2016-11-21 3 | // 4 | // Two things modified: 5 | // 6 | // - StackExchange.mathjaxEditing = (function () { 7 | // + function mjpd() { this.mathjaxEditing = (function () { 8 | // - converterObject.hooks.chain("preSafe", replaceMath); 9 | // + converterObject.hooks.chain("postConversion", replaceMath); 10 | // - return { prepareWmdForMathJax: prepareWmdForMathJax };})(); 11 | // + return { prepareWmdForMathJax: prepareWmdForMathJax } })(); } 12 | 13 | 14 | "use strict"; 15 | 16 | function mjpd() { 17 | this.mathjaxEditing = (function () { 18 | var ready = false; // true after initial typeset is complete 19 | var pending = null; // non-null when typesetting has been queued 20 | var inline = "$"; // the inline math delimiter 21 | var blocks, start, end, last, braces, indent; // used in searching for math 22 | var math; // stores math until markdone is done 23 | var HUB = MathJax.Hub, TEX, NOERRORS; 24 | 25 | // 26 | // Runs after initial typeset 27 | // 28 | HUB.Queue(function () { 29 | TEX = MathJax.InputJax.TeX; 30 | NOERRORS = TEX.config.noErrors; 31 | ready = true; 32 | HUB.processUpdateTime = 50; // reduce update time so that we can cancel easier 33 | HUB.processSectionDelay = 0; // don't pause between input and output phases 34 | MathJax.Extension["fast-preview"].Disable(); // disable fast-preview 35 | HUB.Config({ 36 | // reduce chunk for more frequent updates 37 | "HTML-CSS": { 38 | EqnChunk: 10, 39 | EqnChunkFactor: 1 40 | }, 41 | CommonHTML: { 42 | EqnChunk: 10, 43 | EqnChunkFactor: 1 44 | }, 45 | SVG: { 46 | EqnChunk: 10, 47 | EqnChunkFactor: 1 48 | } 49 | }); 50 | if (pending) return RestartMJ(pending, "Typeset"); 51 | }); 52 | 53 | // 54 | // These get called before and after typsetting 55 | // 56 | function preTypeset() { 57 | NOERRORS.disabled = true; // disable noErrors (error will be shown) 58 | TEX.resetEquationNumbers(); // reset labels 59 | } 60 | function postTypeset() { 61 | NOERRORS.disabled = false; // don't show errors when not editing 62 | } 63 | 64 | // 65 | // The pattern for math delimiters and special symbols 66 | // needed for searching for math in the page. 67 | // 68 | var SPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@|`+)/i; 69 | 70 | // 71 | // The math is in blocks i through j, so 72 | // collect it into one block and clear the others. 73 | // Replace &, <, and > by named entities. 74 | // For IE, put
    at the ends of comments since IE removes \n. 75 | // Clear the current math positions and store the index of the 76 | // math, then push the math string onto the storage array. 77 | // 78 | function processMath(i, j) { 79 | var block = blocks.slice(i, j + 1).join("") 80 | .replace(/&/g, "&") // use HTML entity for & 81 | .replace(//g, ">") // use HTML entity for > 83 | ; 84 | if (indent) block = block.replace(/\n /g, "\n"); 85 | if (HUB.Browser.isMSIE) { 86 | block = block.replace(/(%[^\n]*)\n/g, "$1
    \n"); 87 | } 88 | while (j > i) blocks[j--] = ""; 89 | blocks[i] = "@@" + math.length + "@@"; 90 | math.push(block); 91 | start = end = last = null; 92 | } 93 | 94 | 95 | var capturingStringSplit; 96 | if ("aba".split(/(b)/).length === 3) { 97 | capturingStringSplit = function (str, regex) { return str.split(regex); }; 98 | } 99 | else { // IE8 100 | capturingStringSplit = function (str, regex) { 101 | var result = [], match; 102 | if (!regex.global) { 103 | var source = regex.toString(), 104 | flags = ""; 105 | source = source.replace(/^\/(.*)\/([im]*)$/, function (wholematch, re, fl) { flags = fl; return re; }); 106 | regex = new RegExp(source, flags + "g"); 107 | } 108 | regex.lastIndex = 0; 109 | var lastPos = 0; 110 | while ((match = regex.exec(str))) { 111 | result.push(str.substring(lastPos, match.index)); 112 | result.push.apply(result, match.slice(1)); 113 | lastPos = match.index + match[0].length; 114 | } 115 | result.push(str.substring(lastPos)); 116 | return result; 117 | }; 118 | } 119 | 120 | 121 | // 122 | // Break up the text into its component parts and search 123 | // through them for math delimiters, braces, linebreaks, etc. 124 | // Math delimiters must match and braces must balance. 125 | // Don't allow math to pass through a double linebreak 126 | // (which will be a paragraph). 127 | // Handle backticks (don't do math inside them) 128 | // 129 | function removeMath(text) { 130 | start = end = last = indent = null; // for tracking math delimiters 131 | math = []; // stores math strings for latter 132 | 133 | blocks = capturingStringSplit(text.replace(/\r\n?/g, "\n"), SPLIT); 134 | 135 | for (var i = 1, m = blocks.length; i < m; i += 2) { 136 | var block = blocks[i]; 137 | if (block.charAt(0) === "@") { 138 | // 139 | // Things that look like our math markers will get 140 | // stored and then retrieved along with the math. 141 | // 142 | blocks[i] = "@@" + math.length + "@@"; 143 | math.push(block); 144 | } 145 | else if (start) { 146 | // 147 | // If we are in math or backticks, 148 | // look for the end delimiter, 149 | // but don't go past double line breaks, 150 | // and balance braces within the math, 151 | // but don't process math inside backticks. 152 | // 153 | if (block === end) { 154 | if (braces > 0) { 155 | last = i; 156 | } 157 | else if (braces === 0) { 158 | processMath(start, i); 159 | } 160 | else { 161 | start = end = last = null; 162 | } 163 | } 164 | else if (block.match(/\n.*\n/) || i + 2 >= m) { 165 | if (last) { 166 | i = last; 167 | if (braces >= 0) processMath(start, i); 168 | } 169 | start = end = last = null; 170 | braces = 0; 171 | } 172 | else if (block === "{" && braces >= 0) { 173 | braces++; 174 | } 175 | else if (block === "}" && braces > 0) { 176 | braces--; 177 | } 178 | } 179 | else { 180 | // 181 | // Look for math start delimiters and when 182 | // found, set up the end delimiter. 183 | // 184 | if (block === inline || block === "$$") { 185 | start = i; 186 | end = block; 187 | braces = 0; 188 | } 189 | else if (block.substr(1, 5) === "begin") { 190 | start = i; 191 | end = "\\end" + block.substr(6); 192 | braces = 0; 193 | } 194 | else if (block.charAt(0) === "`") { 195 | start = last = i; 196 | end = block; 197 | braces = -1; // no brace balancing 198 | } 199 | else if (block.charAt(0) === "\n") { 200 | if (block.match(/ $/)) indent = true; 201 | } 202 | } 203 | } 204 | if (last) processMath(start, last); 205 | return blocks.join(""); 206 | } 207 | 208 | // 209 | // Put back the math strings that were saved, 210 | // and clear the math array (no need to keep it around). 211 | // 212 | function replaceMath(text) { 213 | text = text.replace(/@@(\d+)@@/g, function (match, n) { 214 | return math[n]; 215 | }); 216 | math = null; 217 | return text; 218 | } 219 | 220 | // 221 | // This is run to restart MathJax after it has finished 222 | // the previous run (that may have been canceled) 223 | // 224 | function RestartMJ(preview, method) { 225 | pending = false; 226 | HUB.cancelTypeset = false; // won't need to do this in the future 227 | HUB.Queue( 228 | preTypeset, 229 | [method, HUB, preview], 230 | postTypeset 231 | ); 232 | } 233 | 234 | // 235 | // When the preview changes, cancel MathJax and restart, 236 | // if we haven't done that already. 237 | // 238 | function UpdateMJ(preview, method) { 239 | if (!pending) { 240 | pending = preview; 241 | if (ready) { 242 | HUB.Cancel(); 243 | HUB.Queue([RestartMJ, preview, method]); 244 | } 245 | } 246 | } 247 | 248 | // 249 | // Save the preview ID and the inline math delimiter. 250 | // Create a converter for the editor and register a preConversion hook 251 | // to handle escaping the math. 252 | // Create a preview refresh hook to handle starting MathJax. 253 | // Check if any errors are being displayed (in case there were 254 | // errors in the initial display, which doesn't go through 255 | // onPreviewRefresh), and reprocess if there are. 256 | // 257 | function prepareWmdForMathJax(editorObject, wmdId, delimiters) { 258 | var preview = document.getElementById("wmd-preview" + wmdId); 259 | inline = delimiters[0][0]; 260 | 261 | var converterObject = editorObject.getConverter(); 262 | converterObject.hooks.chain("preConversion", removeMath); 263 | converterObject.hooks.chain("postConversion", replaceMath); 264 | editorObject.hooks.chain("onPreviewRefresh", function () { 265 | UpdateMJ(preview, "Typeset"); 266 | }); 267 | 268 | HUB.Queue(function () { 269 | if (preview && preview.querySelector(".mjx-noError")) { 270 | RestartMJ(preview, "Reprocess"); 271 | } 272 | }); 273 | } 274 | 275 | return { 276 | prepareWmdForMathJax: prepareWmdForMathJax 277 | } 278 | })(); 279 | } 280 | // 281 | // Set up MathJax to allow canceling of typesetting, if it 282 | // doesn't already have that. 283 | // 284 | (function () { 285 | var HUB = MathJax.Hub; 286 | 287 | if (!HUB.Cancel) { 288 | 289 | HUB.cancelTypeset = false; 290 | var CANCELMESSAGE = "MathJax Canceled"; 291 | 292 | HUB.Register.StartupHook("HTML-CSS Jax Config", function () { 293 | var HTMLCSS = MathJax.OutputJax["HTML-CSS"], 294 | TRANSLATE = HTMLCSS.Translate; 295 | HTMLCSS.Augment({ 296 | Translate: function (script, state) { 297 | if (HUB.cancelTypeset || state.cancelled) { 298 | throw Error(CANCELMESSAGE) 299 | } 300 | return TRANSLATE.call(HTMLCSS, script, state); 301 | } 302 | }); 303 | }); 304 | 305 | HUB.Register.StartupHook("SVG Jax Config", function () { 306 | var SVG = MathJax.OutputJax["SVG"], 307 | TRANSLATE = SVG.Translate; 308 | SVG.Augment({ 309 | Translate: function (script, state) { 310 | if (HUB.cancelTypeset || state.cancelled) { 311 | throw Error(CANCELMESSAGE) 312 | } 313 | return TRANSLATE.call(SVG, script, state); 314 | } 315 | }); 316 | }); 317 | 318 | HUB.Register.StartupHook("CommonHTML Jax Config", function () { 319 | var CHTML = MathJax.OutputJax.CommonHTML, 320 | TRANSLATE = CHTML.Translate; 321 | CHTML.Augment({ 322 | Translate: function (script, state) { 323 | if (HUB.cancelTypeset || state.cancelled) { 324 | throw Error(CANCELMESSAGE); 325 | } 326 | return TRANSLATE.call(CHTML, script, state); 327 | } 328 | }); 329 | }); 330 | 331 | HUB.Register.StartupHook("PreviewHTML Jax Config", function () { 332 | var PHTML = MathJax.OutputJax.PreviewHTML, 333 | TRANSLATE = PHTML.Translate; 334 | PHTML.Augment({ 335 | Translate: function (script, state) { 336 | if (HUB.cancelTypeset || state.cancelled) { 337 | throw Error(CANCELMESSAGE); 338 | } 339 | return TRANSLATE.call(PHTML, script, state); 340 | } 341 | }); 342 | }); 343 | 344 | HUB.Register.StartupHook("TeX Jax Config", function () { 345 | var TEX = MathJax.InputJax.TeX, 346 | TRANSLATE = TEX.Translate; 347 | TEX.Augment({ 348 | Translate: function (script, state) { 349 | if (HUB.cancelTypeset || state.cancelled) { 350 | throw Error(CANCELMESSAGE) 351 | } 352 | return TRANSLATE.call(TEX, script, state); 353 | } 354 | }); 355 | }); 356 | 357 | var PROCESSERROR = HUB.processError; 358 | HUB.processError = function (error, state, type) { 359 | if (error.message !== CANCELMESSAGE) { 360 | return PROCESSERROR.call(HUB, error, state, type) 361 | } 362 | MathJax.Message.Clear(0, 0); 363 | state.jaxIDs = []; 364 | state.jax = {}; 365 | state.scripts = []; 366 | state.i = state.j = 0; 367 | state.cancelled = true; 368 | return null; 369 | }; 370 | 371 | HUB.Cancel = function () { 372 | this.cancelTypeset = true; 373 | }; 374 | } 375 | })(); --------------------------------------------------------------------------------