├── .gitignore ├── README.md ├── css └── base.css ├── index.html ├── scripts ├── example.js └── showdown.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React comment box example 2 | 3 | This is the React comment box example from [the React tutorial](http://facebook.github.io/react/docs/tutorial.html). 4 | 5 | ## To use 6 | 7 | ``` 8 | npm install express 9 | node server.js 10 | ``` 11 | 12 | And visit http://localhost:3000/. Try opening multiple tabs! 13 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;; 4 | font-size: 15px; 5 | line-height: 1.7; 6 | margin: 0; 7 | padding: 30px; 8 | } 9 | 10 | a { 11 | color: #4183c4; 12 | text-decoration: none; 13 | } 14 | 15 | a:hover { 16 | text-decoration: underline; 17 | } 18 | 19 | code { 20 | background-color: #f8f8f8; 21 | border: 1px solid #ddd; 22 | border-radius: 3px; 23 | font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; 24 | font-size: 12px; 25 | margin: 0 2px; 26 | padding: 0px 5px; 27 | } 28 | 29 | h1, h2, h3, h4 { 30 | font-weight: bold; 31 | margin: 0 0 15px; 32 | padding: 0; 33 | } 34 | 35 | h1 { 36 | border-bottom: 1px solid #ddd; 37 | font-size: 2.5em; 38 | font-weight: bold; 39 | margin: 0 0 15px; 40 | padding: 0; 41 | } 42 | 43 | h2 { 44 | border-bottom: 1px solid #eee; 45 | font-size: 2em; 46 | } 47 | 48 | h3 { 49 | font-size: 1.5em; 50 | } 51 | 52 | h4 { 53 | font-size: 1.2em; 54 | } 55 | 56 | p, ul { 57 | margin: 15px 0; 58 | } 59 | 60 | ul { 61 | padding-left: 30px; 62 | } 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Basic Example with external JSX 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/example.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var converter = new Showdown.converter(); 4 | 5 | var Comment = React.createClass({ 6 | render: function() { 7 | var rawMarkup = converter.makeHtml(this.props.children.toString()); 8 | return ( 9 |
10 |

{this.props.author}

11 | 12 |
13 | ); 14 | } 15 | }); 16 | 17 | var CommentBox = React.createClass({ 18 | loadCommentsFromServer: function() { 19 | $.ajax({ 20 | url: this.props.url, 21 | success: function(data) { 22 | this.setState({data: data}); 23 | }.bind(this) 24 | }); 25 | }, 26 | handleCommentSubmit: function(comment) { 27 | var comments = this.state.data; 28 | comments.push(comment); 29 | this.setState({data: comments}); 30 | $.ajax({ 31 | url: this.props.url, 32 | type: 'POST', 33 | data: comment, 34 | success: function(data) { 35 | this.setState({data: data}); 36 | }.bind(this) 37 | }); 38 | }, 39 | getInitialState: function() { 40 | return {data: []}; 41 | }, 42 | componentWillMount: function() { 43 | this.loadCommentsFromServer(); 44 | setInterval(this.loadCommentsFromServer, this.props.pollInterval); 45 | }, 46 | render: function() { 47 | return ( 48 |
49 |

Comments

50 | 51 | 52 |
53 | ); 54 | } 55 | }); 56 | 57 | var CommentList = React.createClass({ 58 | render: function() { 59 | var commentNodes = this.props.data.map(function (comment, index) { 60 | return {comment.text}; 61 | }); 62 | return
{commentNodes}
; 63 | } 64 | }); 65 | 66 | var CommentForm = React.createClass({ 67 | handleSubmit: function() { 68 | var author = this.refs.author.getDOMNode().value.trim(); 69 | var text = this.refs.text.getDOMNode().value.trim(); 70 | this.props.onCommentSubmit({author: author, text: text}); 71 | this.refs.author.getDOMNode().value = ''; 72 | this.refs.text.getDOMNode().value = ''; 73 | return false; 74 | }, 75 | render: function() { 76 | return ( 77 |
78 | 79 | 80 | 81 |
82 | ); 83 | } 84 | }); 85 | 86 | React.renderComponent( 87 | , 88 | document.getElementById('container') 89 | ); 90 | -------------------------------------------------------------------------------- /scripts/showdown.js: -------------------------------------------------------------------------------- 1 | // 2 | // showdown.js -- A javascript port of Markdown. 3 | // 4 | // Copyright (c) 2007 John Fraser. 5 | // 6 | // Original Markdown Copyright (c) 2004-2005 John Gruber 7 | // 8 | // 9 | // Redistributable under a BSD-style open source license. 10 | // See license.txt for more information. 11 | // 12 | // The full source distribution is at: 13 | // 14 | // A A L 15 | // T C A 16 | // T K B 17 | // 18 | // 19 | // 20 | 21 | // 22 | // Wherever possible, Showdown is a straight, line-by-line port 23 | // of the Perl version of Markdown. 24 | // 25 | // This is not a normal parser design; it's basically just a 26 | // series of string substitutions. It's hard to read and 27 | // maintain this way, but keeping Showdown close to the original 28 | // design makes it easier to port new features. 29 | // 30 | // More importantly, Showdown behaves like markdown.pl in most 31 | // edge cases. So web applications can do client-side preview 32 | // in Javascript, and then build identical HTML on the server. 33 | // 34 | // This port needs the new RegExp functionality of ECMA 262, 35 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 36 | // should do fine. Even with the new regular expression features, 37 | // We do a lot of work to emulate Perl's regex functionality. 38 | // The tricky changes in this file mostly have the "attacklab:" 39 | // label. Major or self-explanatory changes don't. 40 | // 41 | // Smart diff tools like Araxis Merge will be able to match up 42 | // this file with markdown.pl in a useful way. A little tweaking 43 | // helps: in a copy of markdown.pl, replace "#" with "//" and 44 | // replace "$text" with "text". Be sure to ignore whitespace 45 | // and line endings. 46 | // 47 | 48 | 49 | // 50 | // Showdown usage: 51 | // 52 | // var text = "Markdown *rocks*."; 53 | // 54 | // var converter = new Showdown.converter(); 55 | // var html = converter.makeHtml(text); 56 | // 57 | // alert(html); 58 | // 59 | // Note: move the sample code to the bottom of this 60 | // file before uncommenting it. 61 | // 62 | 63 | 64 | // 65 | // Showdown namespace 66 | // 67 | var Showdown = {}; 68 | 69 | // 70 | // converter 71 | // 72 | // Wraps all "globals" so that the only thing 73 | // exposed is makeHtml(). 74 | // 75 | Showdown.converter = function() { 76 | 77 | // 78 | // Globals: 79 | // 80 | 81 | // Global hashes, used by various utility routines 82 | var g_urls; 83 | var g_titles; 84 | var g_html_blocks; 85 | 86 | // Used to track when we're inside an ordered or unordered list 87 | // (see _ProcessListItems() for details): 88 | var g_list_level = 0; 89 | 90 | 91 | this.makeHtml = function(text) { 92 | // 93 | // Main function. The order in which other subs are called here is 94 | // essential. Link and image substitutions need to happen before 95 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 96 | // and tags get encoded. 97 | // 98 | 99 | // Clear the global hashes. If we don't clear these, you get conflicts 100 | // from other articles when generating a page which contains more than 101 | // one article (e.g. an index page that shows the N most recent 102 | // articles): 103 | g_urls = new Array(); 104 | g_titles = new Array(); 105 | g_html_blocks = new Array(); 106 | 107 | // attacklab: Replace ~ with ~T 108 | // This lets us use tilde as an escape char to avoid md5 hashes 109 | // The choice of character is arbitray; anything that isn't 110 | // magic in Markdown will work. 111 | text = text.replace(/~/g,"~T"); 112 | 113 | // attacklab: Replace $ with ~D 114 | // RegExp interprets $ as a special character 115 | // when it's in a replacement string 116 | text = text.replace(/\$/g,"~D"); 117 | 118 | // Standardize line endings 119 | text = text.replace(/\r\n/g,"\n"); // DOS to Unix 120 | text = text.replace(/\r/g,"\n"); // Mac to Unix 121 | 122 | // Make sure text begins and ends with a couple of newlines: 123 | text = "\n\n" + text + "\n\n"; 124 | 125 | // Convert all tabs to spaces. 126 | text = _Detab(text); 127 | 128 | // Strip any lines consisting only of spaces and tabs. 129 | // This makes subsequent regexen easier to write, because we can 130 | // match consecutive blank lines with /\n+/ instead of something 131 | // contorted like /[ \t]*\n+/ . 132 | text = text.replace(/^[ \t]+$/mg,""); 133 | 134 | // Turn block-level HTML blocks into hash entries 135 | text = _HashHTMLBlocks(text); 136 | 137 | // Strip link definitions, store in hashes. 138 | text = _StripLinkDefinitions(text); 139 | 140 | text = _RunBlockGamut(text); 141 | 142 | text = _UnescapeSpecialChars(text); 143 | 144 | // attacklab: Restore dollar signs 145 | text = text.replace(/~D/g,"$$"); 146 | 147 | // attacklab: Restore tildes 148 | text = text.replace(/~T/g,"~"); 149 | 150 | return text; 151 | } 152 | 153 | 154 | var _StripLinkDefinitions = function(text) { 155 | // 156 | // Strips link definitions from text, stores the URLs and titles in 157 | // hash references. 158 | // 159 | 160 | // Link defs are in the form: ^[id]: url "optional title" 161 | 162 | /* 163 | var text = text.replace(/ 164 | ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 165 | [ \t]* 166 | \n? // maybe *one* newline 167 | [ \t]* 168 | ? // url = $2 169 | [ \t]* 170 | \n? // maybe one newline 171 | [ \t]* 172 | (?: 173 | (\n*) // any lines skipped = $3 attacklab: lookbehind removed 174 | ["(] 175 | (.+?) // title = $4 176 | [")] 177 | [ \t]* 178 | )? // title is optional 179 | (?:\n+|$) 180 | /gm, 181 | function(){...}); 182 | */ 183 | var text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|\Z)/gm, 184 | function (wholeMatch,m1,m2,m3,m4) { 185 | m1 = m1.toLowerCase(); 186 | g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive 187 | if (m3) { 188 | // Oops, found blank lines, so it's not a title. 189 | // Put back the parenthetical statement we stole. 190 | return m3+m4; 191 | } else if (m4) { 192 | g_titles[m1] = m4.replace(/"/g,"""); 193 | } 194 | 195 | // Completely remove the definition from the text 196 | return ""; 197 | } 198 | ); 199 | 200 | return text; 201 | } 202 | 203 | 204 | var _HashHTMLBlocks = function(text) { 205 | // attacklab: Double up blank lines to reduce lookaround 206 | text = text.replace(/\n/g,"\n\n"); 207 | 208 | // Hashify HTML blocks: 209 | // We only want to do this for block-level HTML tags, such as headers, 210 | // lists, and tables. That's because we still want to wrap

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

219 | //
220 | // tags for inner block must be indented. 221 | //
222 | //
223 | // 224 | // The outermost tags must start at the left margin for this to match, and 225 | // the inner nested divs must be indented. 226 | // We need to do this before the next, more liberal match, because the next 227 | // match will start at the first `
` and stop at the first `
`. 228 | 229 | // attacklab: This regex can be expensive when it fails. 230 | /* 231 | var text = text.replace(/ 232 | ( // save in $1 233 | ^ // start of line (with /m) 234 | <($block_tags_a) // start tag = $2 235 | \b // word break 236 | // attacklab: hack around khtml/pcre bug... 237 | [^\r]*?\n // any number of lines, minimally matching 238 | // the matching end tag 239 | [ \t]* // trailing spaces/tabs 240 | (?=\n+) // followed by a newline 241 | ) // attacklab: there are sentinel newlines at end of document 242 | /gm,function(){...}}; 243 | */ 244 | 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,hashElement); 245 | 246 | // 247 | // Now match more liberally, simply from `\n` to `\n` 248 | // 249 | 250 | /* 251 | var text = text.replace(/ 252 | ( // save in $1 253 | ^ // start of line (with /m) 254 | <($block_tags_b) // start tag = $2 255 | \b // word break 256 | // attacklab: hack around khtml/pcre bug... 257 | [^\r]*? // any number of lines, minimally matching 258 | .* // the matching end tag 259 | [ \t]* // trailing spaces/tabs 260 | (?=\n+) // followed by a newline 261 | ) // attacklab: there are sentinel newlines at end of document 262 | /gm,function(){...}}; 263 | */ 264 | 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,hashElement); 265 | 266 | // Special case just for
. It was easier to make a special case than 267 | // to make the other regex more complicated. 268 | 269 | /* 270 | text = text.replace(/ 271 | ( // save in $1 272 | \n\n // Starting after a blank line 273 | [ ]{0,3} 274 | (<(hr) // start tag = $2 275 | \b // word break 276 | ([^<>])*? // 277 | \/?>) // the matching end tag 278 | [ \t]* 279 | (?=\n{2,}) // followed by a blank line 280 | ) 281 | /g,hashElement); 282 | */ 283 | text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,hashElement); 284 | 285 | // Special case for standalone HTML comments: 286 | 287 | /* 288 | text = text.replace(/ 289 | ( // save in $1 290 | \n\n // Starting after a blank line 291 | [ ]{0,3} // attacklab: g_tab_width - 1 292 | 295 | [ \t]* 296 | (?=\n{2,}) // followed by a blank line 297 | ) 298 | /g,hashElement); 299 | */ 300 | text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,hashElement); 301 | 302 | // PHP and ASP-style processor instructions ( and <%...%>) 303 | 304 | /* 305 | text = text.replace(/ 306 | (?: 307 | \n\n // Starting after a blank line 308 | ) 309 | ( // save in $1 310 | [ ]{0,3} // attacklab: g_tab_width - 1 311 | (?: 312 | <([?%]) // $2 313 | [^\r]*? 314 | \2> 315 | ) 316 | [ \t]* 317 | (?=\n{2,}) // followed by a blank line 318 | ) 319 | /g,hashElement); 320 | */ 321 | text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,hashElement); 322 | 323 | // attacklab: Undo double lines (see comment at top of this function) 324 | text = text.replace(/\n\n/g,"\n"); 325 | return text; 326 | } 327 | 328 | var hashElement = function(wholeMatch,m1) { 329 | var blockText = m1; 330 | 331 | // Undo double lines 332 | blockText = blockText.replace(/\n\n/g,"\n"); 333 | blockText = blockText.replace(/^\n/,""); 334 | 335 | // strip trailing blank lines 336 | blockText = blockText.replace(/\n+$/g,""); 337 | 338 | // Replace the element text with a marker ("~KxK" where x is its key) 339 | blockText = "\n\n~K" + (g_html_blocks.push(blockText)-1) + "K\n\n"; 340 | 341 | return blockText; 342 | }; 343 | 344 | var _RunBlockGamut = function(text) { 345 | // 346 | // These are all the transformations that form block-level 347 | // tags like paragraphs, headers, and list items. 348 | // 349 | text = _DoHeaders(text); 350 | 351 | // Do Horizontal Rules: 352 | var key = hashBlock("
"); 353 | text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key); 354 | text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key); 355 | text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key); 356 | 357 | text = _DoLists(text); 358 | text = _DoCodeBlocks(text); 359 | text = _DoBlockQuotes(text); 360 | 361 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 362 | // was to escape raw HTML in the original Markdown source. This time, 363 | // we're escaping the markup we've just created, so that we don't wrap 364 | //

tags around block-level tags. 365 | text = _HashHTMLBlocks(text); 366 | text = _FormParagraphs(text); 367 | 368 | return text; 369 | } 370 | 371 | 372 | var _RunSpanGamut = function(text) { 373 | // 374 | // These are all the transformations that occur *within* block-level 375 | // tags like paragraphs, headers, and list items. 376 | // 377 | 378 | text = _DoCodeSpans(text); 379 | text = _EscapeSpecialCharsWithinTagAttributes(text); 380 | text = _EncodeBackslashEscapes(text); 381 | 382 | // Process anchor and image tags. Images must come first, 383 | // because ![foo][f] looks like an anchor. 384 | text = _DoImages(text); 385 | text = _DoAnchors(text); 386 | 387 | // Make links out of things like `` 388 | // Must come after _DoAnchors(), because you can use < and > 389 | // delimiters in inline links like [this](). 390 | text = _DoAutoLinks(text); 391 | text = _EncodeAmpsAndAngles(text); 392 | text = _DoItalicsAndBold(text); 393 | 394 | // Do hard breaks: 395 | text = text.replace(/ +\n/g,"
\n"); 396 | 397 | return text; 398 | } 399 | 400 | var _EscapeSpecialCharsWithinTagAttributes = function(text) { 401 | // 402 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they 403 | // don't conflict with their use in Markdown for code, italics and strong. 404 | // 405 | 406 | // Build a regex to find HTML tags and comments. See Friedl's 407 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. 408 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi; 409 | 410 | text = text.replace(regex, function(wholeMatch) { 411 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g,"$1`"); 412 | tag = escapeCharacters(tag,"\\`*_"); 413 | return tag; 414 | }); 415 | 416 | return text; 417 | } 418 | 419 | var _DoAnchors = function(text) { 420 | // 421 | // Turn Markdown link shortcuts into XHTML
tags. 422 | // 423 | // 424 | // First, handle reference-style links: [link text] [id] 425 | // 426 | 427 | /* 428 | text = text.replace(/ 429 | ( // wrap whole match in $1 430 | \[ 431 | ( 432 | (?: 433 | \[[^\]]*\] // allow brackets nested one level 434 | | 435 | [^\[] // or anything else 436 | )* 437 | ) 438 | \] 439 | 440 | [ ]? // one optional space 441 | (?:\n[ ]*)? // one optional newline followed by spaces 442 | 443 | \[ 444 | (.*?) // id = $3 445 | \] 446 | )()()()() // pad remaining backreferences 447 | /g,_DoAnchors_callback); 448 | */ 449 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeAnchorTag); 450 | 451 | // 452 | // Next, inline-style links: [link text](url "optional title") 453 | // 454 | 455 | /* 456 | text = text.replace(/ 457 | ( // wrap whole match in $1 458 | \[ 459 | ( 460 | (?: 461 | \[[^\]]*\] // allow brackets nested one level 462 | | 463 | [^\[\]] // or anything else 464 | ) 465 | ) 466 | \] 467 | \( // literal paren 468 | [ \t]* 469 | () // no id, so leave $3 empty 470 | ? // href = $4 471 | [ \t]* 472 | ( // $5 473 | (['"]) // quote char = $6 474 | (.*?) // Title = $7 475 | \6 // matching quote 476 | [ \t]* // ignore any spaces/tabs between closing quote and ) 477 | )? // title is optional 478 | \) 479 | ) 480 | /g,writeAnchorTag); 481 | */ 482 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag); 483 | 484 | // 485 | // Last, handle reference-style shortcuts: [link text] 486 | // These must come last in case you've also got [link test][1] 487 | // or [link test](/foo) 488 | // 489 | 490 | /* 491 | text = text.replace(/ 492 | ( // wrap whole match in $1 493 | \[ 494 | ([^\[\]]+) // link text = $2; can't contain '[' or ']' 495 | \] 496 | )()()()()() // pad rest of backreferences 497 | /g, writeAnchorTag); 498 | */ 499 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); 500 | 501 | return text; 502 | } 503 | 504 | var writeAnchorTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { 505 | if (m7 == undefined) m7 = ""; 506 | var whole_match = m1; 507 | var link_text = m2; 508 | var link_id = m3.toLowerCase(); 509 | var url = m4; 510 | var title = m7; 511 | 512 | if (url == "") { 513 | if (link_id == "") { 514 | // lower-case and turn embedded newlines into spaces 515 | link_id = link_text.toLowerCase().replace(/ ?\n/g," "); 516 | } 517 | url = "#"+link_id; 518 | 519 | if (g_urls[link_id] != undefined) { 520 | url = g_urls[link_id]; 521 | if (g_titles[link_id] != undefined) { 522 | title = g_titles[link_id]; 523 | } 524 | } 525 | else { 526 | if (whole_match.search(/\(\s*\)$/m)>-1) { 527 | // Special case for explicit empty url 528 | url = ""; 529 | } else { 530 | return whole_match; 531 | } 532 | } 533 | } 534 | 535 | url = escapeCharacters(url,"*_"); 536 | var result = ""; 545 | 546 | return result; 547 | } 548 | 549 | 550 | var _DoImages = function(text) { 551 | // 552 | // Turn Markdown image shortcuts into tags. 553 | // 554 | 555 | // 556 | // First, handle reference-style labeled images: ![alt text][id] 557 | // 558 | 559 | /* 560 | text = text.replace(/ 561 | ( // wrap whole match in $1 562 | !\[ 563 | (.*?) // alt text = $2 564 | \] 565 | 566 | [ ]? // one optional space 567 | (?:\n[ ]*)? // one optional newline followed by spaces 568 | 569 | \[ 570 | (.*?) // id = $3 571 | \] 572 | )()()()() // pad rest of backreferences 573 | /g,writeImageTag); 574 | */ 575 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeImageTag); 576 | 577 | // 578 | // Next, handle inline images: ![alt text](url "optional title") 579 | // Don't forget: encode * and _ 580 | 581 | /* 582 | text = text.replace(/ 583 | ( // wrap whole match in $1 584 | !\[ 585 | (.*?) // alt text = $2 586 | \] 587 | \s? // One optional whitespace character 588 | \( // literal paren 589 | [ \t]* 590 | () // no id, so leave $3 empty 591 | ? // src url = $4 592 | [ \t]* 593 | ( // $5 594 | (['"]) // quote char = $6 595 | (.*?) // title = $7 596 | \6 // matching quote 597 | [ \t]* 598 | )? // title is optional 599 | \) 600 | ) 601 | /g,writeImageTag); 602 | */ 603 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeImageTag); 604 | 605 | return text; 606 | } 607 | 608 | var writeImageTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { 609 | var whole_match = m1; 610 | var alt_text = m2; 611 | var link_id = m3.toLowerCase(); 612 | var url = m4; 613 | var title = m7; 614 | 615 | if (!title) title = ""; 616 | 617 | if (url == "") { 618 | if (link_id == "") { 619 | // lower-case and turn embedded newlines into spaces 620 | link_id = alt_text.toLowerCase().replace(/ ?\n/g," "); 621 | } 622 | url = "#"+link_id; 623 | 624 | if (g_urls[link_id] != undefined) { 625 | url = g_urls[link_id]; 626 | if (g_titles[link_id] != undefined) { 627 | title = g_titles[link_id]; 628 | } 629 | } 630 | else { 631 | return whole_match; 632 | } 633 | } 634 | 635 | alt_text = alt_text.replace(/"/g,"""); 636 | url = escapeCharacters(url,"*_"); 637 | var result = "\""' + _RunSpanGamut(m1) + "");}); 665 | 666 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, 667 | function(matchFound,m1){return hashBlock('

' + _RunSpanGamut(m1) + "

");}); 668 | 669 | // atx-style headers: 670 | // # Header 1 671 | // ## Header 2 672 | // ## Header 2 with closing hashes ## 673 | // ... 674 | // ###### Header 6 675 | // 676 | 677 | /* 678 | text = text.replace(/ 679 | ^(\#{1,6}) // $1 = string of #'s 680 | [ \t]* 681 | (.+?) // $2 = Header text 682 | [ \t]* 683 | \#* // optional closing #'s (not counted) 684 | \n+ 685 | /gm, function() {...}); 686 | */ 687 | 688 | text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, 689 | function(wholeMatch,m1,m2) { 690 | var h_level = m1.length; 691 | return hashBlock("' + _RunSpanGamut(m2) + ""); 692 | }); 693 | 694 | function headerId(m) { 695 | return m.replace(/[^\w]/g, '').toLowerCase(); 696 | } 697 | return text; 698 | } 699 | 700 | // This declaration keeps Dojo compressor from outputting garbage: 701 | var _ProcessListItems; 702 | 703 | var _DoLists = function(text) { 704 | // 705 | // Form HTML ordered (numbered) and unordered (bulleted) lists. 706 | // 707 | 708 | // attacklab: add sentinel to hack around khtml/safari bug: 709 | // http://bugs.webkit.org/show_bug.cgi?id=11231 710 | text += "~0"; 711 | 712 | // Re-usable pattern to match any entirel ul or ol list: 713 | 714 | /* 715 | var whole_list = / 716 | ( // $1 = whole list 717 | ( // $2 718 | [ ]{0,3} // attacklab: g_tab_width - 1 719 | ([*+-]|\d+[.]) // $3 = first list item marker 720 | [ \t]+ 721 | ) 722 | [^\r]+? 723 | ( // $4 724 | ~0 // sentinel for workaround; should be $ 725 | | 726 | \n{2,} 727 | (?=\S) 728 | (?! // Negative lookahead for another list item marker 729 | [ \t]* 730 | (?:[*+-]|\d+[.])[ \t]+ 731 | ) 732 | ) 733 | )/g 734 | */ 735 | var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 736 | 737 | if (g_list_level) { 738 | text = text.replace(whole_list,function(wholeMatch,m1,m2) { 739 | var list = m1; 740 | var list_type = (m2.search(/[*+-]/g)>-1) ? "ul" : "ol"; 741 | 742 | // Turn double returns into triple returns, so that we can make a 743 | // paragraph for the last item in a list, if necessary: 744 | list = list.replace(/\n{2,}/g,"\n\n\n");; 745 | var result = _ProcessListItems(list); 746 | 747 | // Trim any trailing whitespace, to put the closing `` 748 | // up on the preceding line, to get it past the current stupid 749 | // HTML block parser. This is a hack to work around the terrible 750 | // hack that is the HTML block parser. 751 | result = result.replace(/\s+$/,""); 752 | result = "<"+list_type+">" + result + "\n"; 753 | return result; 754 | }); 755 | } else { 756 | whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; 757 | text = text.replace(whole_list,function(wholeMatch,m1,m2,m3) { 758 | var runup = m1; 759 | var list = m2; 760 | 761 | var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol"; 762 | // Turn double returns into triple returns, so that we can make a 763 | // paragraph for the last item in a list, if necessary: 764 | var list = list.replace(/\n{2,}/g,"\n\n\n");; 765 | var result = _ProcessListItems(list); 766 | result = runup + "<"+list_type+">\n" + result + "\n"; 767 | return result; 768 | }); 769 | } 770 | 771 | // attacklab: strip sentinel 772 | text = text.replace(/~0/,""); 773 | 774 | return text; 775 | } 776 | 777 | _ProcessListItems = function(list_str) { 778 | // 779 | // Process the contents of a single ordered or unordered list, splitting it 780 | // into individual list items. 781 | // 782 | // The $g_list_level global keeps track of when we're inside a list. 783 | // Each time we enter a list, we increment it; when we leave a list, 784 | // we decrement. If it's zero, we're not in a list anymore. 785 | // 786 | // We do this because when we're not inside a list, we want to treat 787 | // something like this: 788 | // 789 | // I recommend upgrading to version 790 | // 8. Oops, now this line is treated 791 | // as a sub-list. 792 | // 793 | // As a single paragraph, despite the fact that the second line starts 794 | // with a digit-period-space sequence. 795 | // 796 | // Whereas when we're inside a list (or sub-list), that line will be 797 | // treated as the start of a sub-list. What a kludge, huh? This is 798 | // an aspect of Markdown's syntax that's hard to parse perfectly 799 | // without resorting to mind-reading. Perhaps the solution is to 800 | // change the syntax rules such that sub-lists must start with a 801 | // starting cardinal number; e.g. "1." or "a.". 802 | 803 | g_list_level++; 804 | 805 | // trim trailing blank lines: 806 | list_str = list_str.replace(/\n{2,}$/,"\n"); 807 | 808 | // attacklab: add sentinel to emulate \z 809 | list_str += "~0"; 810 | 811 | /* 812 | list_str = list_str.replace(/ 813 | (\n)? // leading line = $1 814 | (^[ \t]*) // leading whitespace = $2 815 | ([*+-]|\d+[.]) [ \t]+ // list marker = $3 816 | ([^\r]+? // list item text = $4 817 | (\n{1,2})) 818 | (?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+)) 819 | /gm, function(){...}); 820 | */ 821 | list_str = list_str.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm, 822 | function(wholeMatch,m1,m2,m3,m4){ 823 | var item = m4; 824 | var leading_line = m1; 825 | var leading_space = m2; 826 | 827 | if (leading_line || (item.search(/\n{2,}/)>-1)) { 828 | item = _RunBlockGamut(_Outdent(item)); 829 | } 830 | else { 831 | // Recursion for sub-lists: 832 | item = _DoLists(_Outdent(item)); 833 | item = item.replace(/\n$/,""); // chomp(item) 834 | item = _RunSpanGamut(item); 835 | } 836 | 837 | return "
  • " + item + "
  • \n"; 838 | } 839 | ); 840 | 841 | // attacklab: strip sentinel 842 | list_str = list_str.replace(/~0/g,""); 843 | 844 | g_list_level--; 845 | return list_str; 846 | } 847 | 848 | 849 | var _DoCodeBlocks = function(text) { 850 | // 851 | // Process Markdown `
    ` blocks.
     852 | //  
     853 | 
     854 | 	/*
     855 | 		text = text.replace(text,
     856 | 			/(?:\n\n|^)
     857 | 			(								// $1 = the code block -- one or more lines, starting with a space/tab
     858 | 				(?:
     859 | 					(?:[ ]{4}|\t)			// Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
     860 | 					.*\n+
     861 | 				)+
     862 | 			)
     863 | 			(\n*[ ]{0,3}[^ \t\n]|(?=~0))	// attacklab: g_tab_width
     864 | 		/g,function(){...});
     865 | 	*/
     866 | 
     867 | 	// attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
     868 | 	text += "~0";
     869 | 	
     870 | 	text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
     871 | 		function(wholeMatch,m1,m2) {
     872 | 			var codeblock = m1;
     873 | 			var nextChar = m2;
     874 | 		
     875 | 			codeblock = _EncodeCode( _Outdent(codeblock));
     876 | 			codeblock = _Detab(codeblock);
     877 | 			codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
     878 | 			codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
     879 | 
     880 | 			codeblock = "
    " + codeblock + "\n
    "; 881 | 882 | return hashBlock(codeblock) + nextChar; 883 | } 884 | ); 885 | 886 | // attacklab: strip sentinel 887 | text = text.replace(/~0/,""); 888 | 889 | return text; 890 | } 891 | 892 | var hashBlock = function(text) { 893 | text = text.replace(/(^\n+|\n+$)/g,""); 894 | return "\n\n~K" + (g_html_blocks.push(text)-1) + "K\n\n"; 895 | } 896 | 897 | 898 | var _DoCodeSpans = function(text) { 899 | // 900 | // * Backtick quotes are used for spans. 901 | // 902 | // * You can use multiple backticks as the delimiters if you want to 903 | // include literal backticks in the code span. So, this input: 904 | // 905 | // Just type ``foo `bar` baz`` at the prompt. 906 | // 907 | // Will translate to: 908 | // 909 | //

    Just type foo `bar` baz at the prompt.

    910 | // 911 | // There's no arbitrary limit to the number of backticks you 912 | // can use as delimters. If you need three consecutive backticks 913 | // in your code, use four for delimiters, etc. 914 | // 915 | // * You can use spaces to get literal backticks at the edges: 916 | // 917 | // ... type `` `bar` `` ... 918 | // 919 | // Turns to: 920 | // 921 | // ... type `bar` ... 922 | // 923 | 924 | /* 925 | text = text.replace(/ 926 | (^|[^\\]) // Character before opening ` can't be a backslash 927 | (`+) // $2 = Opening run of ` 928 | ( // $3 = The code block 929 | [^\r]*? 930 | [^`] // attacklab: work around lack of lookbehind 931 | ) 932 | \2 // Matching closer 933 | (?!`) 934 | /gm, function(){...}); 935 | */ 936 | 937 | text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, 938 | function(wholeMatch,m1,m2,m3,m4) { 939 | var c = m3; 940 | c = c.replace(/^([ \t]*)/g,""); // leading whitespace 941 | c = c.replace(/[ \t]*$/g,""); // trailing whitespace 942 | c = _EncodeCode(c); 943 | return m1+""+c+""; 944 | }); 945 | 946 | return text; 947 | } 948 | 949 | 950 | var _EncodeCode = function(text) { 951 | // 952 | // Encode/escape certain characters inside Markdown code runs. 953 | // The point is that in code, these characters are literals, 954 | // and lose their special Markdown meanings. 955 | // 956 | // Encode all ampersands; HTML entities are not 957 | // entities within a Markdown code span. 958 | text = text.replace(/&/g,"&"); 959 | 960 | // Do the angle bracket song and dance: 961 | text = text.replace(//g,">"); 963 | 964 | // Now, escape characters that are magic in Markdown: 965 | text = escapeCharacters(text,"\*_{}[]\\",false); 966 | 967 | // jj the line above breaks this: 968 | //--- 969 | 970 | //* Item 971 | 972 | // 1. Subitem 973 | 974 | // special char: * 975 | //--- 976 | 977 | return text; 978 | } 979 | 980 | 981 | var _DoItalicsAndBold = function(text) { 982 | 983 | // must go first: 984 | text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, 985 | "$2"); 986 | 987 | text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, 988 | "$2"); 989 | 990 | return text; 991 | } 992 | 993 | 994 | var _DoBlockQuotes = function(text) { 995 | 996 | /* 997 | text = text.replace(/ 998 | ( // Wrap whole match in $1 999 | ( 1000 | ^[ \t]*>[ \t]? // '>' at the start of a line 1001 | .+\n // rest of the first line 1002 | (.+\n)* // subsequent consecutive lines 1003 | \n* // blanks 1004 | )+ 1005 | ) 1006 | /gm, function(){...}); 1007 | */ 1008 | 1009 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, 1010 | function(wholeMatch,m1) { 1011 | var bq = m1; 1012 | 1013 | // attacklab: hack around Konqueror 3.5.4 bug: 1014 | // "----------bug".replace(/^-/g,"") == "bug" 1015 | 1016 | bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0"); // trim one level of quoting 1017 | 1018 | // attacklab: clean up hack 1019 | bq = bq.replace(/~0/g,""); 1020 | 1021 | bq = bq.replace(/^[ \t]+$/gm,""); // trim whitespace-only lines 1022 | bq = _RunBlockGamut(bq); // recurse 1023 | 1024 | bq = bq.replace(/(^|\n)/g,"$1 "); 1025 | // These leading spaces screw with
     content, so we need to fix that:
    1026 | 			bq = bq.replace(
    1027 | 					/(\s*
    [^\r]+?<\/pre>)/gm,
    1028 | 				function(wholeMatch,m1) {
    1029 | 					var pre = m1;
    1030 | 					// attacklab: hack around Konqueror 3.5.4 bug:
    1031 | 					pre = pre.replace(/^  /mg,"~0");
    1032 | 					pre = pre.replace(/~0/g,"");
    1033 | 					return pre;
    1034 | 				});
    1035 | 			
    1036 | 			return hashBlock("
    \n" + bq + "\n
    "); 1037 | }); 1038 | return text; 1039 | } 1040 | 1041 | 1042 | var _FormParagraphs = function(text) { 1043 | // 1044 | // Params: 1045 | // $text - string to process with html

    tags 1046 | // 1047 | 1048 | // Strip leading and trailing lines: 1049 | text = text.replace(/^\n+/g,""); 1050 | text = text.replace(/\n+$/g,""); 1051 | 1052 | var grafs = text.split(/\n{2,}/g); 1053 | var grafsOut = new Array(); 1054 | 1055 | // 1056 | // Wrap

    tags. 1057 | // 1058 | var end = grafs.length; 1059 | for (var i=0; i= 0) { 1064 | grafsOut.push(str); 1065 | } 1066 | else if (str.search(/\S/) >= 0) { 1067 | str = _RunSpanGamut(str); 1068 | str = str.replace(/^([ \t]*)/g,"

    "); 1069 | str += "

    " 1070 | grafsOut.push(str); 1071 | } 1072 | 1073 | } 1074 | 1075 | // 1076 | // Unhashify HTML blocks 1077 | // 1078 | end = grafsOut.length; 1079 | for (var i=0; i= 0) { 1082 | var blockText = g_html_blocks[RegExp.$1]; 1083 | blockText = blockText.replace(/\$/g,"$$$$"); // Escape any dollar signs 1084 | grafsOut[i] = grafsOut[i].replace(/~K\d+K/,blockText); 1085 | } 1086 | } 1087 | 1088 | return grafsOut.join("\n\n"); 1089 | } 1090 | 1091 | 1092 | var _EncodeAmpsAndAngles = function(text) { 1093 | // Smart processing for ampersands and angle brackets that need to be encoded. 1094 | 1095 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1096 | // http://bumppo.net/projects/amputator/ 1097 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&"); 1098 | 1099 | // Encode naked <'s 1100 | text = text.replace(/<(?![a-z\/?\$!])/gi,"<"); 1101 | 1102 | return text; 1103 | } 1104 | 1105 | 1106 | var _EncodeBackslashEscapes = function(text) { 1107 | // 1108 | // Parameter: String. 1109 | // Returns: The string, with after processing the following backslash 1110 | // escape sequences. 1111 | // 1112 | 1113 | // attacklab: The polite way to do this is with the new 1114 | // escapeCharacters() function: 1115 | // 1116 | // text = escapeCharacters(text,"\\",true); 1117 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1118 | // 1119 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1120 | // as an optimization for Firefox. This function gets called a LOT. 1121 | 1122 | text = text.replace(/\\(\\)/g,escapeCharacters_callback); 1123 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g,escapeCharacters_callback); 1124 | return text; 1125 | } 1126 | 1127 | 1128 | var _DoAutoLinks = function(text) { 1129 | 1130 | text = text.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"
    $1"); 1131 | 1132 | // Email addresses: 1133 | 1134 | /* 1135 | text = text.replace(/ 1136 | < 1137 | (?:mailto:)? 1138 | ( 1139 | [-.\w]+ 1140 | \@ 1141 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1142 | ) 1143 | > 1144 | /gi, _DoAutoLinks_callback()); 1145 | */ 1146 | text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, 1147 | function(wholeMatch,m1) { 1148 | return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); 1149 | } 1150 | ); 1151 | 1152 | return text; 1153 | } 1154 | 1155 | 1156 | var _EncodeEmailAddress = function(addr) { 1157 | // 1158 | // Input: an email address, e.g. "foo@example.com" 1159 | // 1160 | // Output: the email address as a mailto link, with each character 1161 | // of the address encoded as either a decimal or hex entity, in 1162 | // the hopes of foiling most address harvesting spam bots. E.g.: 1163 | // 1164 | // foo 1166 | // @example.com 1167 | // 1168 | // Based on a filter by Matthew Wickline, posted to the BBEdit-Talk 1169 | // mailing list: 1170 | // 1171 | 1172 | // attacklab: why can't javascript speak hex? 1173 | function char2hex(ch) { 1174 | var hexDigits = '0123456789ABCDEF'; 1175 | var dec = ch.charCodeAt(0); 1176 | return(hexDigits.charAt(dec>>4) + hexDigits.charAt(dec&15)); 1177 | } 1178 | 1179 | var encode = [ 1180 | function(ch){return "&#"+ch.charCodeAt(0)+";";}, 1181 | function(ch){return "&#x"+char2hex(ch)+";";}, 1182 | function(ch){return ch;} 1183 | ]; 1184 | 1185 | addr = "mailto:" + addr; 1186 | 1187 | addr = addr.replace(/./g, function(ch) { 1188 | if (ch == "@") { 1189 | // this *must* be encoded. I insist. 1190 | ch = encode[Math.floor(Math.random()*2)](ch); 1191 | } else if (ch !=":") { 1192 | // leave ':' alone (to spot mailto: later) 1193 | var r = Math.random(); 1194 | // roughly 10% raw, 45% hex, 45% dec 1195 | ch = ( 1196 | r > .9 ? encode[2](ch) : 1197 | r > .45 ? encode[1](ch) : 1198 | encode[0](ch) 1199 | ); 1200 | } 1201 | return ch; 1202 | }); 1203 | 1204 | addr = "" + addr + ""; 1205 | addr = addr.replace(/">.+:/g,"\">"); // strip the mailto: from the visible part 1206 | 1207 | return addr; 1208 | } 1209 | 1210 | 1211 | var _UnescapeSpecialChars = function(text) { 1212 | // 1213 | // Swap back in all the special characters we've hidden. 1214 | // 1215 | text = text.replace(/~E(\d+)E/g, 1216 | function(wholeMatch,m1) { 1217 | var charCodeToReplace = parseInt(m1); 1218 | return String.fromCharCode(charCodeToReplace); 1219 | } 1220 | ); 1221 | return text; 1222 | } 1223 | 1224 | 1225 | var _Outdent = function(text) { 1226 | // 1227 | // Remove one level of line-leading tabs or spaces 1228 | // 1229 | 1230 | // attacklab: hack around Konqueror 3.5.4 bug: 1231 | // "----------bug".replace(/^-/g,"") == "bug" 1232 | 1233 | text = text.replace(/^(\t|[ ]{1,4})/gm,"~0"); // attacklab: g_tab_width 1234 | 1235 | // attacklab: clean up hack 1236 | text = text.replace(/~0/g,"") 1237 | 1238 | return text; 1239 | } 1240 | 1241 | var _Detab = function(text) { 1242 | // attacklab: Detab's completely rewritten for speed. 1243 | // In perl we could fix it by anchoring the regexp with \G. 1244 | // In javascript we're less fortunate. 1245 | 1246 | // expand first n-1 tabs 1247 | text = text.replace(/\t(?=\t)/g," "); // attacklab: g_tab_width 1248 | 1249 | // replace the nth with two sentinels 1250 | text = text.replace(/\t/g,"~A~B"); 1251 | 1252 | // use the sentinel to anchor our regex so it doesn't explode 1253 | text = text.replace(/~B(.+?)~A/g, 1254 | function(wholeMatch,m1,m2) { 1255 | var leadingText = m1; 1256 | var numSpaces = 4 - leadingText.length % 4; // attacklab: g_tab_width 1257 | 1258 | // there *must* be a better way to do this: 1259 | for (var i=0; i