/**
 * SCEditor BBCode Plugin
 * http://www.sceditor.com/
 *
 * Copyright (C) 2011-2014, Sam Clarke (samclarke.com)
 *
 * SCEditor is licensed under the MIT license:
 *	http://www.opensource.org/licenses/mit-license.php
 *
 * @fileoverview SCEditor BBCode Plugin
 * @author Sam Clarke
 * @requires jQuery
 */
/*global prompt: true*/
/*jshint maxdepth: false*/
// TODO: Tidy this code up and consider seperating the BBCode parser into a
// standalone module that can be used with other JS/NodeJS
(function ($, window, document) {
	'use strict';

	var SCEditor        = $.sceditor;
	var sceditorPlugins = SCEditor.plugins;
	var escapeEntities  = SCEditor.escapeEntities;
	var escapeUriScheme = SCEditor.escapeUriScheme;

	var IE_VER = SCEditor.ie;

	// In IE < 11 a BR at the end of a block level element
	// causes a double line break.
	var IE_BR_FIX = IE_VER && IE_VER < 11;



	var getEditorCommand = SCEditor.command.get;

	var defaultCommandsOverrides = {
		bold: {
			txtExec: ['[b]', '[/b]']
		},
		italic: {
			txtExec: ['[i]', '[/i]']
		},
		underline: {
			txtExec: ['[u]', '[/u]']
		},
		strike: {
			txtExec: ['[s]', '[/s]']
		},
		subscript: {
			txtExec: ['[sub]', '[/sub]']
		},
		superscript: {
			txtExec: ['[sup]', '[/sup]']
		},
		left: {
			txtExec: ['[left]', '[/left]']
		},
		center: {
			txtExec: ['[center]', '[/center]']
		},
		right: {
			txtExec: ['[right]', '[/right]']
		},
		justify: {
			txtExec: ['[justify]', '[/justify]']
		},
		font: {
			txtExec: function (caller) {
				var editor = this;

				getEditorCommand('font')._dropDown(
					editor,
					caller,
					function (fontName) {
						editor.insertText(
							'[font=' + fontName + ']',
							'[/font]'
						);
					}
				);
			}
		},
		size: {
			txtExec: function (caller) {
				var editor = this;

				getEditorCommand('size')._dropDown(
					editor,
					caller,
					function (fontSize) {
						editor.insertText(
							'[size=' + fontSize + ']',
							'[/size]'
						);
					}
				);
			}
		},
		color: {
			txtExec: function (caller) {
				var editor = this;

				getEditorCommand('color')._dropDown(
					editor,
					caller,
					function (color) {
						editor.insertText(
							'[color=' + color + ']',
							'[/color]'
						);
					}
				);
			}
		},
		bulletlist: {
			txtExec: function (caller, selected) {
				var content = '';

				$.each(selected.split(/\r?\n/), function () {
					content += (content ? '\n' : '') +
						'[li]' + this + '[/li]';
				});

				this.insertText('[ul]\n' + content + '\n[/ul]');
			}
		},
		orderedlist: {
			txtExec: function (caller, selected) {
				var content = '';

				$.each(selected.split(/\r?\n/), function () {
					content += (content ? '\n' : '') +
						'[li]' + this + '[/li]';
				});

				sceditorPlugins.bbcode.bbcode.get('');

				this.insertText('[ol]\n' + content + '\n[/ol]');
			}
		},
		table: {
			txtExec: ['[table][tr][td]', '[/td][/tr][/table]']
		},
		horizontalrule: {
			txtExec: ['[hr]']
		},
		code: {
			txtExec: ['[code]', '[/code]']
		},
		image: {
			txtExec: function (caller, selected) {
				var	editor = this,
					url    = prompt(editor._('Enter the image URL:'), selected);

				if (url) {
					editor.insertText('[img]' + url + '[/img]');
				}
			}
		},
		email: {
			txtExec: function (caller, selected) {
				var	editor  = this,
					display = selected && selected.indexOf('@') > -1 ?
						null : selected,
					email	= prompt(editor._('Enter the e-mail address:'),
						(display ? '' : selected)),
					text	= prompt(editor._('Enter the displayed text:'),
						display || email) || email;

				if (email) {
					editor.insertText('[email=' + email + ']' +
						text + '[/email]');
				}
			}
		},
		link: {
			txtExec: function (caller, selected) {
				var	editor  = this,
					display = /^[a-z]+:\/\//i.test($.trim(selected)) ?
						null : selected,
					url     = prompt(editor._('Enter URL:'),
						(display ? 'http://' : $.trim(selected))),
					text    = prompt(editor._('Enter the displayed text:'),
						display || url) || url;

				if (url) {
					editor.insertText('[url=' + url + ']' + text + '[/url]');
				}
			}
		},
		quote: {
			txtExec: ['[quote]', '[/quote]']
		},
		youtube: {
			txtExec: function (caller) {
				var editor = this;

				getEditorCommand('youtube')._dropDown(
					editor,
					caller,
					function (id) {
						editor.insertText('[youtube]' + id + '[/youtube]');
					}
				);
			}
		},
		rtl: {
			txtExec: ['[rtl]', '[/rtl]']
		},
		ltr: {
			txtExec: ['[ltr]', '[/ltr]']
		}
	};

	/**
	 * Removes any leading or trailing quotes ('")
	 *
	 * @return string
	 * @since v1.4.0
	 */
	var _stripQuotes = function (str) {
		return str ?
			str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str;
	};

	/**
	 * Formats a string replacing {0}, {1}, {2}, ect. with
	 * the params provided
	 *
	 * @param {String} str The string to format
	 * @param {string} args... The strings to replace
	 * @return {String}
	 * @since v1.4.0
	 */
	var _formatString = function () {
		var	undef;
		var args = arguments;

		return args[0].replace(/\{(\d+)\}/g, function (str, p1) {
			return args[p1 - 0 + 1] !== undef ?
				args[p1 - 0 + 1] :
				'{' + p1 + '}';
		});
	};

	/**
	 * Enum of valid token types
	 * @type {Object}
	 * @private
	 */
	var TokenType = {
		OPEN:    'open',
		CONTENT: 'content',
		NEWLINE: 'newline',
		CLOSE:   'close'
	};


	/**
	 * Tokenize token object
	 *
	 * @param  {String} type The type of token this is,
	 *                       should be one of tokenType
	 * @param  {String} name The name of this token
	 * @param  {String} val The originally matched string
	 * @param  {Array} attrs Any attributes. Only set on
	 *                       TokenType.OPEN tokens
	 * @param  {Array} children Any children of this token
	 * @param  {TokenizeToken} closing This tokens closing tag.
	 *                                 Only set on TokenType.OPEN tokens
	 * @class TokenizeToken
	 * @name TokenizeToken
	 * @memberOf jQuery.sceditor.BBCodeParser.prototype
	 */
	var TokenizeToken = function (
		/*jshint maxparams: false*/
		type, name, val, attrs, children, closing
	) {
		var base      = this;

		base.type     = type;
		base.name     = name;
		base.val      = val;
		base.attrs    = attrs || {};
		base.children = children || [];
		base.closing  = closing || null;
	};

	TokenizeToken.prototype = {
		/** @lends jQuery.sceditor.BBCodeParser.prototype.TokenizeToken */
		/**
		 * Clones this token
		 *
		 * @param  {Bool} includeChildren If to include the children in
		 *                                the clone. Defaults to false.
		 * @return {TokenizeToken}
		 */
		clone: function (includeChildren) {
			var base = this;

			return new TokenizeToken(
				base.type,
				base.name,
				base.val,
				base.attrs,
				includeChildren ? base.children : [],
				base.closing ? base.closing.clone() : null
			);
		},
		/**
		 * Splits this token at the specified child
		 *
		 * @param  {TokenizeToken|Int} splitAt The child to split at or the
		 *                                     index of the child
		 * @return {TokenizeToken} The right half of the split token or
		 *                         null if failed
		 */
		splitAt: function (splitAt) {
			var	clone;
			var base          = this;
			var splitAtLength = 0;
			var childrenLen   = base.children.length;

			if (typeof splitAt !== 'number') {
				splitAt = $.inArray(splitAt, base.children);
			}

			if (splitAt < 0 || splitAt > childrenLen) {
				return null;
			}

			// Work out how many items are on the right side of the split
			// to pass to splice()
			while (childrenLen--) {
				if (childrenLen >= splitAt) {
					splitAtLength++;
				} else {
					childrenLen = 0;
				}
			}

			clone          = base.clone();
			clone.children = base.children.splice(splitAt, splitAtLength);
			return clone;
		}
	};


	/**
	 * SCEditor BBCode parser class
	 *
	 * @param {Object} options
	 * @class BBCodeParser
	 * @name jQuery.sceditor.BBCodeParser
	 * @since v1.4.0
	 */
	var BBCodeParser = function (options) {
		// make sure this is not being called as a function
		if (!(this instanceof BBCodeParser)) {
			return new BBCodeParser(options);
		}

		var base = this;

		// Private methods
		var	init,
			tokenizeTag,
			tokenizeAttrs,
			parseTokens,
			normaliseNewLines,
			fixNesting,
			isChildAllowed,
			removeEmpty,
			fixChildren,
			convertToHTML,
			convertToBBCode,
			hasTag,
			quote,
			lower,
			last;


		init = function () {
			base.bbcodes = sceditorPlugins.bbcode.bbcodes;
			base.opts    = $.extend(
				{},
				BBCodeParser.defaults,
				options
			);
		};

		/**
		 * Takes a BBCode string and splits it into open,
		 * content and close tags.
		 *
		 * It does no checking to verify a tag has a matching open
		 * or closing tag or if the tag is valid child of any tag
		 * before it. For that the tokens should be passed to the
		 * parse function.
		 *
		 * @param {String} str
		 * @return {Array}
		 * @memberOf jQuery.sceditor.BBCodeParser.prototype
		 */
		base.tokenize = function (str) {
			var	matches, type, i;
			var toks   = [];
			var tokens = [
				// Close must come before open as they are
				// the same except close has a / at the start.
				{
					type: TokenType.CLOSE,
					regex: /^\[\/[^\[\]]+\]/
				},
				{
					type: TokenType.OPEN,
					regex: /^\[[^\[\]]+\]/
				},
				{
					type: TokenType.NEWLINE,
					regex: /^(\r\n|\r|\n)/
				},
				{
					type: TokenType.CONTENT,
					regex: /^([^\[\r\n]+|\[)/
				}
			];

			tokens.reverse();

			strloop:
			while (str.length) {
				i = tokens.length;
				while (i--) {
					type = tokens[i].type;

					// Check if the string matches any of the tokens
					if (!(matches = str.match(tokens[i].regex)) ||
						!matches[0]) {
						continue;
					}

					// Add the match to the tokens list
					toks.push(tokenizeTag(type, matches[0]));

					// Remove the match from the string
					str = str.substr(matches[0].length);

					// The token has been added so start again
					continue strloop;
				}

				// If there is anything left in the string which doesn't match
				// any of the tokens then just assume it's content and add it.
				if (str.length) {
					toks.push(tokenizeTag(TokenType.CONTENT, str));
				}

				str = '';
			}

			return toks;
		};

		/**
		 * Extracts the name an params from a tag
		 *
		 * @param {tokenType} type
		 * @param {string} val
		 * @return {Object}
		 * @private
		 */
		tokenizeTag = function (type, val) {
			var matches, attrs, name,
				openRegex  = /\[([^\]\s=]+)(?:([^\]]+))?\]/,
				closeRegex = /\[\/([^\[\]]+)\]/;

			// Extract the name and attributes from opening tags and
			// just the name from closing tags.
			if (type === TokenType.OPEN && (matches = val.match(openRegex))) {
				name = lower(matches[1]);

				if (matches[2] && (matches[2] = $.trim(matches[2]))) {
					attrs = tokenizeAttrs(matches[2]);
				}
			}

			if (type === TokenType.CLOSE && (matches = val.match(closeRegex))) {
				name = lower(matches[1]);
			}

			if (type === TokenType.NEWLINE) {
				name = '#newline';
			}

			// Treat all tokens without a name and
			// all unknown BBCodes as content
			if (!name ||
				((type === TokenType.OPEN || type === TokenType.CLOSE) &&
					!sceditorPlugins.bbcode.bbcodes[name])) {
				type = TokenType.CONTENT;
				name = '#';
			}

			return new TokenizeToken(type, name, val, attrs);
		};

		/**
		 * Extracts the individual attributes from a string containing
		 * all the attributes.
		 *
		 * @param {String} attrs
		 * @return {Array} Assoc array of attributes
		 * @private
		 */
		tokenizeAttrs = function (attrs) {
			var	matches,
				/*
				([^\s=]+)				Anything that's not a space or equals
				=						Equals sign =
				(?:
					(?:
						(["'])					The opening quote
						(
							(?:\\\2|[^\2])*?	Anything that isn't the
												unescaped opening quote
						)
						\2						The opening quote again which
												will close the string
					)
						|				If not a quoted string then match
					(
						(?:.(?!\s\S+=))*.?		Anything that isn't part of
												[space][non-space][=] which
												would be a new attribute
					)
				)
				*/
				attrRegex =
			/([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g,
				ret       = {};

			// if only one attribute then remove the = from the start and
			// strip any quotes
			if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) {
				ret.defaultattr = _stripQuotes(attrs.substr(1));
			} else {
				if (attrs.charAt(0) === '=') {
					attrs = 'defaultattr' + attrs;
				}

				// No need to strip quotes here, the regex will do that.
				while ((matches = attrRegex.exec(attrs))) {
					ret[lower(matches[1])] =
						_stripQuotes(matches[3]) || matches[4];
				}
			}

			return ret;
		};

		/**
		 * Parses a string into an array of BBCodes
		 *
		 * @param  {string}  str
		 * @param  {boolean} preserveNewLines If to preserve all new lines, not
		 *                                    strip any based on the passed
		 *                                    formatting options
		 * @return {Array}                    Array of BBCode objects
		 * @memberOf jQuery.sceditor.BBCodeParser.prototype
		 */
		base.parse = function (str, preserveNewLines) {
			var ret  = parseTokens(base.tokenize(str));
			var opts = base.opts;

			if (opts.fixInvalidChildren) {
				fixChildren(ret);
			}

			if (opts.removeEmptyTags) {
				removeEmpty(ret);
			}

			if (opts.fixInvalidNesting) {
				fixNesting(ret);
			}

			normaliseNewLines(ret, null, preserveNewLines);

			if (opts.removeEmptyTags) {
				removeEmpty(ret);
			}

			return ret;
		};

		/**
		 * Checks if an array of TokenizeToken's contains the
		 * specified token.
		 *
		 * Checks the tokens name and type match another tokens
		 * name and type in the array.
		 *
		 * @param  {string}    name
		 * @param  {tokenType} type
		 * @param  {Array}     arr
		 * @return {Boolean}
		 * @private
		 */
		hasTag = function (name, type, arr) {
			var i = arr.length;

			while (i--) {
				if (arr[i].type === type && arr[i].name === name) {
					return true;
				}
			}

			return false;
		};

		/**
		 * Checks if the child tag is allowed as one
		 * of the parent tags children.
		 *
		 * @param  {TokenizeToken}  parent
		 * @param  {TokenizeToken}  child
		 * @return {Boolean}
		 * @private
		 */
		isChildAllowed = function (parent, child) {
			var	parentBBCode    = parent ? base.bbcodes[parent.name] : {},
				allowedChildren = parentBBCode.allowedChildren;

			if (base.opts.fixInvalidChildren && allowedChildren) {
				return $.inArray(child.name || '#', allowedChildren) > -1;
			}

			return true;
		};

// TODO: Tidy this parseTokens() function up a bit.
		/**
		 * Parses an array of tokens created by tokenize()
		 *
		 * @param  {Array} toks
		 * @return {Array} Parsed tokens
		 * @see tokenize()
		 * @private
		 */
		parseTokens = function (toks) {
			var	token, bbcode, curTok, clone, i, previous, next,
				cloned     = [],
				output     = [],
				openTags   = [],
				/**
				 * Returns the currently open tag or undefined
				 * @return {TokenizeToken}
				 */
				currentOpenTag = function () {
					return last(openTags);
				},
				/**
				 * Adds a tag to either the current tags children
				 * or to the output array.
				 * @param {TokenizeToken} token
				 * @private
				 */
				addTag = function (token) {
					if (currentOpenTag()) {
						currentOpenTag().children.push(token);
					} else {
						output.push(token);
					}
				},
				/**
				 * Checks if this tag closes the current tag
				 * @param  {String} name
				 * @return {Void}
				 */
				closesCurrentTag = function (name) {
					return currentOpenTag() &&
						(bbcode = base.bbcodes[currentOpenTag().name]) &&
						bbcode.closedBy &&
						$.inArray(name, bbcode.closedBy) > -1;
				};

			while ((token = toks.shift())) {
				next = toks[0];

				/* jshint indent:false */
				switch (token.type) {
					case TokenType.OPEN:
						// Check it this closes a parent,
						// e.g. for lists [*]one [*]two
						if (closesCurrentTag(token.name)) {
							openTags.pop();
						}

						addTag(token);
						bbcode = base.bbcodes[token.name];

						// If this tag is not self closing and it has a closing
						// tag then it is open and has children so add it to the
						// list of open tags. If has the closedBy property then
						// it is closed by other tags so include everything as
						// it's children until one of those tags is reached.
						if ((!bbcode || !bbcode.isSelfClosing) &&
							(bbcode.closedBy ||
								hasTag(token.name, TokenType.CLOSE, toks))) {
							openTags.push(token);
						} else if (!bbcode || !bbcode.isSelfClosing) {
							token.type = TokenType.CONTENT;
						}
						break;

					case TokenType.CLOSE:
						// check if this closes the current tag,
						// e.g. [/list] would close an open [*]
						if (currentOpenTag() &&
							token.name !== currentOpenTag().name &&
							closesCurrentTag('/' + token.name)) {
							openTags.pop();
						}

						// If this is closing the currently open tag just pop
						// the close tag off the open tags array
						if (currentOpenTag() &&
							token.name === currentOpenTag().name) {
							currentOpenTag().closing = token;
							openTags.pop();

						// If this is closing an open tag that is the parent of
						// the current tag then clone all the tags including the
						// current one until reaching the parent that is being
						// closed. Close the parent and then add the clones back
						// in.
						} else if (hasTag(token.name, TokenType.OPEN,
							openTags)) {

							// Remove the tag from the open tags
							while ((curTok = openTags.pop())) {

								// If it's the tag that is being closed then
								// discard it and break the loop.
								if (curTok.name === token.name) {
									curTok.closing = token;
									break;
								}

								// Otherwise clone this tag and then add any
								// previously cloned tags as it's children
								clone = curTok.clone();

								if (cloned.length) {
									clone.children.push(last(cloned));
								}

								cloned.push(clone);
							}

							// Add the last cloned child to the now current tag
							// (the parent of the tag which was being closed)
							addTag(last(cloned));

							// Add all the cloned tags to the open tags list
							i = cloned.length;
							while (i--) {
								openTags.push(cloned[i]);
							}

							cloned.length = 0;

						// This tag is closing nothing so treat it as content
						} else {
							token.type = TokenType.CONTENT;
							addTag(token);
						}
						break;

					case TokenType.NEWLINE:
						// handle things like
						//     [*]list\nitem\n[*]list1
						// where it should come out as
						//     [*]list\nitem[/*]\n[*]list1[/*]
						// instead of
						//     [*]list\nitem\n[/*][*]list1[/*]
						if (currentOpenTag() && next &&
							closesCurrentTag(
								(next.type === TokenType.CLOSE ? '/' : '') +
								next.name
							)) {
							// skip if the next tag is the closing tag for
							// the option tag, i.e. [/*]
							if (!(next.type === TokenType.CLOSE &&
								next.name === currentOpenTag().name)) {
								bbcode = base.bbcodes[currentOpenTag().name];

								if (bbcode && bbcode.breakAfter) {
									openTags.pop();
								} else if (bbcode &&
									bbcode.isInline === false &&
									base.opts.breakAfterBlock &&
									bbcode.breakAfter !== false) {
									openTags.pop();
								}
							}
						}

						addTag(token);
						break;

					default: // content
						addTag(token);
						break;
				}

				previous = token;
			}

			return output;
		};

		/**
		 * Normalise all new lines
		 *
		 * Removes any formatting new lines from the BBCode
		 * leaving only content ones. I.e. for a list:
		 *
		 * [list]
		 * [*] list item one
		 * with a line break
		 * [*] list item two
		 * [/list]
		 *
		 * would become
		 *
		 * [list] [*] list item one
		 * with a line break [*] list item two [/list]
		 *
		 * Which makes it easier to convert to HTML or add
		 * the formatting new lines back in when converting
		 * back to BBCode
		 *
		 * @param  {Array} children
		 * @param  {TokenizeToken} parent
		 * @param  {Bool} onlyRemoveBreakAfter
		 * @return {void}
		 */
		normaliseNewLines = function (children, parent, onlyRemoveBreakAfter) {
			var	token, left, right, parentBBCode, bbcode,
				removedBreakEnd, removedBreakBefore, remove;
			var childrenLength = children.length;
// TODO: this function really needs tidying up
			if (parent) {
				parentBBCode = base.bbcodes[parent.name];
			}

			var i = childrenLength;
			while (i--) {
				if (!(token = children[i])) {
					continue;
				}

				if (token.type === TokenType.NEWLINE) {
					left   = i > 0 ? children[i - 1] : null;
					right  = i < childrenLength - 1 ? children[i + 1] : null;
					remove = false;

					// Handle the start and end new lines
					// e.g. [tag]\n and \n[/tag]
					if (!onlyRemoveBreakAfter && parentBBCode &&
						parentBBCode.isSelfClosing !== true) {
						// First child of parent so must be opening line break
						// (breakStartBlock, breakStart) e.g. [tag]\n
						if (!left) {
							if (parentBBCode.isInline === false &&
								base.opts.breakStartBlock &&
								parentBBCode.breakStart !== false) {
								remove = true;
							}

							if (parentBBCode.breakStart) {
								remove = true;
							}
						// Last child of parent so must be end line break
						// (breakEndBlock, breakEnd)
						// e.g. \n[/tag]
						// remove last line break (breakEndBlock, breakEnd)
						} else if (!removedBreakEnd && !right) {
							if (parentBBCode.isInline === false &&
								base.opts.breakEndBlock &&
								parentBBCode.breakEnd !== false) {
								remove = true;
							}

							if (parentBBCode.breakEnd) {
								remove = true;
							}

							removedBreakEnd = remove;
						}
					}

					if (left && left.type === TokenType.OPEN) {
						if ((bbcode = base.bbcodes[left.name])) {
							if (!onlyRemoveBreakAfter) {
								if (bbcode.isInline === false &&
									base.opts.breakAfterBlock &&
									bbcode.breakAfter !== false) {
									remove = true;
								}

								if (bbcode.breakAfter) {
									remove = true;
								}
							} else if (bbcode.isInline === false) {
								remove = true;
							}
						}
					}

					if (!onlyRemoveBreakAfter && !removedBreakBefore &&
						right && right.type === TokenType.OPEN) {

						if ((bbcode = base.bbcodes[right.name])) {
							if (bbcode.isInline === false &&
								base.opts.breakBeforeBlock &&
								bbcode.breakBefore !== false) {
								remove = true;
							}

							if (bbcode.breakBefore) {
								remove = true;
							}

							removedBreakBefore = remove;

							if (remove) {
								children.splice(i, 1);
								continue;
							}
						}
					}

					if (remove) {
						children.splice(i, 1);
					}

					// reset double removedBreakBefore removal protection.
					// This is needed for cases like \n\n[\tag] where
					// only 1 \n should be removed but without this they both
					// would be.
					removedBreakBefore = false;
				} else if (token.type === TokenType.OPEN) {
					normaliseNewLines(token.children, token,
						onlyRemoveBreakAfter);
				}
			}
		};

		/**
		 * Fixes any invalid nesting.
		 *
		 * If it is a block level element inside 1 or more inline elements
		 * then those inline elements will be split at the point where the
		 * block level is and the block level element placed between the split
		 * parts. i.e.
		 *     [inline]A[blocklevel]B[/blocklevel]C[/inline]
		 * Will become:
		 *     [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline]
		 *
		 * @param {Array} children
		 * @param {Array} [parents] Null if there is no parents
		 * @param {Array} [insideInline] Boolean, if inside an inline element
		 * @param {Array} [rootArr] Root array if there is one
		 * @return {Array}
		 * @private
		 */
		fixNesting = function (children, parents, insideInline, rootArr) {
			var	token, i, parent, parentIndex, parentParentChildren, right;

			var isInline = function (token) {
				var bbcode = base.bbcodes[token.name];

				return !bbcode || bbcode.isInline !== false;
			};

			parents = parents || [];
			rootArr = rootArr || children;

			// This must check the length each time as it can change when
			// tokens are moved to fix the nesting.
			for (i = 0; i < children.length; i++) {
				if (!(token = children[i]) || token.type !== TokenType.OPEN) {
					continue;
				}

				if (!isInline(token) && insideInline) {
					// if this is a blocklevel element inside an inline one then
					// split the parent at the block level element
					parent = last(parents);
					right  = parent.splitAt(token);

					parentParentChildren = parents.length > 1 ?
						parents[parents.length - 2].children : rootArr;

					parentIndex = $.inArray(parent, parentParentChildren);
					if (parentIndex > -1) {
						// remove the block level token from the right side of
						// the split inline element
						right.children.splice(
							$.inArray(token, right.children), 1);

						// insert the block level token and the right side after
						// the left side of the inline token
						parentParentChildren.splice(
							parentIndex + 1, 0, token, right
						);

						// return to parents loop as the
						// children have now increased
						return;
					}

				}

				parents.push(token);

				fixNesting(
					token.children,
					parents,
					insideInline || isInline(token),
					rootArr
				);

				parents.pop(token);
			}
		};

		/**
		 * Fixes any invalid children.
		 *
		 * If it is an element which isn't allowed as a child of it's parent
		 * then it will be converted to content of the parent element. i.e.
		 *     [code]Code [b]only[/b] allows text.[/code]
		 * Will become:
		 *     <code>Code [b]only[/b] allows text.</code>
		 * Instead of:
		 *     <code>Code <b>only</b> allows text.</code>
		 *
		 * @param {Array} children
		 * @param {Array} [parent] Null if there is no parents
		 * @private
		 */
		fixChildren = function (children, parent) {
			var	token, args;

			var i = children.length;
			while (i--) {
				if (!(token = children[i])) {
					continue;
				}

				if (!isChildAllowed(parent, token)) {
					// if it is not then convert it to text and see if it
					// is allowed
					token.name = null;
					token.type = TokenType.CONTENT;

					if (isChildAllowed(parent, token)) {
						args = [i + 1, 0].concat(token.children);

						if (token.closing) {
							token.closing.name = null;
							token.closing.type = TokenType.CONTENT;
							args.push(token.closing);
						}

						i += args.length - 1;
						Array.prototype.splice.apply(children, args);
					} else {
						parent.children.splice(i, 1);
					}
				}

				if (token.type === TokenType.OPEN) {
					fixChildren(token.children, token);
				}
			}
		};

		/**
		 * Removes any empty BBCodes which are not allowed to be empty.
		 *
		 * @param {Array} tokens
		 * @private
		 */
		removeEmpty = function (tokens) {
			var	token, bbcode;

			/**
			 * Checks if all children are whitespace or not
			 * @private
			 */
			var isTokenWhiteSpace = function (children) {
				var j = children.length;

				while (j--) {
					var type = children[j].type;

					if (type === TokenType.OPEN || type === TokenType.CLOSE) {
						return false;
					}

					if (type === TokenType.CONTENT &&
						/\S|\u00A0/.test(children[j].val)) {
						return false;
					}
				}

				return true;
			};

			var i = tokens.length;
			while (i--) {
				// So skip anything that isn't a tag since only tags can be
				// empty, content can't
				if (!(token = tokens[i]) || token.type !== TokenType.OPEN) {
					continue;
				}

				bbcode = base.bbcodes[token.name];

				// Remove any empty children of this tag first so that if they
				// are all removed this one doesn't think it's not empty.
				removeEmpty(token.children);

				if (isTokenWhiteSpace(token.children) && bbcode &&
					!bbcode.isSelfClosing && !bbcode.allowsEmpty) {
					tokens.splice.apply(
						tokens,
						$.merge([i, 1], token.children)
					);
				}
			}
		};

		/**
		 * Converts a BBCode string to HTML
		 *
		 * @param {String} str
		 * @param {Bool}   preserveNewLines If to preserve all new lines, not
		 *                                  strip any based on the passed
		 *                                  formatting options
		 * @return {String}
		 * @memberOf jQuery.sceditor.BBCodeParser.prototype
		 */
		base.toHTML = function (str, preserveNewLines) {
			return convertToHTML(base.parse(str, preserveNewLines), true);
		};

		/**
		 * @private
		 */
		convertToHTML = function (tokens, isRoot) {
			var	undef, token, bbcode, content, html, needsBlockWrap,
				blockWrapOpen, isInline, lastChild,
				ret = [];

			isInline = function (bbcode) {
				return (!bbcode || (bbcode.isHtmlInline !== undef ?
					bbcode.isHtmlInline : bbcode.isInline)) !== false;
			};

			while (tokens.length > 0) {
				if (!(token = tokens.shift())) {
					continue;
				}

				if (token.type === TokenType.OPEN) {
					lastChild      =
						token.children[token.children.length - 1] || {};
					bbcode         = base.bbcodes[token.name];
					needsBlockWrap = isRoot && isInline(bbcode);
					content        = convertToHTML(token.children, false);

					if (bbcode && bbcode.html) {
						// Only add a line break to the end if this is
						// blocklevel and the last child wasn't block-level
						if (!isInline(bbcode) &&
							isInline(base.bbcodes[lastChild.name]) &&
							!bbcode.isPreFormatted &&
							!bbcode.skipLastLineBreak) {
							// Add placeholder br to end of block level elements
							// in all browsers apart from IE < 9 which handle
							// new lines differently and doesn't need one.
							if (!IE_BR_FIX) {
								content += '<br />';
							}
						}

						if (!$.isFunction(bbcode.html)) {
							token.attrs['0'] = content;
							html = sceditorPlugins.bbcode.formatBBCodeString(
								bbcode.html,
								token.attrs
							);
						} else {
							html = bbcode.html.call(
								base,
								token,
								token.attrs,
								content
							);
						}
					} else {
						html = token.val + content +
							(token.closing ? token.closing.val : '');
					}
				} else if (token.type === TokenType.NEWLINE) {
					if (!isRoot) {
						ret.push('<br />');
						continue;
					}

					// If not already in a block wrap then start a new block
					if (!blockWrapOpen) {
						ret.push('<div>');

						// If it's an empty DIV and compatibility mode is below
						// IE8 then we must add a non-breaking space to the div
						// otherwise the div will be collapsed. Adding a BR
						// works but when you press enter to make a newline it
						// suddenly goes back to the normal IE div behavior and
						// creates two lines, one for the newline and one for
						// the BR. I'm sure there must be a better fix but I've
						// yet to find one.
						// Cannot do zoom: 1; or set a height on the div to fix
						// it as that causes resize handles to be added to the
						// div when it's clicked on.
						if (IE_VER < 8 || (document.documentMode &&
							document.documentMode < 8)) {
							ret.push('\u00a0');
						}
					}

					// Putting BR in a div in IE causes it
					// to do a double line break.
					if (!IE_BR_FIX) {
						ret.push('<br />');
					}

					// Normally the div acts as a line-break with by moving
					// whatever comes after onto a new line.
					// If this is the last token, add an extra line-break so it
					// shows as there will be nothing after it.
					if (!tokens.length) {
						ret.push('<br />');
					}

					ret.push('</div>\n');
					blockWrapOpen = false;
					continue;
				// content
				} else {
					needsBlockWrap = isRoot;
					html           = escapeEntities(token.val, true);
				}

				if (needsBlockWrap && !blockWrapOpen) {
					ret.push('<div>');
					blockWrapOpen = true;
				} else if (!needsBlockWrap && blockWrapOpen) {
					ret.push('</div>\n');
					blockWrapOpen = false;
				}

				ret.push(html);
			}

			if (blockWrapOpen) {
				ret.push('</div>\n');
			}

			return ret.join('');
		};

		/**
		 * Takes a BBCode string, parses it then converts it back to BBCode.
		 *
		 * This will auto fix the BBCode and format it with the specified
		 * options.
		 *
		 * @param {String} str
		 * @param {Bool} preserveNewLines If to preserve all new lines, not
		 *                                strip any based on the passed
		 *                                formatting options
		 * @return {String}
		 * @memberOf jQuery.sceditor.BBCodeParser.prototype
		 */
		base.toBBCode = function (str, preserveNewLines) {
			return convertToBBCode(base.parse(str, preserveNewLines));
		};

		/**
		 * Converts parsed tokens back into BBCode with the
		 * formatting specified in the options and with any
		 * fixes specified.
		 *
		 * @param  {Array} toks Array of parsed tokens from base.parse()
		 * @return {String}
		 * @private
		 */
		convertToBBCode = function (toks) {
			var	token, attr, bbcode, isBlock, isSelfClosing, quoteType,
				breakBefore, breakStart, breakEnd, breakAfter,
				// Create an array of strings which are joined together
				// before being returned as this is faster in slow browsers.
				// (Old versions of IE).
				ret = [];

			while (toks.length > 0) {
				if (!(token = toks.shift())) {
					continue;
				}
// TODO: tidy this
				bbcode        = base.bbcodes[token.name];
				isBlock       = !(!bbcode || bbcode.isInline !== false);
				isSelfClosing = bbcode && bbcode.isSelfClosing;

				breakBefore = (isBlock && base.opts.breakBeforeBlock &&
						bbcode.breakBefore !== false) ||
					(bbcode && bbcode.breakBefore);

				breakStart = (isBlock && !isSelfClosing &&
						base.opts.breakStartBlock &&
						bbcode.breakStart !== false) ||
					(bbcode && bbcode.breakStart);

				breakEnd = (isBlock && base.opts.breakEndBlock &&
						bbcode.breakEnd !== false) ||
					(bbcode && bbcode.breakEnd);

				breakAfter = (isBlock && base.opts.breakAfterBlock &&
						bbcode.breakAfter !== false) ||
					(bbcode && bbcode.breakAfter);

				quoteType = (bbcode ? bbcode.quoteType : null) ||
					base.opts.quoteType || BBCodeParser.QuoteType.auto;

				if (!bbcode && token.type === TokenType.OPEN) {
					ret.push(token.val);

					if (token.children) {
						ret.push(convertToBBCode(token.children));
					}

					if (token.closing) {
						ret.push(token.closing.val);
					}
				} else if (token.type === TokenType.OPEN) {
					if (breakBefore) {
						ret.push('\n');
					}

					// Convert the tag and it's attributes to BBCode
					ret.push('[' + token.name);
					if (token.attrs) {
						if (token.attrs.defaultattr) {
							ret.push('=', quote(
								token.attrs.defaultattr,
								quoteType,
								'defaultattr'
							));

							delete token.attrs.defaultattr;
						}

						for (attr in token.attrs) {
							if (token.attrs.hasOwnProperty(attr)) {
								ret.push(' ', attr, '=',
									quote(token.attrs[attr], quoteType, attr));
							}
						}
					}
					ret.push(']');

					if (breakStart) {
						ret.push('\n');
					}

					// Convert the tags children to BBCode
					if (token.children) {
						ret.push(convertToBBCode(token.children));
					}

					// add closing tag if not self closing
					if (!isSelfClosing && !bbcode.excludeClosing) {
						if (breakEnd) {
							ret.push('\n');
						}

						ret.push('[/' + token.name + ']');
					}

					if (breakAfter) {
						ret.push('\n');
					}

					// preserve whatever was recognized as the
					// closing tag if it is a self closing tag
					if (token.closing && isSelfClosing) {
						ret.push(token.closing.val);
					}
				} else {
					ret.push(token.val);
				}
			}

			return ret.join('');
		};

		/**
		 * Quotes an attribute
		 *
		 * @param {String} str
		 * @param {BBCodeParser.QuoteType} quoteType
		 * @param {String} name
		 * @return {String}
		 * @private
		 */
		quote = function (str, quoteType, name) {
			var	QuoteTypes  = BBCodeParser.QuoteType,
				needsQuotes = /\s|=/.test(str);

			if ($.isFunction(quoteType)) {
				return quoteType(str, name);
			}

			if (quoteType === QuoteTypes.never ||
				(quoteType === QuoteTypes.auto && !needsQuotes)) {
				return str;
			}

			return '"' + str.replace('\\', '\\\\').replace('"', '\\"') + '"';
		};

		/**
		 * Returns the last element of an array or null
		 *
		 * @param {Array} arr
		 * @return {Object} Last element
		 * @private
		 */
		last = function (arr) {
			if (arr.length) {
				return arr[arr.length - 1];
			}

			return null;
		};

		/**
		 * Converts a string to lowercase.
		 *
		 * @param {String} str
		 * @return {String} Lowercase version of str
		 * @private
		 */
		lower = function (str) {
			return str.toLowerCase();
		};

		init();
	};

	/**
	 * Quote type
	 * @type {Object}
	 * @class QuoteType
	 * @name jQuery.sceditor.BBCodeParser.QuoteType
	 * @since v1.4.0
	 */
	BBCodeParser.QuoteType = {
		/** @lends jQuery.sceditor.BBCodeParser.QuoteType */
		/**
		 * Always quote the attribute value
		 * @type {Number}
		 */
		always: 1,

		/**
		 * Never quote the attributes value
		 * @type {Number}
		 */
		never: 2,

		/**
		 * Only quote the attributes value when it contains spaces to equals
		 * @type {Number}
		 */
		auto: 3
	};

	/**
	 * Default BBCode parser options
	 * @type {Object}
	 */
	BBCodeParser.defaults = {
		/**
		 * If to add a new line before block level elements
		 *
		 * @type {Boolean}
		 */
		breakBeforeBlock: false,

		/**
		 * If to add a new line after the start of block level elements
		 *
		 * @type {Boolean}
		 */
		breakStartBlock: false,

		/**
		 * If to add a new line before the end of block level elements
		 *
		 * @type {Boolean}
		 */
		breakEndBlock: false,

		/**
		 * If to add a new line after block level elements
		 *
		 * @type {Boolean}
		 */
		breakAfterBlock: true,

		/**
		 * If to remove empty tags
		 *
		 * @type {Boolean}
		 */
		removeEmptyTags: true,

		/**
		 * If to fix invalid nesting,
		 * i.e. block level elements inside inline elements.
		 *
		 * @type {Boolean}
		 */
		fixInvalidNesting: true,

		/**
		 * If to fix invalid children.
		 * i.e. A tag which is inside a parent that doesn't
		 * allow that type of tag.
		 *
		 * @type {Boolean}
		 */
		fixInvalidChildren: true,

		/**
		 * Attribute quote type
		 *
		 * @type {BBCodeParser.QuoteType}
		 * @since 1.4.1
		 */
		quoteType: BBCodeParser.QuoteType.auto
	};

	/**
	 * Deprecated, use sceditorPlugins.bbcode
	 *
	 * @class sceditorBBCodePlugin
	 * @name jQuery.sceditor.sceditorBBCodePlugin
	 * @deprecated
	 */
	$.sceditorBBCodePlugin =
	/**
	 * BBCode plugin for SCEditor
	 *
	 * @class bbcode
	 * @name jQuery.sceditor.plugins.bbcode
	 * @since 1.4.1
	 */
	sceditorPlugins.bbcode = function () {
		var base = this;

		/**
		 * Private methods
		 * @private
		 */
		var	buildBbcodeCache,
			handleStyles,
			handleTags,
			removeFirstLastDiv;

		base.bbcodes     = sceditorPlugins.bbcode.bbcodes;
		base.stripQuotes = _stripQuotes;

		/**
		 * cache of all the tags pointing to their bbcodes to enable
		 * faster lookup of which bbcode a tag should have
		 * @private
		 */
		var tagsToBBCodes = {};

		/**
		 * Same as tagsToBBCodes but instead of HTML tags it's styles
		 * @private
		 */
		var stylesToBBCodes = {};

		/**
		 * Allowed children of specific HTML tags. Empty array if no
		 * children other than text nodes are allowed
		 * @private
		 */
		var validChildren = {
			ul: ['li', 'ol', 'ul'],
			ol: ['li', 'ol', 'ul'],
			table: ['tr'],
			tr: ['td', 'th'],
			code: ['br', 'p', 'div']
		};

		/**
		 * Initializer
		 * @private
		 */
		base.init = function () {
			base.opts = this.opts;

			// build the BBCode cache
			buildBbcodeCache();

			this.commands = $.extend(
				true, {}, defaultCommandsOverrides, this.commands
			);

			// Add BBCode helper methods
			this.toBBCode   = base.signalToSource;
			this.fromBBCode = base.signalToWysiwyg;
		};

		/**
		 * Populates tagsToBBCodes and stylesToBBCodes to enable faster lookups
		 *
		 * @private
		 */
		buildBbcodeCache = function () {
			$.each(base.bbcodes, function (bbcode) {
				var	isBlock,
					tags   = base.bbcodes[bbcode].tags,
					styles = base.bbcodes[bbcode].styles;

				if (tags) {
					$.each(tags, function (tag, values) {
						isBlock = base.bbcodes[bbcode].isInline === false;

						tagsToBBCodes[tag] = tagsToBBCodes[tag] || {};

						tagsToBBCodes[tag][isBlock] =
							tagsToBBCodes[tag][isBlock] || {};

						tagsToBBCodes[tag][isBlock][bbcode] = values;
					});
				}

				if (styles) {
					$.each(styles, function (style, values) {
						isBlock = base.bbcodes[bbcode].isInline === false;

						stylesToBBCodes[isBlock] =
							stylesToBBCodes[isBlock] || {};

						stylesToBBCodes[isBlock][style] =
							stylesToBBCodes[isBlock][style] || {};

						stylesToBBCodes[isBlock][style][bbcode] = values;
					});
				}
			});
		};

		/**
		 * Checks if any bbcode styles match the elements styles
		 *
		 * @return string Content with any matching
		 *                bbcode tags wrapped around it.
		 * @private
		 */
		handleStyles = function ($element, content, blockLevel) {
			var	styleValue, format,
				getStyle = SCEditor.dom.getStyle;

			// convert blockLevel to boolean
			blockLevel = !!blockLevel;

			if (!stylesToBBCodes[blockLevel]) {
				return content;
			}

			$.each(stylesToBBCodes[blockLevel], function (property, bbcodes) {
				styleValue = getStyle($element[0], property);

				// if the parent has the same style use that instead of this one
				// so you don't end up with [i]parent[i]child[/i][/i]
				if (!styleValue ||
					getStyle($element.parent()[0], property) === styleValue) {
					return;
				}

				$.each(bbcodes, function (bbcode, values) {
					if (!values ||
						$.inArray(styleValue.toString(), values) > -1) {
						format = base.bbcodes[bbcode].format;

						if ($.isFunction(format)) {
							content = format.call(base, $element, content);
						} else {
							content = _formatString(format, content);
						}
					}
				});
			});

			return content;
		};

		/**
		 * Handles a HTML tag and finds any matching bbcodes
		 *
		 * @param {jQuery} $element The element to convert
		 * @param {String} content  The Tags text content
		 * @param {Bool} blockLevel If to convert block level tags
		 * @return {String} Content with any matching bbcode tags
		 *                  wrapped around it.
		 * @private
		 */
		handleTags = function ($element, content, blockLevel) {
			var	convertBBCode, format,
				element = $element[0],
				tag     = element.nodeName.toLowerCase();

			// convert blockLevel to boolean
			blockLevel = !!blockLevel;

			if (tagsToBBCodes[tag] && tagsToBBCodes[tag][blockLevel]) {
				// loop all bbcodes for this tag
				$.each(tagsToBBCodes[tag][blockLevel], function (
					bbcode, bbcodeAttribs) {
					// if the bbcode requires any attributes then check this has
					// all needed
					if (bbcodeAttribs) {
						convertBBCode = false;

						// loop all the bbcode attribs
						$.each(bbcodeAttribs, function (attrib, values) {
							// Skip if the element doesn't have the attibue or
							// the attribute doesn't match one of the require
							// values
							if (!$element.attr(attrib) || (values &&
								$.inArray($element.attr(attrib), values) < 0)) {
								return;
							}

							// break this loop as we have matched this bbcode
							convertBBCode = true;
							return false;
						});

						if (!convertBBCode) {
							return;
						}
					}

					format = base.bbcodes[bbcode].format;

					if ($.isFunction(format)) {
						content = format.call(base, $element, content);
					} else {
						content = _formatString(format, content);
					}
				});
			}

			var isInline = SCEditor.dom.isInline;
			if (blockLevel && (!isInline(element, true) || tag === 'br')) {
				var	isLastBlockChild, parent, parentLastChild,
					previousSibling = element.previousSibling;

				// Skips selection makers and ignored elements
				// Skip empty inline elements
				while (previousSibling &&
						previousSibling.nodeType === 1 &&
						!$(previousSibling).is('br') &&
						isInline(previousSibling, true) &&
						!previousSibling.firstChild) {
					previousSibling = previousSibling.previousSibling;
				}

				// If it's the last block of an inline that is the last
				// child of a block then it shouldn't cause a line break
				// except in IE < 11
				// <block><inline><br></inline></block>
				do {
					parent          = element.parentNode;
					parentLastChild = parent.lastChild;

					isLastBlockChild = parentLastChild === element;
					element = parent;
				} while (parent && isLastBlockChild && isInline(parent, true));

				// If this block is:
				//	* Not the last child of a block level element
				//	* Is a <li> tag (lists are blocks)
				//	* Is IE < 11 and the tag is BR. IE < 11 never collapses BR
				//	  tags.
				if (!isLastBlockChild || tag === 'li' ||
					(tag === 'br' && IE_BR_FIX)) {
					content += '\n';
				}

				// Check for:
				// <block>text<block>text</block></block>
				//
				// The second opening <block> opening tag should cause a
				// line break because the previous sibing is inline.
				if (tag !== 'br' && previousSibling &&
					!$(previousSibling).is('br') &&
					isInline(previousSibling, true)) {
					content = '\n' + content;
				}
			}

			return content;
		};

		/**
		 * Converts HTML to BBCode
		 *
		 * @param {String}	html   Html string, this function ignores this,
		 *                         it works off domBody
		 * @param {jQuery}	$body  Editors dom body object to convert
		 * @return {String} BBCode which has been converted from HTML
		 * @memberOf jQuery.plugins.bbcode.prototype
		 */
		base.signalToSource = function (html, $body) {
			var	$tmpContainer, bbcode,
				parser = new BBCodeParser(base.opts.parserOptions);

			if (!$body) {
				if (typeof html === 'string') {
					$tmpContainer = $('<div />')
						.css('visibility', 'hidden')
						.appendTo(document.body)
						.html(html);

					$body = $tmpContainer;
				} else {
					$body = $(html);
				}
			}

			if (!$body || !$body.jquery) {
				return '';
			}

			SCEditor.dom.removeWhiteSpace($body[0]);

			// Remove all the stuff that is meant to be ignored
			$('.sceditor-ignore', $body).remove();

			bbcode = base.elementToBbcode($body);

			if ($tmpContainer) {
				$tmpContainer.remove();
			}

			bbcode = parser.toBBCode(bbcode, true);

			if (base.opts.bbcodeTrim) {
				bbcode = $.trim(bbcode);
			}

			return bbcode;
		};

		/**
		 * Converts a HTML dom element to BBCode starting from
		 * the innermost element and working backwards
		 *
		 * @private
		 * @param {jQuery}	$element The element to convert to BBCode
		 * @return {string} BBCode
		 * @memberOf jQuery.plugins.bbcode.prototype
		 */
		base.elementToBbcode = function ($element) {
			var toBBCode = function (node, vChildren) {
				var ret = '';
// TODO: Move to BBCode class?
				SCEditor.dom.traverse(node, function (node) {
					var	$node        = $(node),
						curTag       = '',
						nodeType     = node.nodeType,
						tag          = node.nodeName.toLowerCase(),
						vChild       = validChildren[tag],
						firstChild   = node.firstChild,
						isValidChild = true;

					if (typeof vChildren === 'object') {
						isValidChild = $.inArray(tag, vChildren) > -1;

						// Emoticons should always be converted
						if ($node.is('img') &&
							$node.data('sceditor-emoticon')) {
							isValidChild = true;
						}

						// if this tag is one of the parents allowed children
						// then set this tags allowed children to whatever it
						// allows, otherwise set to what the parent allows
						if (!isValidChild) {
							vChild = vChildren;
						}
					}

					// 3 = text and 1 = element
					if (nodeType !== 3 && nodeType !== 1) {
						return;
					}

					if (nodeType === 1) {
						// skip empty nlf elements (new lines automatically
						// added after block level elements like quotes)
						if ($node.hasClass('sceditor-nlf')) {
							if (!firstChild || (!IE_BR_FIX &&
								node.childNodes.length === 1 &&
								/br/i.test(firstChild.nodeName))) {
								return;
							}
						}

						// don't loop inside iframes
						if (tag !== 'iframe') {
							curTag = toBBCode(node, vChild);
						}

// TODO: isValidChild is no longer needed. Should use valid children bbcodes
// instead by creating BBCode tokens like the parser.
						if (isValidChild) {
							// code tags should skip most styles
							if (tag !== 'code') {
								// handle inline bbcodes
								curTag = handleStyles($node, curTag);
								curTag = handleTags($node, curTag);

								// handle blocklevel bbcodes
								curTag = handleStyles($node, curTag, true);
							}

							ret += handleTags($node, curTag, true);
						} else {
							ret += curTag;
						}
					} else {
						ret += node.nodeValue;
					}
				}, false, true);

				return ret;
			};

			return toBBCode($element[0]);
		};

		/**
		 * Converts BBCode to HTML
		 *
		 * @param {String} text
		 * @param {Bool} asFragment
		 * @return {String} HTML
		 * @memberOf jQuery.plugins.bbcode.prototype
		 */
		base.signalToWysiwyg = function (text, asFragment) {
			var	parser = new BBCodeParser(base.opts.parserOptions),
				html   = parser.toHTML(base.opts.bbcodeTrim ?
					$.trim(text) : text);

			return asFragment ? removeFirstLastDiv(html) : html;
		};

		/**
		 * Removes the first and last divs from the HTML.
		 *
		 * This is needed for pasting
		 * @param  {String} html
		 * @return {String}
		 * @private
		 */
		removeFirstLastDiv = function (html) {
			var	node, next, removeDiv,
				$output = $('<div />').hide().appendTo(document.body),
				output  = $output[0];

			removeDiv = function (node, isFirst) {
				// Don't remove divs that have styling
				if (SCEditor.dom.hasStyling(node)) {
					return;
				}

				if (IE_BR_FIX || (node.childNodes.length !== 1 ||
					!$(node.firstChild).is('br'))) {
					while ((next = node.firstChild)) {
						output.insertBefore(next, node);
					}
				}

				if (isFirst) {
					var lastChild = output.lastChild;

					if (node !== lastChild && $(lastChild).is('div') &&
						node.nextSibling === lastChild) {
						output.insertBefore(document.createElement('br'), node);
					}
				}

				output.removeChild(node);
			};

			output.innerHTML = html.replace(/<\/div>\n/g, '</div>');

			if ((node = output.firstChild) && $(node).is('div')) {
				removeDiv(node, true);
			}

			if ((node = output.lastChild) && $(node).is('div')) {
				removeDiv(node);
			}

			output = output.innerHTML;
			$output.remove();

			return output;
		};
	};



	/**
	 * Formats a string replacing {name} with the values of
	 * obj.name properties.
	 *
	 * If there is no property for the specified {name} then
	 * it will be left intact.
	 *
	 * @param  {String} str
	 * @param  {Object} obj
	 * @return {String}
	 * @since 1.4.5
	 */
	sceditorPlugins.bbcode.formatBBCodeString = function (str, obj) {
		return str.replace(/\{([^}]+)\}/g, function (match, group) {
			var	undef,
				escape = true;

			if (group.charAt(0) === '!') {
				escape = false;
				group = group.substring(1);
			}

			if (group === '0') {
				escape = false;
			}

			if (obj[group] === undef) {
				return match;
			}

			return escape ?
				escapeEntities(obj[group], true) :
				obj[group];
		});
	};

	/**
	 * Converts a number 0-255 to hex.
	 *
	 * Will return 00 if number is not a valid number.
	 *
	 * @param  {Number} number
	 * @return {String}
	 * @private
	 */
	var toHex = function (number) {
		number = parseInt(number, 10);

		if (isNaN(number)) {
			return '00';
		}

		number = Math.max(0, Math.min(number, 255)).toString(16);

		return number.length < 2 ? '0' + number : number;
	};

	var _normaliseColour = function (colorStr) {
		var match;

		colorStr = colorStr || '#000';

		// rgb(n,n,n);
		if ((match =
			colorStr.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i))) {
			return '#' +
				toHex(match[1]) +
				toHex(match[2] - 0) +
				toHex(match[3] - 0);
		}

		// expand shorthand
		if ((match = colorStr.match(/#([0-f])([0-f])([0-f])\s*?$/i))) {
			return '#' +
				match[1] + match[1] +
				match[2] + match[2] +
				match[3] + match[3];
		}

		return colorStr;
	};

	var bbcodes = {
		// START_COMMAND: Bold
		b: {
			tags: {
				b: null,
				strong: null
			},
			styles: {
				// 401 is for FF 3.5
				'font-weight': ['bold', 'bolder', '401', '700', '800', '900']
			},
			format: '[b]{0}[/b]',
			html: '<strong>{0}</strong>'
		},
		// END_COMMAND

		// START_COMMAND: Italic
		i: {
			tags: {
				i: null,
				em: null
			},
			styles: {
				'font-style': ['italic', 'oblique']
			},
			format: '[i]{0}[/i]',
			html: '<em>{0}</em>'
		},
		// END_COMMAND

		// START_COMMAND: Underline
		u: {
			tags: {
				u: null
			},
			styles: {
				'text-decoration': ['underline']
			},
			format: '[u]{0}[/u]',
			html: '<u>{0}</u>'
		},
		// END_COMMAND

		// START_COMMAND: Strikethrough
		s: {
			tags: {
				s: null,
				strike: null
			},
			styles: {
				'text-decoration': ['line-through']
			},
			format: '[s]{0}[/s]',
			html: '<s>{0}</s>'
		},
		// END_COMMAND

		// START_COMMAND: Subscript
		sub: {
			tags: {
				sub: null
			},
			format: '[sub]{0}[/sub]',
			html: '<sub>{0}</sub>'
		},
		// END_COMMAND

		// START_COMMAND: Superscript
		sup: {
			tags: {
				sup: null
			},
			format: '[sup]{0}[/sup]',
			html: '<sup>{0}</sup>'
		},
		// END_COMMAND

		// START_COMMAND: Font
		font: {
			tags: {
				font: {
					face: null
				}
			},
			styles: {
				'font-family': null
			},
			quoteType: BBCodeParser.QuoteType.never,
			format: function ($element, content) {
				var font;

				if (!$element.is('font') || !(font = $element.attr('face'))) {
					font = $element.css('font-family');
				}

				return '[font=' + _stripQuotes(font) + ']' +
					content + '[/font]';
			},
			html: '<font face="{defaultattr}">{0}</font>'
		},
		// END_COMMAND

		// START_COMMAND: Size
		size: {
			tags: {
				font: {
					size: null
				}
			},
			styles: {
				'font-size': null
			},
			format: function (element, content) {
				var	fontSize = element.attr('size'),
					size     = 2;

				if (!fontSize) {
					fontSize = element.css('fontSize');
				}

				// Most browsers return px value but IE returns 1-7
				if (fontSize.indexOf('px') > -1) {
					// convert size to an int
					fontSize = fontSize.replace('px', '') - 0;

					if (fontSize < 12) {
						size = 1;
					}
					if (fontSize > 15) {
						size = 3;
					}
					if (fontSize > 17) {
						size = 4;
					}
					if (fontSize > 23) {
						size = 5;
					}
					if (fontSize > 31) {
						size = 6;
					}
					if (fontSize > 47) {
						size = 7;
					}
				} else {
					size = fontSize;
				}

				return '[size=' + size + ']' + content + '[/size]';
			},
			html: '<font size="{defaultattr}">{!0}</font>'
		},
		// END_COMMAND

		// START_COMMAND: Color
		color: {
			tags: {
				font: {
					color: null
				}
			},
			styles: {
				color: null
			},
			quoteType: BBCodeParser.QuoteType.never,
			format: function ($element, content) {
				var	color;

				if (!$element.is('font') || !(color = $element.attr('color'))) {
					color = $element[0].style.color || $element.css('color');
				}

				return '[color=' + _normaliseColour(color) + ']' +
					content + '[/color]';
			},
			html: function (token, attrs, content) {
				return '<font color="' +
					escapeEntities(_normaliseColour(attrs.defaultattr), true) +
					'">' + content + '</font>';
			}
		},
		// END_COMMAND

		// START_COMMAND: Lists
		ul: {
			tags: {
				ul: null
			},
			breakStart: true,
			isInline: false,
			skipLastLineBreak: true,
			format: '[ul]{0}[/ul]',
			html: '<ul>{0}</ul>'
		},
		list: {
			breakStart: true,
			isInline: false,
			skipLastLineBreak: true,
			html: '<ul>{0}</ul>'
		},
		ol: {
			tags: {
				ol: null
			},
			breakStart: true,
			isInline: false,
			skipLastLineBreak: true,
			format: '[ol]{0}[/ol]',
			html: '<ol>{0}</ol>'
		},
		li: {
			tags: {
				li: null
			},
			isInline: false,
			closedBy: ['/ul', '/ol', '/list', '*', 'li'],
			format: '[li]{0}[/li]',
			html: '<li>{0}</li>'
		},
		'*': {
			isInline: false,
			closedBy: ['/ul', '/ol', '/list', '*', 'li'],
			html: '<li>{0}</li>'
		},
		// END_COMMAND

		// START_COMMAND: Table
		table: {
			tags: {
				table: null
			},
			isInline: false,
			isHtmlInline: true,
			skipLastLineBreak: true,
			format: '[table]{0}[/table]',
			html: '<table>{0}</table>'
		},
		tr: {
			tags: {
				tr: null
			},
			isInline: false,
			skipLastLineBreak: true,
			format: '[tr]{0}[/tr]',
			html: '<tr>{0}</tr>'
		},
		th: {
			tags: {
				th: null
			},
			allowsEmpty: true,
			isInline: false,
			format: '[th]{0}[/th]',
			html: '<th>{0}</th>'
		},
		td: {
			tags: {
				td: null
			},
			allowsEmpty: true,
			isInline: false,
			format: '[td]{0}[/td]',
			html: '<td>{0}</td>'
		},
		// END_COMMAND

		// START_COMMAND: Emoticons
		emoticon: {
			allowsEmpty: true,
			tags: {
				img: {
					src: null,
					'data-sceditor-emoticon': null
				}
			},
			format: function ($elm, content) {
				return $elm.data('sceditor-emoticon') + content;
			},
			html: '{0}'
		},
		// END_COMMAND

		// START_COMMAND: Horizontal Rule
		hr: {
			tags: {
				hr: null
			},
			allowsEmpty: true,
			isSelfClosing: true,
			isInline: false,
			format: '[hr]{0}',
			html: '<hr />'
		},
		// END_COMMAND

		// START_COMMAND: Image
		img: {
			allowsEmpty: true,
			tags: {
				img: {
					src: null
				}
			},
			allowedChildren: ['#'],
			quoteType: BBCodeParser.QuoteType.never,
			format: function ($element, content) {
				var	width, height,
					attribs   = '',
					element   = $element[0],
					style     = function (name) {
						return element.style ? element.style[name] : null;
					};

				// check if this is an emoticon image
				if ($element.attr('data-sceditor-emoticon')) {
					return content;
				}

				width = $element.attr('width') || style('width');
				height = $element.attr('height') || style('height');

				// only add width and height if one is specified
				if ((element.complete && (width || height)) ||
					(width && height)) {
					attribs = '=' + $element.width() + 'x' + $element.height();
				}

				return '[img' + attribs + ']' + $element.attr('src') + '[/img]';
			},
			html: function (token, attrs, content) {
				var	undef, width, height, match,
					attribs = '';

				// handle [img width=340 height=240]url[/img]
				width  = attrs.width;
				height = attrs.height;

				// handle [img=340x240]url[/img]
				if (attrs.defaultattr) {
					match = attrs.defaultattr.split(/x/i);

					width  = match[0];
					height = (match.length === 2 ? match[1] : match[0]);
				}

				if (width !== undef) {
					attribs += ' width="' + escapeEntities(width, true) + '"';
				}

				if (height !== undef) {
					attribs += ' height="' + escapeEntities(height, true) + '"';
				}

				return '<img' + attribs +
					' src="' + escapeUriScheme(content) + '" />';
			}
		},
		// END_COMMAND

		// START_COMMAND: URL
		url: {
			allowsEmpty: true,
			tags: {
				a: {
					href: null
				}
			},
			quoteType: BBCodeParser.QuoteType.never,
			format: function (element, content) {
				var url = element.attr('href');

				// make sure this link is not an e-mail,
				// if it is return e-mail BBCode
				if (url.substr(0, 7) === 'mailto:') {
					return '[email="' + url.substr(7) + '"]' +
						content + '[/email]';
				}

				return '[url=' + url + ']' + content + '[/url]';
			},
			html: function (token, attrs, content) {
				attrs.defaultattr =
					escapeEntities(attrs.defaultattr, true) || content;

				return '<a href="' + escapeUriScheme(attrs.defaultattr) +
					'">' + content + '</a>';
			}
		},
		// END_COMMAND

		// START_COMMAND: E-mail
		email: {
			quoteType: BBCodeParser.QuoteType.never,
			html: function (token, attrs, content) {
				return '<a href="mailto:' +
					(escapeEntities(attrs.defaultattr, true) || content) +
					'">' + content + '</a>';
			}
		},
		// END_COMMAND

		// START_COMMAND: Quote
		quote: {
			tags: {
				blockquote: null
			},
			isInline: false,
			quoteType: BBCodeParser.QuoteType.never,
			format: function (element, content) {
				var	author = '';
				var $elm  = $(element);
				var $cite = $elm.children('cite').first();

				if ($cite.length === 1 || $elm.data('author')) {
					author = $cite.text() || $elm.data('author');

					$elm.data('author', author);
					$cite.remove();

					content	= this.elementToBbcode($(element));
					author  = '=' + author.replace(/(^\s+|\s+$)/g, '');

					$elm.prepend($cite);
				}

				return '[quote' + author + ']' + content + '[/quote]';
			},
			html: function (token, attrs, content) {
				if (attrs.defaultattr) {
					content = '<cite>' + escapeEntities(attrs.defaultattr) +
						'</cite>' + content;
				}

				return '<blockquote>' + content + '</blockquote>';
			}
		},
		// END_COMMAND

		// START_COMMAND: Code
		code: {
			tags: {
				code: null
			},
			isInline: false,
			allowedChildren: ['#', '#newline'],
			format: '[code]{0}[/code]',
			html: '<code>{0}</code>'
		},
		// END_COMMAND


		// START_COMMAND: Left
		left: {
			styles: {
				'text-align': [
					'left',
					'-webkit-left',
					'-moz-left',
					'-khtml-left'
				]
			},
			isInline: false,
			format: '[left]{0}[/left]',
			html: '<div align="left">{0}</div>'
		},
		// END_COMMAND

		// START_COMMAND: Centre
		center: {
			styles: {
				'text-align': [
					'center',
					'-webkit-center',
					'-moz-center',
					'-khtml-center'
				]
			},
			isInline: false,
			format: '[center]{0}[/center]',
			html: '<div align="center">{0}</div>'
		},
		// END_COMMAND

		// START_COMMAND: Right
		right: {
			styles: {
				'text-align': [
					'right',
					'-webkit-right',
					'-moz-right',
					'-khtml-right'
				]
			},
			isInline: false,
			format: '[right]{0}[/right]',
			html: '<div align="right">{0}</div>'
		},
		// END_COMMAND

		// START_COMMAND: Justify
		justify: {
			styles: {
				'text-align': [
					'justify',
					'-webkit-justify',
					'-moz-justify',
					'-khtml-justify'
				]
			},
			isInline: false,
			format: '[justify]{0}[/justify]',
			html: '<div align="justify">{0}</div>'
		},
		// END_COMMAND

		// START_COMMAND: YouTube
		youtube: {
			allowsEmpty: true,
			tags: {
				iframe: {
					'data-youtube-id': null
				}
			},
			format: function (element, content) {
				element = element.attr('data-youtube-id');

				return element ? '[youtube]' + element + '[/youtube]' : content;
			},
			html: '<iframe width="560" height="315" frameborder="0" ' +
				'src="https://www.youtube.com/embed/{0}?wmode=opaque" ' +
				'data-youtube-id="{0}" allowfullscreen></iframe>'
		},
		// END_COMMAND


		// START_COMMAND: Rtl
		rtl: {
			styles: {
				direction: ['rtl']
			},
			format: '[rtl]{0}[/rtl]',
			html: '<div style="direction: rtl">{0}</div>'
		},
		// END_COMMAND

		// START_COMMAND: Ltr
		ltr: {
			styles: {
				direction: ['ltr']
			},
			format: '[ltr]{0}[/ltr]',
			html: '<div style="direction: ltr">{0}</div>'
		},
		// END_COMMAND

		// this is here so that commands above can be removed
		// without having to remove the , after the last one.
		// Needed for IE.
		ignore: {}
	};

	/**
	 * Static BBCode helper class
	 * @class command
	 * @name jQuery.plugins.bbcode.bbcode
	 */
	sceditorPlugins.bbcode.bbcode =
	/** @lends jQuery.plugins.bbcode.bbcode */
	{
		/**
		 * Gets a BBCode
		 *
		 * @param {String} name
		 * @return {Object|null}
		 * @since v1.3.5
		 */
		get: function (name) {
			return bbcodes[name] || null;
		},

		/**
		 * <p>Adds a BBCode to the parser or updates an existing
		 * BBCode if a BBCode with the specified name already exists.</p>
		 *
		 * @param {String} name
		 * @param {Object} bbcode
		 * @return {this|false} Returns false if name or bbcode is false
		 * @since v1.3.5
		 */
		set: function (name, bbcode) {
			if (!name || !bbcode) {
				return false;
			}

			// merge any existing command properties
			bbcode = $.extend(bbcodes[name] || {}, bbcode);

			bbcode.remove = function () {
				delete bbcodes[name];
			};

			bbcodes[name] = bbcode;

			return this;
		},

		/**
		 * Renames a BBCode
		 *
		 * This does not change the format or HTML handling, those must be
		 * changed manually.
		 *
		 * @param  {String} name    [description]
		 * @param  {String} newName [description]
		 * @return {this|false}
		 * @since v1.4.0
		 */
		rename: function (name, newName) {
			if (name in bbcodes) {
				bbcodes[newName] = bbcodes[name];

				delete bbcodes[name];
			} else {
				return false;
			}

			return this;
		},

		/**
		 * Removes a BBCode
		 *
		 * @param {String} name
		 * @return {this}
		 * @since v1.3.5
		 */
		remove: function (name) {
			if (name in bbcodes) {
				delete bbcodes[name];
			}

			return this;
		}
	};

	/**
	 * Deprecated, use plugins: option instead. I.e.:
	 *
	 * $('textarea').sceditor({
	 *      plugins: 'bbcode'
	 * });
	 *
	 * @deprecated
	 */
	$.fn.sceditorBBCodePlugin = function (options) {
		options = options || {};

		if ($.isPlainObject(options)) {
			options.plugins = (options.plugins || '') + 'bbcode';
		}

		return this.sceditor(options);
	};

	/**
	 * Converts CSS RGB and hex shorthand into hex
	 *
	 * @since v1.4.0
	 * @param {String} colorStr
	 * @return {String}
	 * @deprecated
	 */
	sceditorPlugins.bbcode.normaliseColour = _normaliseColour;
	sceditorPlugins.bbcode.formatString    = _formatString;
	sceditorPlugins.bbcode.stripQuotes     = _stripQuotes;
	sceditorPlugins.bbcode.bbcodes         = bbcodes;
	SCEditor.BBCodeParser                  = BBCodeParser;
})(jQuery, window, document);
