1 /* 2 * Kajabity Wiki Text Plugin for jQuery http://www.kajabity.com/jquery-wikitext/ 3 * 4 * Copyright (c) 2011 Williams Technologies Limited 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 * use this file except in compliance with the License. You may obtain a copy of 8 * the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 * License for the specific language governing permissions and limitations under 16 * the License. 17 * 18 * Kajabity is a trademark of Williams Technologies Limited. 19 * http://www.williams-technologies.co.uk 20 */ 21 /** 22 * @fileOverview Kajabity Wiki Text Plugin for jQuery 23 * @author Simon J. Williams 24 * @version: 0.3 25 */ 26 (function( jQuery ) 27 { 28 /** 29 * jQuery definition to anchor JsDoc comments. 30 * 31 * @see http://jquery.com/ 32 * @name jQuery 33 * @class jQuery Library 34 */ 35 36 /** 37 * jQuery Utility Function to convert Wiki formatted text to HTML. 38 * 39 * @namespace Kajabity Wiki Text 40 * @function 41 * @param {string} text the Wiki text to be converted to HTML. 42 * @return {string} HTML formatted text. 43 * @memberOf jQuery 44 */ 45 jQuery.wikiText = function( text ) 46 { 47 // The source text with nulls/undefined taken care of. 48 var source = (text || '').toString(); 49 // The resultant HTML string - initially empty. 50 var html = ''; 51 52 // A regular expression to read the source one line at a time. 53 var regex = /([^\r\n]*)(\r\n?|\n)/g; 54 var lineMatches; 55 var offset = 0; 56 var line; 57 58 // Regular expressions to match each kind of line level format mark-up. 59 var re_blank = /^([ \t]*)$/; 60 var re_heading = /^(={1,6})[ \t]+([^=]+)(={1,6})[ \t]*$/; 61 var re_bullet = /^[ \t]+\*[ \t]+(.+)$/; 62 var re_numbered = /^[ \t]+#[ \t]+(.+)$/; 63 var re_mono_start = /^\{{3}$/; 64 var re_mono_end = /^\}{3}$/; 65 var re_blockquote = /^[ \t]+(.+)$/; 66 var re_hr = /^-{4,}$/; 67 var matches; 68 69 // Flags indicating which kind of block we are currently in. 70 var paragraph = false; 71 var olist = false; 72 var ulist = false; 73 var bq = false; 74 var mono = false; 75 76 // Regular expression to find mark-up tokens in each line. 77 var regexToken = /(((ftp|https?):\/\/)[\-\w@:%_\+.~#?,&\/\/=]+)|((mailto:)?[_.\w-]+@([\w][\w\-]+\.)+[a-zA-Z]{2,3})|(!+\[)|(!*((_{2})|(\{{3})|(\}{3})|(~{2})|(\^)|(,{2})|('{2,5}))|(\[{1,2}[^\]]+\]{1,2}))/g; 78 79 // Individual mark-up regular expressions - also see the public ones 80 // near the bottom of the file. 81 var re_named_link = /^\[((((ftp|https?):\/\/)?[\-\w@:%_\+.~#?,&\/\/=]+)|((mailto:)?([_.\w\-]+@([\w][\w\-]+\.)+[a-zA-Z]{2,3})))([ \t]*([^\]]*))?\]$/; 82 var tn_monospace = "{{{"; 83 var tn_monospace_end = "}}}"; 84 var tn_bolditalic = "'''''"; 85 var tn_bold = "'''"; 86 var tn_italic = "''"; 87 var tn_superscript = "^"; 88 var tn_subscript = ",,"; 89 var tn_underline = "__"; 90 var tn_strikethrough = "~~"; 91 var tn_break = "[[BR]]"; 92 var monospace = false; 93 94 // Keep track of inline format nesting. 95 var tagStack = []; 96 var poppedStack = []; 97 98 // Inline formatting start tags. 99 var beginings = 100 { 101 bold : "<strong>", 102 italic : "<em>", 103 monospace : "<tt>", 104 underline : "<u>", 105 strikethrough : "<strike>", 106 superscript : "<sup>", 107 subscript : "<sub>" 108 }; 109 110 // ...and end tags. 111 var endings = 112 { 113 bold : "</strong>", 114 italic : "</em>", 115 monospace : "</tt>", 116 underline : "</u>", 117 strikethrough : "</strike>", 118 superscript : "</sup>", 119 subscript : "</sub>" 120 }; 121 122 /** 123 * Remove inline formatting at end of a block. Puts it on the 124 * poppedStack to add at the start of the next block. 125 * 126 * @return {string} end tags for any current inline formatting. 127 */ 128 var endFormatting = function() 129 { 130 var tags = ''; 131 var popped; 132 while( tagStack.length > 0 ) 133 { 134 popped = tagStack.pop(); 135 tags += endings[popped]; 136 poppedStack.push( popped ); 137 } 138 return tags; 139 }; 140 141 /** 142 * End the current block and, temporarily, any nested inline formatting, 143 * if any. 144 * 145 * @return {string} the block (and inline formatting) HTML end tags. 146 */ 147 var endBlock = function() 148 { 149 html += endFormatting(); 150 151 if( paragraph ) 152 { 153 html += "</p>\n"; 154 paragraph = false; 155 } 156 if( olist ) 157 { 158 html += "</li>\n</ol>\n"; 159 olist = false; 160 } 161 if( ulist ) 162 { 163 html += "</li>\n</ul>\n"; 164 ulist = false; 165 } 166 if( bq ) 167 { 168 html += "</blockquote>\n"; 169 bq = false; 170 } 171 }; 172 173 /** 174 * Re-add nested formatting removed at the end of the previous block. 175 * 176 * @return {string} HTML start tags for all continued formatting. 177 */ 178 var restartFormatting = function() 179 { 180 var tags = ''; 181 while( poppedStack.length > 0 ) 182 { 183 var popped = poppedStack.pop(); 184 tags += beginings[popped]; 185 tagStack.push( popped ); 186 } 187 return tags; 188 }; 189 190 /** 191 * As most inline format tags are the same at the start or end, this 192 * toggles the formatting on or off depending if it is currently in the 193 * tagStack. 194 * 195 * @param {string} label the name of the format to toggle. 196 * @return {string} any HTML start or end tags to toggle the formatting 197 * with proper nesting. 198 */ 199 var toggleFormatting = function( label ) 200 { 201 var tags = ''; 202 if( jQuery.inArray( label, tagStack ) > -1 ) 203 { 204 var popped; 205 do 206 { 207 popped = tagStack.pop(); 208 tags += endings[popped]; 209 if( popped === label ) 210 { 211 break; 212 } 213 poppedStack.push( popped ); 214 } while( popped !== label ); 215 216 tags += restartFormatting(); 217 } 218 else 219 { 220 tagStack.push( label ); 221 tags = beginings[label]; 222 } 223 224 return tags; 225 }; 226 227 /** 228 * Apply inline formatting to text in a line - and escape any HTML 229 * mark-up tags. 230 * 231 * @param {string} text the plain text to be formatted and escaped. 232 * @return {string} HTML formatted text. 233 */ 234 var formatText = function( text ) 235 { 236 var sourceToken = (text || '').toString(); 237 var formattedText = ''; 238 var token; 239 var offset = 0; 240 var tokenArray; 241 var linkText; 242 var nl_tokenArray; 243 244 // Iterate through any mark-up tokens in the line. 245 while( (tokenArray = regexToken.exec( sourceToken )) !== null ) 246 { 247 token = tokenArray[0]; 248 if( offset < tokenArray.index ) 249 { 250 // Add non-mark-up text. 251 formattedText += jQuery.wikiText.safeText( sourceToken 252 .substring( offset, tokenArray.index ) ); 253 } 254 255 if( monospace ) 256 { 257 // Ignore mark-up until end of monospace. 258 if( tn_monospace_end === token ) 259 { 260 monospace = false; 261 formattedText += toggleFormatting( "monospace" ); 262 } 263 else 264 { 265 formattedText += jQuery.wikiText.safeText( token ); 266 } 267 } 268 else if( tn_monospace === token ) 269 { 270 monospace = true; 271 formattedText += toggleFormatting( "monospace" ); 272 } 273 else if( jQuery.wikiText.re_link.test( token ) ) 274 { 275 formattedText += jQuery.wikiText.namedLink( token ); 276 } 277 else if( jQuery.wikiText.re_mail.test( token ) ) 278 { 279 formattedText += jQuery.wikiText.namedLink( token ); 280 } 281 else if( tn_bold === token ) 282 { 283 formattedText += toggleFormatting( "bold" ); 284 } 285 else if( tn_italic === token ) 286 { 287 formattedText += toggleFormatting( "italic" ); 288 } 289 else if( tn_bolditalic === token ) 290 { 291 // Avoid empty tag if nesting is wrong way around. 292 if( jQuery.inArray( "bold", tagStack ) > jQuery.inArray( 293 "italic", tagStack ) ) 294 { 295 formattedText += toggleFormatting( "bold" ); 296 formattedText += toggleFormatting( "italic" ); 297 } 298 else 299 { 300 formattedText += toggleFormatting( "italic" ); 301 formattedText += toggleFormatting( "bold" ); 302 } 303 } 304 else if( tn_superscript === token ) 305 { 306 formattedText += toggleFormatting( "superscript" ); 307 } 308 else if( tn_subscript === token ) 309 { 310 formattedText += toggleFormatting( "subscript" ); 311 } 312 else if( tn_underline === token ) 313 { 314 formattedText += toggleFormatting( "underline" ); 315 } 316 else if( tn_strikethrough === token ) 317 { 318 formattedText += toggleFormatting( "strikethrough" ); 319 } 320 else if( tn_break === token ) 321 { 322 formattedText += "<br/>"; 323 } 324 else if( (nl_tokenArray = re_named_link.exec( token )) !== null ) 325 { 326 formattedText += jQuery.wikiText.namedLink( 327 nl_tokenArray[1], nl_tokenArray[10] ); 328 } 329 else if( token[0] === "!" ) 330 { 331 formattedText += jQuery.wikiText.safeText( token 332 .substring( 1 ) ); 333 } 334 else 335 { 336 formattedText += jQuery.wikiText.safeText( token ); 337 } 338 339 offset = regexToken.lastIndex; 340 } 341 342 if( offset < sourceToken.length ) 343 { 344 // Add trailing non-mark-up text. 345 formattedText += jQuery.wikiText.safeText( sourceToken 346 .substring( offset ) ); 347 } 348 349 return formattedText; 350 }; 351 352 /** 353 * Get a single line from the input. This resolves the issue where the 354 * last line is not returned because it doesn't end with CR/LF. 355 * 356 * @return {string} a single line of input - or null at end of string. 357 */ 358 var getLine = function() 359 { 360 if( offset < source.length ) 361 { 362 lineMatches = regex.exec( source ); 363 if( lineMatches != null ) 364 { 365 offset = regex.lastIndex; 366 line = lineMatches[1]; 367 } 368 else 369 { 370 line = source.substring( offset ); 371 offset = source.length; 372 } 373 } 374 else 375 { 376 line = null; 377 } 378 379 return line; 380 }; 381 382 // -------------------------------------------------------------------- 383 384 while( getLine() != null ) 385 { 386 if( mono ) 387 { 388 if( line.match( re_mono_end ) ) 389 { 390 mono = false; 391 html += "</pre>\n"; 392 } 393 else 394 { 395 html += jQuery.wikiText.safeText( line ) + "\n"; 396 } 397 } 398 else if( line.length === 0 || re_blank.test( line ) ) 399 { 400 endBlock(); 401 } 402 else if( (matches = line.match( re_heading )) !== null ) 403 { 404 endBlock(); 405 var headingLevel = matches[1].length; 406 html += "\n<h" + headingLevel + ">" + restartFormatting() 407 + formatText( matches[2] ) + endFormatting() + "</h" 408 + headingLevel + ">\n\n"; 409 } 410 else if( (matches = line.match( re_bullet )) !== null ) 411 { 412 if( ulist ) 413 { 414 html += endFormatting() + "</li>\n"; 415 } 416 else 417 { 418 endBlock(); 419 html += "<ul>\n"; 420 ulist = true; 421 } 422 423 html += "<li>" + restartFormatting() + formatText( matches[1] ); 424 } 425 else if( (matches = line.match( re_numbered )) !== null ) 426 { 427 if( olist ) 428 { 429 html += endFormatting() + "</li>\n"; 430 } 431 else 432 { 433 endBlock(); 434 html += "<ol>\n"; 435 olist = true; 436 } 437 438 html += "<li>" + restartFormatting() + formatText( matches[1] ); 439 } 440 else if( line.match( re_mono_start ) ) 441 { 442 endBlock(); 443 html += "<pre>\n"; 444 mono = true; 445 } 446 else if( line.match( re_hr ) ) 447 { 448 endBlock(); 449 html += "<hr/>\n"; 450 } 451 else if( (matches = line.match( re_blockquote )) ) 452 { 453 // If not already in blockquote - or a list... 454 if( !(bq || olist || ulist) ) 455 { 456 endBlock(); 457 html += "<blockquote>\n"; 458 html += restartFormatting(); 459 bq = true; 460 } 461 462 html += "\n" + formatText( matches[1] ); 463 } 464 else 465 { 466 if( !paragraph ) 467 { 468 endBlock(); 469 html += "<p>\n"; 470 html += restartFormatting(); 471 paragraph = true; 472 } 473 474 html += formatText( line ) + "\n"; 475 } 476 } 477 478 endBlock(); 479 480 return html; 481 }; 482 483 /** 484 * Escape HTML special characters. 485 * 486 * @param {string} text which may contain HTML mark-up characters. 487 * @return {string} text with HTML mark-up characters escaped. 488 * @memberOf jQuery.wikiText 489 */ 490 jQuery.wikiText.safeText = function( text ) 491 { 492 return (text || '').replace( /&/g, "&" ).replace( /</g, "<" ) 493 .replace( />/g, ">" ); 494 }; 495 496 /** 497 * A regular expression which detects HTTP(S) and FTP URLs. 498 * @type RegExp 499 */ 500 jQuery.wikiText.re_link = /^((ftp|https?):\/\/)[\-\w@:%_\+.~#?,&\/\/=]+$/; 501 502 /** 503 * A regular expression to match an email address with or without "mailto:" 504 * in front. 505 * @type RegExp 506 */ 507 jQuery.wikiText.re_mail = /^(mailto:)?([_.\w\-]+@([\w][\w\-]+\.)+[a-zA-Z]{2,3})$/; 508 509 /** 510 * Create a HTML link from a URL and Display Text - default the display to 511 * the URL (tidied up). 512 * <p> 513 * If the URL is missing, the text is returned, if the Name is missing the 514 * URL is tidied up (remove 'mailto:' and un-escape characters) and used as 515 * the name. 516 * </p> 517 * <p> 518 * The name is then escaped using safeText. 519 * </p> 520 * 521 * @param {string} url the URL which may be a full HTTP(S), FTP or Email URL 522 * or a relative URL. 523 * @param {string} name 524 * @return {string} text containing a HTML link tag. 525 * @memberOf jQuery.wikiText 526 */ 527 jQuery.wikiText.namedLink = function( url, name ) 528 { 529 var linkUrl; 530 var linkText; 531 532 if( !url ) { return jQuery.wikiText.safeText( name ); } 533 534 if( jQuery.wikiText.re_mail.test( url ) ) 535 { 536 url = url.replace( /mailto:/, "" ); 537 linkUrl = encodeURI( "mailto:" + url ); 538 } 539 else 540 { 541 linkUrl = url; 542 } 543 544 if( !name ) 545 { 546 name = decodeURI( url ); 547 } 548 549 linkText = jQuery.wikiText.safeText( name ); 550 return linkText.link( linkUrl ); 551 }; 552 553 /** 554 * jQuery 'fn' definition to anchor JsDoc comments. 555 * 556 * 557 * @see http://jquery.com/ 558 * @name fn 559 * @class jQuery Library 560 * @memberOf jQuery 561 */ 562 563 /** 564 * A jQuery Wrapper Function to append Wiki formatted text to a DOM object 565 * converted to HTML. 566 * 567 * @class Wiki Text Wrapper 568 * @param {string} text text with Wiki mark-up. 569 * @return {jQuery} chainable jQuery class 570 * @memberOf jQuery.fn 571 */ 572 jQuery.fn.wikiText = function( text ) 573 { 574 return this.html( jQuery.wikiText( text ) ); 575 }; 576 })( jQuery ); 577