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