1 /**
  2  * SCEditor BBCode Plugin
  3  * http://www.samclarke.com/2011/07/sceditor/ 
  4  *
  5  * Copyright (C) 2011-2012, Sam Clarke (samclarke.com)
  6  *
  7  * SCEditor is dual licensed under the MIT and GPL licenses:
  8  *	http://www.opensource.org/licenses/mit-license.php
  9  *	http://www.gnu.org/licenses/gpl.html
 10  * 
 11  * @author Sam Clarke
 12  * @version 1.3.5
 13  * @requires jQuery 
 14  */
 15 
 16 // ==ClosureCompiler==
 17 // @output_file_name jquery.sceditor.min.js
 18 // @compilation_level SIMPLE_OPTIMIZATIONS
 19 // ==/ClosureCompiler==
 20 
 21 /*jshint smarttabs: true, jquery: true, eqnull:true, curly: false */
 22 
 23 (function($) {
 24 	'use strict';
 25 	
 26 	/**
 27 	 * BBCode plugin for SCEditor
 28 	 *
 29 	 * @param {Element} el The textarea to be converted
 30 	 * @return {Object} options
 31 	 * @class sceditorBBCodePlugin
 32 	 * @name jQuery.sceditorBBCodePlugin
 33 	 */
 34 	$.sceditorBBCodePlugin = function(element, options) {
 35 		var base = this;
 36 
 37 		/**
 38 		 * Private methods
 39 		 * @private
 40 		 */
 41 		var	init,
 42 			buildBbcodeCache,
 43 			handleStyles,
 44 			handleTags,
 45 			formatString,
 46 			getStyle,
 47 			wrapInDivs,
 48 			mergeTextModeCommands;
 49 
 50 		base.bbcodes = $.sceditorBBCodePlugin.bbcodes;
 51 
 52 
 53 		/**
 54 		 * cache of all the tags pointing to their bbcodes to enable
 55 		 * faster lookup of which bbcode a tag should have
 56 		 * @private
 57 		 */
 58 		var tagsToBbcodes = {};
 59 
 60 		/**
 61 		 * Same as tagsToBbcodes but instead of HTML tags it's styles
 62 		 * @private
 63 		 */
 64 		var stylesToBbcodes = {};
 65 		
 66 		/**
 67 		 * Allowed children of specific HTML tags. Empty array if no
 68 		 * children other than text nodes are allowed
 69 		 * @private
 70 		 */
 71 		var validChildren = {
 72 			ul: ['li'],
 73 			ol: ['li'],
 74 			table: ['tr'],
 75 			tr: ['td', 'th'],
 76 			code: ['br', 'p', 'div'],
 77 			youtube: []
 78 		};
 79 
 80 
 81 		/**
 82 		 * Initializer
 83 		 * @private
 84 		 * @name sceditorBBCodePlugin.init
 85 		 */
 86 		init = function() {
 87 			$.data(element, "sceditorbbcode", base);
 88 			
 89 			base.options = $.extend({}, $.sceditor.defaultOptions, options);
 90 
 91 			// build the BBCode cache
 92 			buildBbcodeCache();
 93 
 94 			(new $.sceditor(element,
 95 				$.extend({}, base.options, {
 96 					getHtmlHandler: base.getHtmlHandler,
 97 					getTextHandler: base.getTextHandler,
 98 					commands: mergeTextModeCommands()
 99 				})
100 			));
101 		};
102 		
103 		mergeTextModeCommands = function() {
104 			var merge = {
105 				bold: { txtExec: ["[b]", "[/b]"] },
106 				italic: { txtExec: ["[i]", "[/i]"] },
107 				underline: { txtExec: ["[u]", "[/u]"] },
108 				strike: { txtExec: ["[s]", "[/s]"] },
109 				subscript: { txtExec: ["[sub]", "[/sub]"] },
110 				superscript: { txtExec: ["[sup]", "[/sup]"] },
111 				left: { txtExec: ["[left]", "[/left]"] },
112 				center: { txtExec: ["[center]", "[/center]"] },
113 				right: { txtExec: ["[right]", "[/right]"] },
114 				justify: { txtExec: ["[justify]", "[/justify]"] },
115 				font: { txtExec: function(caller) {
116 					var editor = this;
117 					
118 					$.sceditor.command.get('font')._createDropDown(
119 						editor,
120 						caller,
121 						function(fontName) {
122 							editor.insertText("[font="+fontName+"]", "[/font]");
123 						}
124 					);
125 				} },
126 				size: { txtExec: function(caller) {
127 					var editor = this;
128 					
129 					$.sceditor.command.get('size')._createDropDown(
130 						editor,
131 						caller,
132 						function(fontSize) {
133 							editor.insertText("[size="+fontSize+"]", "[/size]");
134 						}
135 					);
136 				} },
137 				color: { txtExec: function(caller) {
138 					var editor = this;
139 					
140 					$.sceditor.command.get('color')._createDropDown(
141 						editor,
142 						caller,
143 						function(color) {
144 							editor.insertText("[color="+color+"]", "[/color]");
145 						}
146 					);
147 				} },
148 				bulletlist: { txtExec: ["[ul][li]", "[/li][/ul]"] },
149 				orderedlist: { txtExec: ["[ol][li]", "[/li][/ol]"] },
150 				table: { txtExec: ["[table][tr][td]", "[/td][/tr][/table]"] },
151 				horizontalrule: { txtExec: ["[hr]"] },
152 				code: { txtExec: ["[code]", "[/code]"] },
153 				image: { txtExec: function(caller, selected) {
154 					var url = prompt(this._("Enter the image URL:"), selected);
155 					
156 					if(url)
157 						this.insertText("[img]" + url + "[/img]");
158 				} },
159 				email: { txtExec: function(caller, selected) {
160 					var	email	= prompt(this._("Enter the e-mail address:"), selected || "@"),
161 						text	= prompt(this._("Enter the displayed text:"), email) || email;
162 					
163 					if(email)
164 						this.insertText("[email=" + email + "]" + text + "[/email]");
165 				} },
166 				link: { txtExec: function(caller, selected) {
167 					var	url	= prompt(this._("Enter URL:"), selected || "http://"),
168 						text	= prompt(this._("Enter the displayed text:"), url) || url;
169 					
170 					if(url)
171 						this.insertText("[url=" + url + "]" + text + "[/url]");
172 				} },
173 				quote: { txtExec: ["[quote]", "[/quote]"] },
174 				youtube: { txtExec: function(caller, selected) {
175 					var url = prompt(this._("Enter the YouTube video URL or ID:"), selected);
176 					
177 					if(url)
178 					{
179 						if(url.indexOf("://") > -1)
180 							url = url.replace(/^[^v]+v.(.{11}).*/,"$1");
181 						
182 						this.insertText("[youtube]" + url + "[/youtube]");
183 					}
184 				} },
185 				rtl: { txtExec: ["[rtl]", "[/rtl]"] },
186 				ltr: { txtExec: ["[ltr]", "[/ltr]"] }
187 			};
188 
189 			return $.extend(true, {}, merge, $.sceditor.commands);
190 		};
191 		
192 		/**
193 		 * Populates tagsToBbcodes and stylesToBbcodes to enable faster lookups
194 		 * 
195 		 * @private
196 		 */
197 		buildBbcodeCache = function() {
198 			$.each(base.bbcodes, function(bbcode, info) {
199 				if(typeof base.bbcodes[bbcode].tags !== "undefined")
200 					$.each(base.bbcodes[bbcode].tags, function(tag, values) {
201 						var isBlock = !!base.bbcodes[bbcode].isBlock;
202 						tagsToBbcodes[tag] = (tagsToBbcodes[tag] || {});
203 						tagsToBbcodes[tag][isBlock] = (tagsToBbcodes[tag][isBlock] || {});
204 						tagsToBbcodes[tag][isBlock][bbcode] = values;
205 					});
206 
207 				if(typeof base.bbcodes[bbcode].styles !== "undefined")
208 					$.each(base.bbcodes[bbcode].styles, function(style, values) {
209 						var isBlock = !!base.bbcodes[bbcode].isBlock;
210 						stylesToBbcodes[isBlock] = (stylesToBbcodes[isBlock] || {});
211 						stylesToBbcodes[isBlock][style] = (stylesToBbcodes[isBlock][style] || {});
212 						stylesToBbcodes[isBlock][style][bbcode] = values;
213 					});
214 			});
215 		};
216 		
217 		getStyle = function(element, property) {
218 			var	name = $.camelCase(property),
219 				$elm, ret, dir;
220 
221 			// add exception for align
222 			if("text-align" === property)
223 			{
224 				$elm = $(element);
225 				
226 				if($elm.parent().css(property) !== $elm.css(property) &&
227 					$elm.css('display') === "block" && !$elm.is('hr') && !$elm.is('th'))
228 					ret = $elm.css(property);
229 				
230 				// IE changes text-align to the same as direction so skip unless overried by user
231 				dir = element.style['direction'];
232 				if(dir && ((/right/i.test(ret) && dir === 'rtl') || (/left/i.test(ret) && dir === 'ltr')))
233 					return null;
234 				
235 				return ret;
236 			}
237 			
238 			if(element.style)
239 				return element.style[name];
240 			
241 			return null;
242 		};
243 
244 		/**
245 		 * Checks if any bbcode styles match the elements styles
246 		 * 
247 		 * @private
248 		 * @return string Content with any matching bbcode tags wrapped around it.
249 		 * @Private
250 		 */
251 		handleStyles = function(element, content, blockLevel) {
252 			var	elementPropVal,
253 				tag = element[0].nodeName.toLowerCase();
254 
255 			// convert blockLevel to boolean
256 			blockLevel = !!blockLevel;
257 			
258 			if(!stylesToBbcodes[blockLevel])
259 				return content;
260 			
261 			$.each(stylesToBbcodes[blockLevel], function(property, bbcodes) {
262 				elementPropVal = getStyle(element[0], property);
263 				if(elementPropVal == null || elementPropVal === "")
264 					return;
265 
266 				// if the parent has the same style use that instead of this one
267 				// so you dont end up with [i]parent[i]child[/i][/i]
268 				if(getStyle(element.parent()[0], property) === elementPropVal)
269 					return;
270 
271 				$.each(bbcodes, function(bbcode, values) {
272 					if((element[0].childNodes.length === 0 || element[0].childNodes[0].nodeName.toLowerCase() === "br") &&
273 						!base.bbcodes[bbcode].allowsEmpty)
274 						return;
275 					
276 					if(values === null || $.inArray(elementPropVal.toString(), values) > -1) {
277 						if($.isFunction(base.bbcodes[bbcode].format))
278 							content = base.bbcodes[bbcode].format.call(base, element, content);
279 						else
280 							content = formatString(base.bbcodes[bbcode].format, content);
281 					}
282 				});
283 			});
284 
285 			return content;
286 		};
287 
288 		/**
289 		 * Handles a HTML tag and finds any matching bbcodes
290 		 * 
291 		 * @private
292 		 * @param	jQuery element	element		The element to convert
293 		 * @param	string			content		The Tags text content
294 		 * @param	bool			blockLevel	If to convert block level tags
295 		 * @return	string	Content with any matching bbcode tags wrapped around it.
296 		 * @Private
297 		 */
298 		handleTags = function(element, content, blockLevel) {
299 			var tag = element[0].nodeName.toLowerCase();
300 			
301 			// convert blockLevel to boolean
302 			blockLevel = !!blockLevel;
303 
304 			if(tagsToBbcodes[tag] && tagsToBbcodes[tag][blockLevel]) {
305 				// loop all bbcodes for this tag
306 				$.each(tagsToBbcodes[tag][blockLevel], function(bbcode, bbcodeAttribs) {
307 					if(!base.bbcodes[bbcode].allowsEmpty &&
308 						(element[0].childNodes.length === 0 || (element[0].childNodes[0].nodeName.toLowerCase() === "br" && element[0].childNodes.length === 1))						)
309 						return;
310 					
311 					// if the bbcode requires any attributes then check this has
312 					// all needed
313 					if(bbcodeAttribs !== null) {
314 						var runBbcode = false;
315 
316 						// loop all the bbcode attribs
317 						$.each(bbcodeAttribs, function(attrib, values)
318 						{
319 							// check if has the bbcodes attrib
320 							if(element.attr(attrib) == null)
321 								return;
322 
323 							// if the element has the bbcodes attribute and the bbcode attribute
324 							// has values check one of the values matches
325 							if(values !== null && $.inArray(element.attr(attrib), values) < 0)
326 								return;
327 
328 							// break this loop as we have matched this bbcode
329 							runBbcode = true;
330 							return false;
331 						});
332 
333 						if(!runBbcode)
334 							return;
335 					}
336 
337 					if($.isFunction(base.bbcodes[bbcode].format))
338 						content = base.bbcodes[bbcode].format.call(base, element, content);
339 					else
340 						content = formatString(base.bbcodes[bbcode].format, content);
341 				});
342 			}
343 			
344 			// add newline after paragraph elements p and div (WebKit uses divs) and br tags
345 			if(blockLevel && /^(br|div|p)$/.test(tag))
346 			{
347 				var parentChildren = element[0].parentNode.childNodes;
348 
349 				// if it's a <p><br /></p> the paragraph will put the newline so skip the br
350 				if(!("br" === tag && parentChildren.length === 1) &&
351 					!("br" === tag && parentChildren[parentChildren.length-1] === element[0])) {
352 					content += "\n";
353 				}
354 
355 				// needed for browsers that enter textnode then when return is pressed put the rest in a div, i.e.:
356 				// text<div>line 2</div>
357 				if("br" !== tag && !$.sceditor.dom.isInline(element.parent()[0]) && element[0].previousSibling &&
358 					element[0].previousSibling.nodeType === 3) {
359 					content = "\n" + content;
360 				}
361 			}
362 
363 			return content;
364 		};
365 
366 		/**
367 		 * Formats a string in the format
368 		 * {0}, {1}, {2}, ect. with the params provided
369 		 * @private
370 		 * @return string
371 		 * @Private
372 		 */
373 		formatString = function() {
374 			var args = arguments;
375 			return args[0].replace(/\{(\d+)\}/g, function(str, p1) {
376 				return typeof args[p1-0+1] !== "undefined"? 
377 						args[p1-0+1] :
378 						'{' + p1 + '}';
379 			});
380 		};
381 
382 		/**
383 		 * Removes any leading or trailing quotes ('")
384 		 *
385 		 * @return string
386 		 * @memberOf jQuery.sceditorBBCodePlugin.prototype
387 		 */
388 		base.stripQuotes = function(str) {
389 			return str.replace(/^["']+/, "").replace(/["']+$/, "");
390 		};
391 
392 		/**
393 		 * Converts HTML to BBCode
394 		 * @param string	html	Html string, this function ignores this, it works off domBody
395 		 * @param HtmlElement	domBody	Editors dom body object to convert
396 		 * @return string BBCode which has been converted from HTML 
397 		 * @memberOf jQuery.sceditorBBCodePlugin.prototype
398 		 */
399 		base.getHtmlHandler = function(html, domBody) {
400 			$.sceditor.dom.removeWhiteSpace(domBody[0]);
401 
402 			return $.trim(base.elementToBbcode(domBody));
403 		};
404 
405 		/**
406 		 * Converts a HTML dom element to BBCode starting from
407 		 * the innermost element and working backwards
408 		 * 
409 		 * @private
410 		 * @param HtmlElement	element		The element to convert to BBCode
411 		 * @param array			vChildren	Valid child tags allowed
412 		 * @return string BBCode
413 		 * @memberOf jQuery.sceditorBBCodePlugin.prototype
414 		 */
415 		base.elementToBbcode = function($element) {
416 			return (function toBBCode(node, vChildren) {
417 				var ret = '';
418 
419 				$.sceditor.dom.traverse(node, function(node) {
420 					var	$node		= $(node),
421 						curTag		= '',
422 						tag		= node.nodeName.toLowerCase(),
423 						vChild		= validChildren[tag],
424 						isValidChild	= true;
425 					
426 					if(typeof vChildren === 'object')
427 					{
428 						isValidChild = $.inArray(tag, vChildren) > -1;
429 
430 						// if this tag is one of the parents allowed children
431 						// then set this tags allowed children to whatever it allows,
432 						// otherwise set to what the parent allows
433 						if(!isValidChild)
434 							vChild = vChildren;
435 					}
436 					
437 					// 3 is text element
438 					if(node.nodeType !== 3)
439 					{
440 						// skip ignored elments
441 						if($node.hasClass("sceditor-ignore"))
442 							return;
443 
444 						// don't loop inside iframes
445 						if(tag !== 'iframe')
446 							curTag = toBBCode(node, vChild);
447 						
448 						if(isValidChild)
449 						{
450 							// code tags should skip most styles
451 							if(!$node.is('code'))
452 							{
453 								// handle inline bbcodes
454 								curTag = handleStyles($node, curTag);
455 								curTag = handleTags($node, curTag);
456 								
457 								// handle blocklevel bbcodes
458 								curTag = handleStyles($node, curTag, true);
459 							}
460 							
461 							ret += handleTags($node, curTag, true);
462 						}
463 						else
464 							ret += curTag;
465 					}
466 					else if(node.wholeText && (!node.previousSibling || node.previousSibling.nodeType !== 3))
467 					{
468 						if($(node).parents('code').length === 0)
469 							ret += node.wholeText.replace(/ +/g, " ");
470 						else
471 							ret += node.wholeText;
472 					}
473 					else if(!node.wholeText)
474 						ret += node.nodeValue;
475 				}, false, true);
476 				
477 				return ret;
478 			}($element.get(0)));
479 		};
480 
481 		/**
482 		 * Converts BBCode to HTML
483 		 * 
484 		 * @param {String} text
485 		 * @param {Bool} isFragment
486 		 * @return {String} HTML
487 		 * @memberOf jQuery.sceditorBBCodePlugin.prototype
488 		 */
489 		base.getTextHandler = function(text, isFragment) {
490 
491 			var	oldText, replaceBBCodeFunc,
492 				bbcodeRegex = /\[([^\[\s=]+)(?:([^\[]+))?\]((?:[\s\S](?!\[\1))*?)\[\/(\1)\]/g,
493 				atribsRegex = /(\S+)=((?:(?:(["'])(?:\\\3|[^\3])*?\3))|(?:[^'"\s]+))/g;
494 
495 			replaceBBCodeFunc = function(str, bbcode, attrs, content)
496 			{
497 				var	attrsMap = {},
498 					matches;
499 					
500 				if(attrs)
501 				{
502 					attrs = $.trim(attrs);
503 					
504 					// if only one attribute then remove the = from the start and strip any quotes
505 					if((attrs.charAt(0) === "=" && (attrs.split("=").length - 1) <= 1) || bbcode === 'url')
506 						attrsMap.defaultattr = base.stripQuotes(attrs.substr(1));
507 					else
508 					{
509 						if(attrs.charAt(0) === "=")
510 							attrs = "defaultattr" + attrs;
511 
512 						while((matches = atribsRegex.exec(attrs)))
513 							attrsMap[matches[1].toLowerCase()] = base.stripQuotes(matches[2]);
514 					}
515 				}
516 
517 				if(!base.bbcodes[bbcode])
518 					return str;
519 
520 				if($.isFunction(base.bbcodes[bbcode].html))
521 					return base.bbcodes[bbcode].html.call(base, bbcode, attrsMap, content);
522 				else
523 					return formatString(base.bbcodes[bbcode].html, content);
524 			};
525 
526 			text = text.replace(/&/g, "&")
527 					.replace(/</g, "<")
528 					.replace(/>/g, ">")
529 					.replace(/\r/g, "")
530 					.replace(/(\[\/?(?:left|center|right|justify|align|rtl|ltr)\])\n/g, "$1")
531 					.replace(/\n/g, "<br />");
532 
533 			while(text !== oldText)
534 			{
535 				oldText = text;
536 				text    = text.replace(bbcodeRegex, replaceBBCodeFunc);
537 			}
538 
539 			// As hr is the only bbcode not to have a start and end tag it's
540 			// just being replace here instead of adding support for it above.
541 			text = text.replace(/\[hr\]/gi, "<hr>")
542 					.replace(/\[\*\]/gi, "<li>");
543 
544 			// replace multi-spaces which are not inside tags with a non-breaking space
545 			// to preserve them. Otherwise they will just be converted to 1!
546 			text = text.replace(/ {2}(?=([^<\>]*?<|[^<\>]*?$))/g, "  ");
547 			
548 			return wrapInDivs(text, isFragment);
549 		};
550 		
551 		/**
552 		 * Wraps divs around inline HTML. Needed for IE
553 		 * 
554 		 * @param string html
555 		 * @return string HTML
556 		 * @private
557 		 */
558 		wrapInDivs = function(html, excludeFirstLast)
559 		{
560 			var	d		= document,
561 				inlineFrag	= d.createDocumentFragment(),
562 				outputDiv	= d.createElement('div'),
563 				tmpDiv		= d.createElement('div'),
564 				div, node, next, nodeName;
565 			
566 			$(tmpDiv).hide().appendTo(d.body);
567 			tmpDiv.innerHTML = html;
568 			
569 			node = tmpDiv.firstChild;
570 			while(node)
571 			{
572 				next = node.nextSibling;
573 				nodeName = node.nodeName.toLowerCase();
574 
575 				if((node.nodeType === 1 && !$.sceditor.dom.isInline(node)) || nodeName === "br")
576 				{
577 					if(inlineFrag.childNodes.length > 0 || nodeName === "br")
578 					{
579 						div = d.createElement('div');
580 						div.appendChild(inlineFrag);
581 						
582 						// Putting BR in a div in IE9 causes it to do a double line break,
583 						// as much as I hate browser UA sniffing, to do feature detection would
584 						// be more code than it's worth for this specific bug.
585 						if(nodeName === "br" && (!$.sceditor.ie || $.sceditor.ie < 9))
586 							div.appendChild(d.createElement('br'));
587 						
588 						outputDiv.appendChild(div);
589 						inlineFrag = d.createDocumentFragment();
590 					}
591 					
592 					if(nodeName !== "br")
593 						outputDiv.appendChild(node);
594 				}
595 				else
596 					inlineFrag.appendChild(node);
597 					
598 				node = next;
599 			}
600 			
601 			if(inlineFrag.childNodes.length > 0)
602 			{
603 				div = d.createElement('div');
604 				div.appendChild(inlineFrag);
605 				outputDiv.appendChild(div);
606 			}
607 			
608 			// needed for paste, the first shouldn't be wrapped in a div
609 			if(excludeFirstLast)
610 			{
611 				node = outputDiv.firstChild;
612 				if(node && node.nodeName.toLowerCase() === "div")
613 				{
614 					while((next = node.firstChild))
615 						outputDiv.insertBefore(next, node);
616 					
617 					if($.sceditor.ie >= 9)
618 						outputDiv.insertBefore(d.createElement('br'), node);
619 					
620 					outputDiv.removeChild(node);
621 				}
622 				
623 				node = outputDiv.lastChild;
624 				if(node && node.nodeName.toLowerCase() === "div")
625 				{
626 					while((next = node.firstChild))
627 						outputDiv.insertBefore(next, node);
628 
629 					if($.sceditor.ie >= 9)
630 						outputDiv.insertBefore(d.createElement('br'), node);
631 					
632 					outputDiv.removeChild(node);
633 				}
634 			}
635 
636 			$(tmpDiv).remove();
637 			return outputDiv.innerHTML;
638 		};
639 
640 		init();
641 	};
642 	
643 	$.sceditorBBCodePlugin.bbcodes = {
644 		// START_COMMAND: Bold
645 		b: {
646 			tags: {
647 				b: null,
648 				strong: null
649 			},
650 			styles: {
651 				// 401 is for FF 3.5
652 				"font-weight": ["bold", "bolder", "401", "700", "800", "900"]
653 			},
654 			format: "[b]{0}[/b]",
655 			html: '<strong>{0}</strong>'
656 		},
657 		// END_COMMAND
658 
659 		// START_COMMAND: Italic
660 		i: {
661 			tags: {
662 				i: null,
663 				em: null
664 			},
665 			styles: {
666 				"font-style": ["italic", "oblique"]
667 			},
668 			format: "[i]{0}[/i]",
669 			html: '<em>{0}</em>'
670 		},
671 		// END_COMMAND
672 
673 		// START_COMMAND: Underline
674 		u: {
675 			tags: {
676 				u: null
677 			},
678 			styles: {
679 				"text-decoration": ["underline"]
680 			},
681 			format: "[u]{0}[/u]",
682 			html: '<u>{0}</u>'
683 		},
684 		// END_COMMAND
685 
686 		// START_COMMAND: Strikethrough
687 		s: {
688 			tags: {
689 				s: null,
690 				strike: null
691 			},
692 			styles: {
693 				"text-decoration": ["line-through"]
694 			},
695 			format: "[s]{0}[/s]",
696 			html: '<s>{0}</s>'
697 		},
698 		// END_COMMAND
699 
700 		// START_COMMAND: Subscript
701 		sub: {
702 			tags: {
703 				sub: null
704 			},
705 			format: "[sub]{0}[/sub]",
706 			html: '<sub>{0}</sub>'
707 		},
708 		// END_COMMAND
709 
710 		// START_COMMAND: Superscript
711 		sup: {
712 			tags: {
713 				sup: null
714 			},
715 			format: "[sup]{0}[/sup]",
716 			html: '<sup>{0}</sup>'
717 		},
718 		// END_COMMAND
719 
720 		// START_COMMAND: Font
721 		font: {
722 			tags: {
723 				font: {
724 					face: null
725 				}
726 			},
727 			styles: {
728 				"font-family": null
729 			},
730 			format: function(element, content) {
731 				if(element[0].nodeName.toLowerCase() === "font" && element.attr('face'))
732 					return '[font=' + this.stripQuotes(element.attr('face')) + ']' + content + '[/font]';
733 
734 				return '[font=' + this.stripQuotes(element.css('font-family')) + ']' + content + '[/font]';
735 			},
736 			html: function(element, attrs, content) {
737 				return '<font face="' + attrs.defaultattr + '">' + content + '</font>';
738 			}
739 		},
740 		// END_COMMAND
741 
742 		// START_COMMAND: Size
743 		size: {
744 			tags: {
745 				font: {
746 					size: null
747 				}
748 			},
749 			styles: {
750 				"font-size": null
751 			},
752 			format: function(element, content) {
753 				var	fontSize = element.css('fontSize'),
754 					size     = 1;
755 
756 				// Most browsers return px value but IE returns 1-7
757 				if(fontSize.indexOf("px") > -1) {
758 					// convert size to an int
759 					fontSize = fontSize.replace("px", "") - 0;
760 
761 					if(fontSize > 12)
762 						size = 2;
763 					if(fontSize > 15)
764 						size = 3;
765 					if(fontSize > 17)
766 						size = 4;
767 					if(fontSize > 23)
768 						size = 5;
769 					if(fontSize > 31)
770 						size = 6;
771 					if(fontSize > 47)
772 						size = 7;
773 				}
774 				else
775 					size = fontSize;
776 
777 				return '[size=' + size + ']' + content + '[/size]';
778 			},
779 			html: function(element, attrs, content) {
780 				return '<font size="' + attrs.defaultattr + '">' + content + '</font>';
781 			}
782 		},
783 		// END_COMMAND
784 
785 		// START_COMMAND: Color
786 		color: {
787 			tags: {
788 				font: {
789 					color: null
790 				}
791 			},
792 			styles: {
793 				color: null
794 			},
795 			format: function(element, content) {
796 				/**
797 				 * Converts CSS rgb value into hex
798 				 * @private
799 				 * @return string Hex color
800 				 */
801 				var rgbToHex = function(rgbStr) {
802 					var m;
803 		
804 					function toHex(n) {
805 						n = parseInt(n,10);
806 						if(isNaN(n))
807 							return "00";
808 						n = Math.max(0,Math.min(n,255)).toString(16);
809 		
810 						return n.length<2 ? '0'+n : n;
811 					}
812 		
813 					// rgb(n,n,n);
814 					if((m = rgbStr.match(/rgb\((\d+),\s*?(\d+),\s*?(\d+)\)/i)))
815 						return '#' + toHex(m[1]) + toHex(m[2]-0) + toHex(m[3]-0);
816 		
817 					// expand shorthand
818 					if((m = rgbStr.match(/#([0-f])([0-f])([0-f])\s*?$/i)))
819 						return '#' + m[1] + m[1] + m[2] + m[2] + m[3] + m[3];
820 					
821 					return rgbStr;
822 				};
823 		
824 				var color = element.css('color');
825 
826 				if(element[0].nodeName.toLowerCase() === "font" && element.attr('color'))
827 					color = element.attr('color');
828 				
829 				color = rgbToHex(color);
830 
831 				return '[color=' + color + ']' + content + '[/color]';
832 			},
833 			html: function(element, attrs, content) {
834 				return '<font color="' + attrs.defaultattr + '">' + content + '</font>';
835 			}
836 		},
837 		// END_COMMAND
838 
839 		// START_COMMAND: Lists
840 		ul: {
841 			tags: {
842 				ul: null
843 			},
844 			isBlock: true,
845 			format: "[ul]{0}[/ul]",
846 			html: '<ul>{0}</ul>'
847 		},
848 		list: {
849 			html: '<ul>{0}</ul>'
850 		},
851 		ol: {
852 			tags: {
853 				ol: null
854 			},
855 			isBlock: true,
856 			format: "[ol]{0}[/ol]",
857 			html: '<ol>{0}</ol>'
858 		},
859 		li: {
860 			tags: {
861 				li: null
862 			},
863 			format: "[li]{0}[/li]",
864 			html: '<li>{0}</li>'
865 		},
866 		"*": {
867 			html: '<li>{0}</li>'
868 		},
869 		// END_COMMAND
870 
871 		// START_COMMAND: Table
872 		table: {
873 			tags: {
874 				table: null
875 			},
876 			format: "[table]{0}[/table]",
877 			html: '<table>{0}</table>'
878 		},
879 		tr: {
880 			tags: {
881 				tr: null
882 			},
883 			format: "[tr]{0}[/tr]",
884 			html: '<tr>{0}</tr>'
885 		},
886 		th: {
887 			tags: {
888 				th: null
889 			},
890 			isBlock: true,
891 			format: "[th]{0}[/th]",
892 			html: '<th>{0}</th>'
893 		},
894 		td: {
895 			tags: {
896 				td: null
897 			},
898 			isBlock: true,
899 			format: "[td]{0}[/td]",
900 			html: '<td>{0}<br class="sceditor-ignore" /></td>'
901 		},
902 		// END_COMMAND
903 
904 		// START_COMMAND: Emoticons
905 		emoticon: {
906 			allowsEmpty: true,
907 			tags: {
908 				img: {
909 					src: null,
910 					"data-sceditor-emoticon": null
911 				}
912 			},
913 			format: function(element, content) {
914 				return element.attr('data-sceditor-emoticon') + content;
915 			},
916 			html: '{0}'
917 		},
918 		// END_COMMAND
919 
920 		// START_COMMAND: Horizontal Rule
921 		horizontalrule: {
922 			allowsEmpty: true,
923 			tags: {
924 				hr: null
925 			},
926 			format: "[hr]{0}",
927 			html: "<hr />"
928 		},
929 		// END_COMMAND
930 
931 		// START_COMMAND: Image
932 		img: {
933 			allowsEmpty: true,
934 			tags: {
935 				img: {
936 					src: null
937 				}
938 			},
939 			format: function(element, content) {
940 				// check if this is an emoticon image
941 				if(typeof element.attr('data-sceditor-emoticon') !== "undefined")
942 					return content;
943 
944 				var attribs = "=" + $(element).width() + "x" + $(element).height();
945 
946 				return '[img' + attribs + ']' + element.attr('src') + '[/img]';
947 			},
948 			html: function(element, attrs, content) {
949 				var attribs = "", parts;
950 
951 				// handle [img width=340 height=240]url[/img]
952 				if(typeof attrs.width !== "undefined")
953 					attribs += ' width="' + attrs.width + '"';
954 				if(typeof attrs.height !== "undefined")
955 					attribs += ' height="' + attrs.height + '"';
956 
957 				// handle [img=340x240]url[/img]
958 				if(typeof attrs.defaultattr !== "undefined") {
959 					parts = attrs.defaultattr.split(/x/i);
960 
961 					attribs = ' width="' + parts[0] + '"' +
962 						' height="' + (parts.length === 2 ? parts[1] : parts[0]) + '"';
963 				}
964 
965 				return '<img ' + attribs + ' src="' + content + '" />';
966 			}
967 		},
968 		// END_COMMAND
969 
970 		// START_COMMAND: URL
971 		url: {
972 			allowsEmpty: true,
973 			tags: {
974 				a: {
975 					href: null
976 				}
977 			},
978 			format: function(element, content) {
979 				// make sure this link is not an e-mail, if it is return e-mail BBCode
980 				if(element.attr('href').substr(0, 7) === 'mailto:')
981 					return '[email=' + element.attr('href').substr(7) + ']' + content + '[/email]';
982 
983 				return '[url=' + decodeURI(element.attr('href')) + ']' + content + '[/url]';
984 			},
985 			html: function(element, attrs, content) {
986 				if(typeof attrs.defaultattr === "undefined" || attrs.defaultattr.length === 0)
987 					attrs.defaultattr = content;
988 
989 				return '<a href="' + encodeURI(attrs.defaultattr) + '">' + content + '</a>';
990 			}
991 		},
992 		// END_COMMAND
993 
994 		// START_COMMAND: E-mail
995 		email: {
996 			html: function(element, attrs, content) {
997 				if(typeof attrs.defaultattr === "undefined")
998 					attrs.defaultattr = content;
999 
1000 				return '<a href="mailto:' + attrs.defaultattr + '">' + content + '</a>';
1001 			}
1002 		},
1003 		// END_COMMAND
1004 
1005 		// START_COMMAND: Quote
1006 		quote: {
1007 			tags: {
1008 				blockquote: null
1009 			},
1010 			isBlock: true,
1011 			format: function(element, content) {
1012 				var	author,
1013 					attr = '',
1014 					$elm = $(element);
1015 		
1016 				if($elm.children("cite:first").length === 1 || $elm.data("author")) {
1017 					author = $(element).children("cite:first").text() || $elm.data("author");
1018 		
1019 					
1020 					$elm.data("author", author)
1021 					$(element).children("cite:first").remove();
1022 		
1023 					content	= '';
1024 					content	= this.elementToBbcode($(element));
1025 					attr	= '=' + author;
1026 				}
1027 
1028 				return '[quote' + attr + ']' + content + '[/quote]';
1029 			},
1030 			html: function(element, attrs, content) {
1031 				if(typeof attrs.defaultattr !== "undefined")
1032 					content = '<cite>' + attrs.defaultattr + '</cite>' + content;
1033 
1034 				return '<blockquote>' + content + '</blockquote>';
1035 			}
1036 		},
1037 		// END_COMMAND
1038 
1039 		// START_COMMAND: Code
1040 		code: {
1041 			tags: {
1042 				code: null
1043 			},
1044 			isBlock: true,
1045 			format: "[code]{0}[/code]",
1046 			html: '<code>{0}</code>'
1047 		},
1048 		// END_COMMAND
1049 
1050 
1051 		// START_COMMAND: Left
1052 		left: {
1053 			styles: {
1054 				"text-align": ["left", "-webkit-left", "-moz-left", "-khtml-left"]
1055 			},
1056 			isBlock: true,
1057 			format: "[left]{0}[/left]",
1058 			html: '<div align="left">{0}</div>'
1059 		},
1060 		// END_COMMAND
1061 
1062 		// START_COMMAND: Centre
1063 		center: {
1064 			styles: {
1065 				"text-align": ["center", "-webkit-center", "-moz-center", "-khtml-center"]
1066 			},
1067 			isBlock: true,
1068 			format: "[center]{0}[/center]",
1069 			html: '<div align="center">{0}</div>'
1070 		},
1071 		// END_COMMAND
1072 
1073 		// START_COMMAND: Right
1074 		right: {
1075 			styles: {
1076 				"text-align": ["right", "-webkit-right", "-moz-right", "-khtml-right"]
1077 			},
1078 			isBlock: true,
1079 			format: "[right]{0}[/right]",
1080 			html: '<div align="right">{0}</div>'
1081 		},
1082 		// END_COMMAND
1083 
1084 		// START_COMMAND: Justify
1085 		justify: {
1086 			styles: {
1087 				"text-align": ["justify", "-webkit-justify", "-moz-justify", "-khtml-justify"]
1088 			},
1089 			isBlock: true,
1090 			format: "[justify]{0}[/justify]",
1091 			html: '<div align="justify">{0}</div>'
1092 		},
1093 		// END_COMMAND
1094 
1095 		// START_COMMAND: YouTube
1096 		youtube: {
1097 			allowsEmpty: true,
1098 			tags: {
1099 				iframe: {
1100 					'data-youtube-id': null
1101 				}
1102 			},
1103 			format: function(element, content) {
1104 				if(!element.attr('data-youtube-id'))
1105 					return content;
1106 
1107 				return '[youtube]' + element.attr('data-youtube-id') + '[/youtube]';
1108 			},
1109 			html: '<iframe width="560" height="315" src="http://www.youtube.com/embed/{0}?wmode=opaque' +
1110 				'" data-youtube-id="{0}" frameborder="0" allowfullscreen></iframe>'
1111 		},
1112 		// END_COMMAND
1113 		
1114 		
1115 		// START_COMMAND: Rtl
1116 		rtl: {
1117 			styles: {
1118 				"direction": ["rtl"]
1119 			},
1120 			format: "[rtl]{0}[/rtl]",
1121 			html: '<div style="direction: rtl">{0}</div>'
1122 		},
1123 		// END_COMMAND
1124 		
1125 		// START_COMMAND: Ltr
1126 		ltr: {
1127 			styles: {
1128 				"direction": ["ltr"]
1129 			},
1130 			format: "[ltr]{0}[/ltr]",
1131 			html: '<div style="direction: ltr">{0}</div>'
1132 		},
1133 		// END_COMMAND
1134 		
1135 		
1136 		// this is here so that commands above can be removed
1137 		// without having to remove the , after the last one.
1138 		// Needed for IE.
1139 		ignore: {}
1140 	};
1141 	
1142 	/**
1143 	 * Static BBCode helper class
1144 	 * @class command
1145 	 * @name jQuery.sceditorBBCodePlugin.bbcode
1146 	 */
1147 	$.sceditorBBCodePlugin.bbcode = 
1148 	/** @lends jQuery.sceditorBBCodePlugin.bbcode */
1149 	{
1150 		/**
1151 		 * Gets a BBCode
1152 		 * 
1153 		 * @param {String} name
1154 		 * @return {Object|null}
1155 		 * @since v1.3.5
1156 		 */
1157 		get: function(name) {
1158 			return $.sceditorBBCodePlugin.bbcodes[name] || null;
1159 		},
1160 		
1161 		/**
1162 		 * <p>Adds a BBCode to the parser or updates an exisiting
1163 		 * BBCode if a BBCode with the specified name already exists.</p>
1164 		 * 
1165 		 * @param {String} name
1166 		 * @param {Object} bbcode
1167 		 * @return {this|false} Returns false if name or bbcode is false
1168 		 * @since v1.3.5
1169 		 */
1170 		set: function(name, bbcode) {
1171 			if(!name || !bbcode)
1172 				return false;
1173 			
1174 			// merge any existing command properties
1175 			bbcode = $.extend($.sceditorBBCodePlugin.bbcodes[name] || {}, bbcode);
1176 		
1177 			bbcode.remove = function() { $.sceditorBBCodePlugin.bbcode.remove(name); };
1178 			
1179 			$.sceditorBBCodePlugin.bbcodes[name] = bbcode;
1180 			return this;
1181 		},
1182 		
1183 		/**
1184 		 * Removes a BBCode
1185 		 * 
1186 		 * @param {String} name
1187 		 * @return {this}
1188 		 * @since v1.3.5
1189 		 */
1190 		remove: function(name) {
1191 			if($.sceditorBBCodePlugin.bbcodes[name])
1192 				delete $.sceditorBBCodePlugin.bbcodes[name];
1193 			
1194 			return this;
1195 		}
1196 	};
1197 	
1198 	/**
1199 	 * Checks if a command with the specified name exists
1200 	 * 
1201 	 * @param string name
1202 	 * @return bool
1203 	 * @deprecated Since v1.3.5
1204 	 * @memberOf jQuery.sceditorBBCodePlugin
1205 	 */
1206 	$.sceditorBBCodePlugin.commandExists = function(name) {
1207 		return !!$.sceditorBBCodePlugin.bbcode.get(name);
1208 	};
1209 	
1210 	/**
1211 	 * Adds/updates a BBCode.
1212 	 * 
1213 	 * @param String		name		The BBCode name
1214 	 * @param Object		tags		Any html tags this bbcode applies to, i.e. strong for [b]
1215 	 * @param Object		styles		Any style properties this applies to, i.e. font-weight for [b]
1216 	 * @param String|Function	format		Function or string to convert the element into BBCode
1217 	 * @param String|Function	html		String or function to format the BBCode back into HTML.
1218 	 * @param bool			allowsEmpty	If this BBCodes is allowed to be empty, e.g. [b][/b]
1219 	 * @return Bool
1220 	 * @deprecated Since v1.3.5
1221 	 * @memberOf jQuery.sceditorBBCodePlugin
1222 	 */
1223 	$.sceditorBBCodePlugin.setCommand = function(name, tags, styles, format, html, allowsEmpty, isBlock) {
1224 		return $.sceditorBBCodePlugin.bbcode.set(name,
1225 		{
1226 			tags: tags || {},
1227 			styles: styles || {},
1228 			allowsEmpty: allowsEmpty,
1229 			isBlock: isBlock,
1230 			format: format,
1231 			html: html
1232 		});
1233 	};
1234 
1235 	$.fn.sceditorBBCodePlugin = function(options) {
1236 		if((!options || !options.runWithoutWysiwygSupport) && !$.sceditor.isWysiwygSupported())
1237 			return;
1238 		
1239 		return this.each(function() {
1240 			(new $.sceditorBBCodePlugin(this, options));
1241 		});
1242 	};
1243 })(jQuery);
1244