/**
 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

( function() {
	CKEDITOR.on( 'dialogDefinition', function( ev ) {
		var tab,
			name = ev.data.name,
			definition = ev.data.definition;

		if ( name == 'link' ) {
			definition.removeContents( 'target' );
			definition.removeContents( 'upload' );
			definition.removeContents( 'advanced' );
			tab = definition.getContents( 'info' );
			tab.remove( 'emailSubject' );
			tab.remove( 'emailBody' );
		} else if ( name == 'image' ) {
			definition.removeContents( 'advanced' );
			tab = definition.getContents( 'Link' );
			tab.remove( 'cmbTarget' );
			tab = definition.getContents( 'info' );
			tab.remove( 'txtAlt' );
			tab.remove( 'basic' );
		}
	} );

	var bbcodeMap = { b: 'strong', u: 'u', i: 'em', s: 's', color: 'span', size: 'span', left: 'div', right: 'div', center: 'div', justify: 'div', quote: 'blockquote', code: 'code', url: 'a', email: 'span', img: 'span', '*': 'li', list: 'ol' },
		convertMap = { strong: 'b', b: 'b', u: 'u', em: 'i', i: 'i', s: 's', code: 'code', li: '*' },
		tagnameMap = { strong: 'b', em: 'i', u: 'u', s: 's', li: '*', ul: 'list', ol: 'list', code: 'code', a: 'link', img: 'img', blockquote: 'quote' },
		stylesMap = { color: 'color', size: 'font-size', left: 'text-align', center: 'text-align', right: 'text-align', justify: 'text-align' },
		attributesMap = { url: 'href', email: 'mailhref', quote: 'cite', list: 'listType' };

	// List of block-like tags.
	var dtd = CKEDITOR.dtd,
		blockLikeTags = CKEDITOR.tools.extend( { table: 1 }, dtd.$block, dtd.$listItem, dtd.$tableContent, dtd.$list );

	var semicolonFixRegex = /\s*(?:;\s*|$)/;

	function serializeStyleText( stylesObject ) {
		var styleText = '';
		for ( var style in stylesObject ) {
			var styleVal = stylesObject[ style ],
				text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' );

			styleText += text;
		}
		return styleText;
	}

	// Maintain the map of smiley-to-description.
	// jscs:disable maximumLineLength
	var smileyMap = { smiley: ':)', sad: ':(', wink: ';)', laugh: ':D', cheeky: ':P', blush: ':*)', surprise: ':-o', indecision: ':|', angry: '>:(', angel: 'o:)', cool: '8-)', devil: '>:-)', crying: ';(', kiss: ':-*' },
	// jscs:enable maximumLineLength
		smileyReverseMap = {},
		smileyRegExp = [];

	// Build regexp for the list of smiley text.
	for ( var i in smileyMap ) {
		smileyReverseMap[ smileyMap[ i ] ] = i;
		smileyRegExp.push( smileyMap[ i ].replace( /\(|\)|\:|\/|\*|\-|\|/g, function( match ) {
			return '\\' + match;
		} ) );
	}

	smileyRegExp = new RegExp( smileyRegExp.join( '|' ), 'g' );

	var decodeHtml = ( function() {
		var regex = [],
			entities = {
				nbsp: '\u00A0', // IE | FF
				shy: '\u00AD' // IE
			};

		for ( var entity in entities )
			regex.push( entity );

		regex = new RegExp( '&(' + regex.join( '|' ) + ');', 'g' );

		return function( html ) {
			return html.replace( regex, function( match, entity ) {
				return entities[ entity ];
			} );
		};
	} )();

	CKEDITOR.BBCodeParser = function() {
		this._ = {
			bbcPartsRegex: /(?:\[([^\/\]=]*?)(?:=([^\]]*?))?\])|(?:\[\/([a-z]{1,16})\])/ig
		};
	};

	CKEDITOR.BBCodeParser.prototype = {
		parse: function( bbcode ) {
			var parts, part,
				lastIndex = 0;

			while ( ( parts = this._.bbcPartsRegex.exec( bbcode ) ) ) {
				var tagIndex = parts.index;
				if ( tagIndex > lastIndex ) {
					var text = bbcode.substring( lastIndex, tagIndex );
					this.onText( text, 1 );
				}

				lastIndex = this._.bbcPartsRegex.lastIndex;

				// "parts" is an array with the following items:
				// 0 : The entire match for opening/closing tags and line-break;
				// 1 : line-break;
				// 2 : open of tag excludes option;
				// 3 : tag option;
				// 4 : close of tag;

				part = ( parts[ 1 ] || parts[ 3 ] || '' ).toLowerCase();
				// Unrecognized tags should be delivered as a simple text (https://dev.ckeditor.com/ticket/7860).
				if ( part && !bbcodeMap[ part ] ) {
					this.onText( parts[ 0 ] );
					continue;
				}

				// Opening tag
				if ( parts[ 1 ] ) {
					var tagName = bbcodeMap[ part ],
						attribs = {},
						styles = {},
						optionPart = parts[ 2 ];

					// Special handling of justify tags, these provide the alignment as a tag name (#2248).
					if ( part == 'left' || part == 'right' || part == 'center' || part == 'justify' ) {
						optionPart = part;
					}

					if ( optionPart ) {
						if ( part == 'list' ) {
							if ( !isNaN( optionPart ) )
								optionPart = 'decimal';
							else if ( /^[a-z]+$/.test( optionPart ) )
								optionPart = 'lower-alpha';
							else if ( /^[A-Z]+$/.test( optionPart ) )
								optionPart = 'upper-alpha';
						}

						if ( stylesMap[ part ] ) {
							// Font size represents percentage.
							if ( part == 'size' ) {
								optionPart += '%';
							}

							styles[ stylesMap[ part ] ] = optionPart;
							attribs.style = serializeStyleText( styles );
						} else if ( attributesMap[ part ] ) {
							// All the input BBCode is encoded at the beginning so <> characters in the textual part
							// are later correctly preserved in HTML. However... it affects parts that now become
							// attributes, so we need to revert that. As a matter of fact, the content should not be
							// encoded at the beginning, but only later when creating text nodes (encoding should be more precise)
							// but it's too late not for such changes.
							attribs[ attributesMap[ part ] ] = CKEDITOR.tools.htmlDecode( optionPart );
						}
					}

					// Two special handling - image and email, protect them
					// as "span" with an attribute marker.
					if ( part == 'email' || part == 'img' )
						attribs.bbcode = part;

					this.onTagOpen( tagName, attribs, CKEDITOR.dtd.$empty[ tagName ] );
				}
				// Closing tag
				else if ( parts[ 3 ] ) {
					this.onTagClose( bbcodeMap[part] );
				}
			}

			if ( bbcode.length > lastIndex )
				this.onText( bbcode.substring( lastIndex, bbcode.length ), 1 );
		}
	};

	/**
	 * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.
	 *
	 *		var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );
	 *		alert( fragment.children[ 0 ].name );		// 'b'
	 *		alert( fragment.children[ 1 ].value );	// ' Text'
	 *
	 * @static
	 * @member CKEDITOR.htmlParser.fragment
	 * @param {String} source The HTML to be parsed, filling the fragment.
	 * @returns {CKEDITOR.htmlParser.fragment} The fragment created.
	 */
	CKEDITOR.htmlParser.fragment.fromBBCode = function( source ) {
		var parser = new CKEDITOR.BBCodeParser(),
			fragment = new CKEDITOR.htmlParser.fragment(),
			pendingInline = [],
			pendingBrs = 0,
			currentNode = fragment,
			returnPoint;

		function checkPending( newTagName ) {
			if ( pendingInline.length > 0 ) {
				for ( var i = 0; i < pendingInline.length; i++ ) {
					var pendingElement = pendingInline[ i ],
						pendingName = pendingElement.name,
						pendingDtd = CKEDITOR.dtd[ pendingName ],
						currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ];

					if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) {
						// Get a clone for the pending element.
						pendingElement = pendingElement.clone();

						// Add it to the current node and make it the current,
						// so the new element will be added inside of it.
						pendingElement.parent = currentNode;
						currentNode = pendingElement;

						// Remove the pending element (back the index by one
						// to properly process the next entry).
						pendingInline.splice( i, 1 );
						i--;
					}
				}
			}
		}

		function checkPendingBrs( tagName, closing ) {
			var len = currentNode.children.length,
				previous = len > 0 && currentNode.children[ len - 1 ],
				lineBreakParent = !previous && writer.getRule( tagnameMap[ currentNode.name ], 'breakAfterOpen' ),
				lineBreakPrevious = previous && previous.type == CKEDITOR.NODE_ELEMENT && writer.getRule( tagnameMap[ previous.name ], 'breakAfterClose' ),
				lineBreakCurrent = tagName && writer.getRule( tagnameMap[ tagName ], closing ? 'breakBeforeClose' : 'breakBeforeOpen' );

			if ( pendingBrs && ( lineBreakParent || lineBreakPrevious || lineBreakCurrent ) )
				pendingBrs--;

			// 1. Either we're at the end of block, where it requires us to compensate the br filler
			// removing logic (from htmldataprocessor).
			// 2. Or we're at the end of pseudo block, where it requires us to compensate
			// the bogus br effect.
			if ( pendingBrs && tagName in blockLikeTags )
				pendingBrs++;

			while ( pendingBrs && pendingBrs-- )
				currentNode.children.push( previous = new CKEDITOR.htmlParser.element( 'br' ) );
		}

		function addElement( node, target ) {
			checkPendingBrs( node.name, 1 );

			target = target || currentNode || fragment;

			var len = target.children.length,
				previous = len > 0 && target.children[ len - 1 ] || null;

			node.previous = previous;
			node.parent = target;

			target.children.push( node );

			if ( node.returnPoint ) {
				currentNode = node.returnPoint;
				delete node.returnPoint;
			}
		}

		parser.onTagOpen = function( tagName, attributes ) {
			var element = new CKEDITOR.htmlParser.element( tagName, attributes );

			// This is a tag to be removed if empty, so do not add it immediately.
			if ( CKEDITOR.dtd.$removeEmpty[ tagName ] ) {
				pendingInline.push( element );
				return;
			}

			var currentName = currentNode.name;

			var currentDtd = currentName && ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) );

			// If the element cannot be child of the current element.
			if ( currentDtd && !currentDtd[ tagName ] ) {
				var reApply = false,
					addPoint; // New position to start adding nodes.

				// If the element name is the same as the current element name,
				// then just close the current one and append the new one to the
				// parent. This situation usually happens with <p>, <li>, <dt> and
				// <dd>, specially in IE. Do not enter in this if block in this case.
				if ( tagName == currentName )
					addElement( currentNode, currentNode.parent );
				else if ( tagName in CKEDITOR.dtd.$listItem ) {
					parser.onTagOpen( 'ul', {} );
					addPoint = currentNode;
					reApply = true;
				} else {
					addElement( currentNode, currentNode.parent );

					// The current element is an inline element, which
					// cannot hold the new one. Put it in the pending list,
					// and try adding the new one after it.
					pendingInline.unshift( currentNode );
					reApply = true;
				}

				if ( addPoint )
					currentNode = addPoint;
				// Try adding it to the return point, or the parent element.
				else
					currentNode = currentNode.returnPoint || currentNode.parent;

				if ( reApply ) {
					parser.onTagOpen.apply( this, arguments );
					return;
				}
			}

			checkPending( tagName );
			checkPendingBrs( tagName );

			element.parent = currentNode;
			element.returnPoint = returnPoint;
			returnPoint = 0;

			if ( element.isEmpty )
				addElement( element );
			else
				currentNode = element;
		};

		parser.onTagClose = function( tagName ) {
			// Check if there is any pending tag to be closed.
			for ( var i = pendingInline.length - 1; i >= 0; i-- ) {
				// If found, just remove it from the list.
				if ( tagName == pendingInline[ i ].name ) {
					pendingInline.splice( i, 1 );
					return;
				}
			}

			var pendingAdd = [],
				newPendingInline = [],
				candidate = currentNode;

			while ( candidate.type && candidate.name != tagName ) {
				// If this is an inline element, add it to the pending list, if we're
				// really closing one of the parents element later, they will continue
				// after it.
				if ( !candidate._.isBlockLike )
					newPendingInline.unshift( candidate );

				// This node should be added to it's parent at this point. But,
				// it should happen only if the closing tag is really closing
				// one of the nodes. So, for now, we just cache it.
				pendingAdd.push( candidate );

				candidate = candidate.parent;
			}

			if ( candidate.type ) {
				// Add all elements that have been found in the above loop.
				for ( i = 0; i < pendingAdd.length; i++ ) {
					var node = pendingAdd[ i ];
					addElement( node, node.parent );
				}

				currentNode = candidate;


				addElement( candidate, candidate.parent );

				// The parent should start receiving new nodes now, except if
				// addElement changed the currentNode.
				if ( candidate == currentNode )
					currentNode = currentNode.parent;

				pendingInline = pendingInline.concat( newPendingInline );
			}
		};

		parser.onText = function( text ) {
			var currentDtd = CKEDITOR.dtd[ currentNode.name ];
			if ( !currentDtd || currentDtd[ '#' ] ) {
				checkPendingBrs();
				checkPending();

				text.replace( /(\r\n|[\r\n])|[^\r\n]*/g, function( piece, lineBreak ) {
					if ( lineBreak !== undefined && lineBreak.length )
						pendingBrs++;
					else if ( piece.length ) {
						var lastIndex = 0;

						// Create smiley from text emotion.
						piece.replace( smileyRegExp, function( match, index ) {
							addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, index ) ), currentNode );
							addElement( new CKEDITOR.htmlParser.element( 'smiley', { desc: smileyReverseMap[ match ] } ), currentNode );
							lastIndex = index + match.length;
						} );

						if ( lastIndex != piece.length )
							addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, piece.length ) ), currentNode );
					}
				} );
			}
		};

		// Parse it.
		parser.parse( CKEDITOR.tools.htmlEncode( source ) );

		// Close all hanging nodes.
		while ( currentNode.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) {
			var parent = currentNode.parent,
				node = currentNode;

			addElement( node, parent );
			currentNode = parent;
		}

		return fragment;
	};

	var BBCodeWriter = CKEDITOR.tools.createClass( {
		$: function() {
			this._ = {
				output: [],
				rules: []
			};

			// List and list item.
			this.setRules( 'list', { breakBeforeOpen: 1, breakAfterOpen: 1, breakBeforeClose: 1, breakAfterClose: 1 } );

			this.setRules( '*', {
				breakBeforeOpen: 1,
				breakAfterOpen: 0,
				breakBeforeClose: 1,
				breakAfterClose: 0
			} );

			this.setRules( 'quote', {
				breakBeforeOpen: 1,
				breakAfterOpen: 0,
				breakBeforeClose: 0,
				breakAfterClose: 1
			} );
		},

		proto: {
			//
			// Sets formatting rules for a given tag. The possible rules are:
			// <ul>
			//	<li><b>breakBeforeOpen</b>: break line before the opener tag for this element.</li>
			//	<li><b>breakAfterOpen</b>: break line after the opener tag for this element.</li>
			//	<li><b>breakBeforeClose</b>: break line before the closer tag for this element.</li>
			//	<li><b>breakAfterClose</b>: break line after the closer tag for this element.</li>
			// </ul>
			//
			// All rules default to "false". Each call to the function overrides
			// already present rules, leaving the undefined untouched.
			//
			// @param {String} tagName The tag name to which set the rules.
			// @param {Object} rules An object containing the element rules.
			// @example
			// // Break line before and after "img" tags.
			// writer.setRules( 'list',
			//		 {
			//				 breakBeforeOpen : true
			//				 breakAfterOpen : true
			//		 });
			setRules: function( tagName, rules ) {
				var currentRules = this._.rules[ tagName ];

				if ( currentRules )
					CKEDITOR.tools.extend( currentRules, rules, true );
				else
					this._.rules[ tagName ] = rules;
			},

			getRule: function( tagName, ruleName ) {
				return this._.rules[ tagName ] && this._.rules[ tagName ][ ruleName ];
			},

			openTag: function( tag ) {
				if ( tag in bbcodeMap ) {
					if ( this.getRule( tag, 'breakBeforeOpen' ) )
						this.lineBreak( 1 );

					this.write( '[', tag );
				}
			},

			openTagClose: function( tag ) {
				if ( tag == 'br' )
					this._.output.push( '\n' );
				else if ( tag in bbcodeMap ) {
					this.write( ']' );
					if ( this.getRule( tag, 'breakAfterOpen' ) )
						this.lineBreak( 1 );
				}
			},

			attribute: function( name, val ) {
				if ( name == 'option' ) {
					this.write( '=', val );
				}
			},

			closeTag: function( tag ) {
				if ( tag in bbcodeMap ) {
					if ( this.getRule( tag, 'breakBeforeClose' ) )
						this.lineBreak( 1 );

					tag != '*' && this.write( '[/', tag, ']' );

					if ( this.getRule( tag, 'breakAfterClose' ) )
						this.lineBreak( 1 );
				}
			},

			text: function( text ) {
				this.write( text );
			},

			comment: function() {},

			// Output line-break for formatting.
			lineBreak: function() {
				// Avoid line break when:
				// 1) Previous tag already put one.
				// 2) We're at output start.
				if ( !this._.hasLineBreak && this._.output.length ) {
					this.write( '\n' );
					this._.hasLineBreak = 1;
				}
			},

			write: function() {
				this._.hasLineBreak = 0;
				var data = Array.prototype.join.call( arguments, '' );
				this._.output.push( data );
			},

			reset: function() {
				this._.output = [];
				this._.hasLineBreak = 0;
			},

			getHtml: function( reset ) {
				var bbcode = this._.output.join( '' );

				if ( reset )
					this.reset();

				return decodeHtml( bbcode );
			}
		}
	} );

	var writer = new BBCodeWriter();

	CKEDITOR.plugins.add( 'bbcode', {
		requires: 'entities',

		// Adapt some critical editor configuration for better support
		// of BBCode environment.
		beforeInit: function( editor ) {
			var config = editor.config;

			CKEDITOR.tools.extend( config, {
				// This one is for backwards compatibility only as
				// editor#enterMode is already set at this stage (https://dev.ckeditor.com/ticket/11202).
				enterMode: CKEDITOR.ENTER_BR,
				basicEntities: false,
				entities: false,
				fillEmptyBlocks: false
			}, true );

			editor.filter.disable();

			// Since CKEditor 4.3.0, editor#(active)enterMode is set before
			// beforeInit. Properties got to be updated (https://dev.ckeditor.com/ticket/11202).
			editor.activeEnterMode = editor.enterMode = CKEDITOR.ENTER_BR;
		},

		init: function( editor ) {
			var config = editor.config;

			function BBCodeToHtml( code ) {
				var fragment = CKEDITOR.htmlParser.fragment.fromBBCode( code ),
					writer = new CKEDITOR.htmlParser.basicWriter();

				fragment.writeHtml( writer, bbcodeFilter );
				return writer.getHtml( true );
			}

			var bbcodeFilter = new CKEDITOR.htmlParser.filter();
			bbcodeFilter.addRules( {
				elements: {
					blockquote: function( element ) {
						var quoted = new CKEDITOR.htmlParser.element( 'div' );
						quoted.children = element.children;
						element.children = [ quoted ];
						var citeText = element.attributes.cite;
						if ( citeText ) {
							var cite = new CKEDITOR.htmlParser.element( 'cite' );
							cite.add( new CKEDITOR.htmlParser.text( citeText.replace( /^"|"$/g, '' ) ) );
							delete element.attributes.cite;
							element.children.unshift( cite );
						}
					},
					span: function( element ) {
						var bbcode;
						if ( ( bbcode = element.attributes.bbcode ) ) {
							if ( bbcode == 'img' ) {
								element.name = 'img';
								element.attributes.src = element.children[ 0 ].value;
								element.children = [];
							} else if ( bbcode == 'email' ) {
								element.name = 'a';
								element.attributes.href = 'mailto:' + element.children[ 0 ].value;
							}

							delete element.attributes.bbcode;
						}
					},
					ol: function( element ) {
						if ( element.attributes.listType ) {
							if ( element.attributes.listType != 'decimal' )
								element.attributes.style = 'list-style-type:' + element.attributes.listType;
						} else {
							element.name = 'ul';
						}

						delete element.attributes.listType;
					},
					a: function( element ) {
						if ( !element.attributes.href )
							element.attributes.href = element.children[ 0 ].value;
					},
					smiley: function( element ) {
						element.name = 'img';

						var description = element.attributes.desc,
							image = config.smiley_images[ CKEDITOR.tools.indexOf( config.smiley_descriptions, description ) ],
							src = CKEDITOR.tools.htmlEncode( config.smiley_path + image );

						element.attributes = {
							src: src,
							'data-cke-saved-src': src,
							title: description,
							alt: description
						};
					}
				}
			} );

			editor.dataProcessor.htmlFilter.addRules( {
				elements: {
					$: function( element ) {
						var attributes = element.attributes,
							style = CKEDITOR.tools.parseCssText( attributes.style, 1 ),
							value;

						var tagName = element.name;
						if ( tagName in convertMap )
							tagName = convertMap[ tagName ];
						else if ( tagName == 'span' ) {
							if ( ( value = style.color ) ) {
								tagName = 'color';
								value = CKEDITOR.tools.convertRgbToHex( value );
							} else if ( ( value = style[ 'font-size' ] ) ) {
								var percentValue = value.match( /(\d+)%$/ );
								if ( percentValue ) {
									value = percentValue[ 1 ];
									tagName = 'size';
								}
							}
						} else if ( tagName == 'ol' || tagName == 'ul' ) {
							if ( ( value = style[ 'list-style-type' ] ) ) {
								switch ( value ) {
									case 'lower-alpha':
										value = 'a';
										break;
									case 'upper-alpha':
										value = 'A';
										break;
								}
							} else if ( tagName == 'ol' ) {
								value = 1;
							}

							tagName = 'list';
						} else if ( tagName == 'blockquote' ) {
							try {
								var cite = element.children[ 0 ],
									quoted = element.children[ 1 ],
									citeText = cite.name == 'cite' && cite.children[ 0 ].value;

								if ( citeText ) {
									value = '"' + citeText + '"';
									element.children = quoted.children;
								}

							} catch ( er ) {}

							tagName = 'quote';
						} else if ( tagName == 'a' ) {
							if ( ( value = attributes.href ) ) {
								if ( value.indexOf( 'mailto:' ) !== -1 ) {
									tagName = 'email';
									// [email] should have a single text child with email address.
									element.children = [ new CKEDITOR.htmlParser.text( value.replace( 'mailto:', '' ) ) ];
									value = '';
								} else {
									var singleton = element.children.length == 1 && element.children[ 0 ];
									if ( singleton && singleton.type == CKEDITOR.NODE_TEXT && singleton.value == value )
										value = '';

									tagName = 'url';
								}
							}
						} else if ( tagName == 'img' ) {
							element.isEmpty = 0;

							// Translate smiley (image) to text emotion.
							var src = attributes[ 'data-cke-saved-src' ] || attributes.src,
								alt = attributes.alt;

							if ( src && src.indexOf( editor.config.smiley_path ) != -1 && alt )
								return new CKEDITOR.htmlParser.text( smileyMap[ alt ] );
							else
								element.children = [ new CKEDITOR.htmlParser.text( src ) ];
						}

						element.name = tagName;
						value && ( element.attributes.option = value );

						return null;
					},

					div: function( element ) {
						var alignment = CKEDITOR.tools.parseCssText( element.attributes.style, 1 )[ 'text-align' ] || '';

						if ( alignment ) {
							element.name = alignment;
							return null;
						}
					},

					// Remove any bogus br from the end of a pseudo block,
					// e.g. <div>some text<br /><p>paragraph</p></div>
					br: function( element ) {
						var next = element.next;
						if ( next && next.name in blockLikeTags )
							return false;
					}
				}
			}, 1 );

			editor.dataProcessor.writer = writer;

			function onSetData( evt ) {
				var bbcode = evt.data.dataValue;
				evt.data.dataValue = BBCodeToHtml( bbcode );
			}

			// Skip the first "setData" call from inline creator, to allow content of
			// HTML to be loaded from the page element.
			if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
				editor.once( 'contentDom', function() {
					editor.on( 'setData', onSetData );
				} );
			else
				editor.on( 'setData', onSetData );

		},

		afterInit: function( editor ) {
			var filters;
			if ( editor._.elementsPath ) {
				// Eliminate irrelevant elements from displaying, e.g body and p.
				if ( ( filters = editor._.elementsPath.filters ) ) {
					filters.push( function( element ) {
						var htmlName = element.getName(),
							name = tagnameMap[ htmlName ] || false;

						// Specialized anchor presents as email.
						if ( name == 'link' && element.getAttribute( 'href' ).indexOf( 'mailto:' ) === 0 )
							name = 'email';
						// Styled span could be either size or color.
						else if ( htmlName == 'span' ) {
							if ( element.getStyle( 'font-size' ) )
								name = 'size';
							else if ( element.getStyle( 'color' ) )
								name = 'color';
						// Styled div could be align
						} else if ( htmlName == 'div' && element.getStyle( 'text-align' ) ) {
							name = element.getStyle( 'text-align' );
						} else if ( name == 'img' ) {
							var src = element.data( 'cke-saved-src' ) || element.getAttribute( 'src' );
							if ( src && src.indexOf( editor.config.smiley_path ) === 0 )
								name = 'smiley';
						}

						return name;
					} );
				}
			}
		}
	} );

} )();
