1 /*
  2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
  3 For licensing, see LICENSE.html or http://ckeditor.com/license
  4 */
  5 
  6 (function()
  7 {
  8 	CKEDITOR.on( 'dialogDefinition', function( ev )
  9 	{
 10 		var tab, name = ev.data.name,
 11 			definition = ev.data.definition;
 12 
 13 		if ( name == 'link' )
 14 		{
 15 			definition.removeContents( 'target' );
 16 			definition.removeContents( 'upload' );
 17 			definition.removeContents( 'advanced' );
 18 			tab = definition.getContents( 'info' );
 19 			tab.remove( 'emailSubject' );
 20 			tab.remove( 'emailBody' );
 21 		}
 22 		else if ( name == 'image' )
 23 		{
 24 			definition.removeContents( 'advanced' );
 25 			tab = definition.getContents( 'Link' );
 26 			tab.remove( 'cmbTarget' );
 27 			tab = definition.getContents( 'info' );
 28 			tab.remove( 'txtAlt' );
 29 			tab.remove( 'basic' );
 30 		}
 31 	});
 32 
 33 	var bbcodeMap = { 'b' : 'strong', 'u': 'u', 'i' : 'em', 'color' : 'span', 'size' : 'span', 'quote' : 'blockquote', 'code' : 'code', 'url' : 'a', 'email' : 'span', 'img' : 'span', '*' : 'li', 'list' : 'ol' },
 34 			convertMap = { 'strong' : 'b' , 'b' : 'b', 'u': 'u', 'em' : 'i', 'i': 'i', 'code' : 'code', 'li' : '*' },
 35 			tagnameMap = { 'strong' : 'b', 'em' : 'i', 'u' : 'u', 'li' : '*', 'ul' : 'list', 'ol' : 'list', 'code' : 'code', 'a' : 'link', 'img' : 'img', 'blockquote' : 'quote' },
 36 			stylesMap = { 'color' : 'color', 'size' : 'font-size' },
 37 			attributesMap = { 'url' : 'href', 'email' : 'mailhref', 'quote': 'cite', 'list' : 'listType' };
 38 
 39 	// List of block-like tags.
 40 	var dtd =  CKEDITOR.dtd,
 41 		blockLikeTags = CKEDITOR.tools.extend( { table:1 }, dtd.$block, dtd.$listItem, dtd.$tableContent, dtd.$list );
 42 
 43 	var semicolonFixRegex = /\s*(?:;\s*|$)/;
 44 	function serializeStyleText( stylesObject )
 45 	{
 46 		var styleText = '';
 47 		for ( var style in stylesObject )
 48 		{
 49 			var styleVal = stylesObject[ style ],
 50 				text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' );
 51 
 52 			styleText += text;
 53 		}
 54 		return styleText;
 55 	}
 56 
 57 	function parseStyleText( styleText )
 58 	{
 59 		var retval = {};
 60 		( styleText || '' )
 61 				.replace( /"/g, '"' )
 62 				.replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value )
 63 		{
 64 			retval[ name.toLowerCase() ] = value;
 65 		} );
 66 		return retval;
 67 	}
 68 
 69 	function RGBToHex( cssStyle )
 70 	{
 71 		return cssStyle.replace( /(?:rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))/gi, function( match, red, green, blue )
 72 			{
 73 				red = parseInt( red, 10 ).toString( 16 );
 74 				green = parseInt( green, 10 ).toString( 16 );
 75 				blue = parseInt( blue, 10 ).toString( 16 );
 76 				var color = [red, green, blue] ;
 77 
 78 				// Add padding zeros if the hex value is less than 0x10.
 79 				for ( var i = 0 ; i < color.length ; i++ )
 80 					color[i] = String( '0' + color[i] ).slice( -2 ) ;
 81 
 82 				return '#' + color.join( '' ) ;
 83 			 });
 84 	}
 85 
 86 	// Maintain the map of smiley-to-description.
 87 	var smileyMap = {"smiley":":)","sad":":(","wink":";)","laugh":":D","cheeky":":P","blush":":*)","surprise":":-o","indecision":":|","angry":">:(","angel":"o:)","cool":"8-)","devil":">:-)","crying":";(","kiss":":-*" },
 88 		smileyReverseMap = {},
 89 		smileyRegExp = [];
 90 
 91 	// Build regexp for the list of smiley text.
 92 	for ( var i in smileyMap )
 93 	{
 94 		smileyReverseMap[ smileyMap[ i ] ] = i;
 95 		smileyRegExp.push( smileyMap[ i ].replace( /\(|\)|\:|\/|\*|\-|\|/g, function( match ) { return '\\' + match; } ) );
 96 	}
 97 
 98 	smileyRegExp = new RegExp( smileyRegExp.join( '|' ), 'g' );
 99 
100 	var decodeHtml = ( function ()
101 	{
102 		var regex = [],
103 			entities =
104 			{
105 				nbsp	: '\u00A0',		// IE | FF
106 				shy		: '\u00AD',		// IE
107 				gt		: '\u003E',		// IE | FF |   --   | Opera
108 				lt		: '\u003C'		// IE | FF | Safari | Opera
109 			};
110 
111 		for ( var entity in entities )
112 			regex.push( entity );
113 
114 		regex = new RegExp( '&(' + regex.join( '|' ) + ');', 'g' );
115 
116 		return function( html )
117 		{
118 			return html.replace( regex, function( match, entity )
119 			{
120 				return entities[ entity ];
121 			});
122 		};
123 	})();
124 
125 	CKEDITOR.BBCodeParser = function()
126 	{
127 		this._ =
128 		{
129 			bbcPartsRegex : /(?:\[([^\/\]=]*?)(?:=([^\]]*?))?\])|(?:\[\/([a-z]{1,16})\])/ig
130 		};
131 	};
132 
133 	CKEDITOR.BBCodeParser.prototype =
134 	{
135 		parse : function( bbcode )
136 		{
137 			var parts,
138 					part,
139 					lastIndex = 0;
140 
141 			while ( ( parts = this._.bbcPartsRegex.exec( bbcode ) ) )
142 			{
143 				var tagIndex = parts.index;
144 				if ( tagIndex > lastIndex )
145 				{
146 					var text = bbcode.substring( lastIndex, tagIndex );
147 					this.onText( text, 1 );
148 				}
149 
150 				lastIndex = this._.bbcPartsRegex.lastIndex;
151 
152 				/*
153 				 "parts" is an array with the following items:
154 				 0 : The entire match for opening/closing tags and line-break;
155 				 1 : line-break;
156 				 2 : open of tag excludes option;
157 				 3 : tag option;
158 				 4 : close of tag;
159 				 */
160 
161 				part = ( parts[ 1 ] || parts[ 3 ] || '' ).toLowerCase();
162 				// Unrecognized tags should be delivered as a simple text (#7860).
163 				if ( part && !bbcodeMap[ part ] )
164 				{
165 					this.onText( parts[ 0 ] );
166 					continue;
167 				}
168 
169 				// Opening tag
170 				if ( parts[ 1 ] )
171 				{
172 					var tagName = bbcodeMap[ part ],
173 							attribs = {},
174 							styles = {},
175 							optionPart = parts[ 2 ];
176 
177 					if ( optionPart )
178 					{
179 						if ( part == 'list' )
180 						{
181 							if ( !isNaN( optionPart ) )
182 								optionPart = 'decimal';
183 							else if ( /^[a-z]+$/.test( optionPart ) )
184 								optionPart = 'lower-alpha';
185 							else if ( /^[A-Z]+$/.test( optionPart ) )
186 								optionPart = 'upper-alpha';
187 						}
188 
189 						if ( stylesMap[ part ] )
190 						{
191 							// Font size represents percentage.
192 							if ( part == 'size' )
193 								optionPart += '%';
194 
195 							styles[ stylesMap[ part ] ] = optionPart;
196 							attribs.style = serializeStyleText( styles );
197 						}
198 						else if ( attributesMap[ part ] )
199 							attribs[ attributesMap[ part ] ] = optionPart;
200 					}
201 
202 					// Two special handling - image and email, protect them
203 					// as "span" with an attribute marker.
204 					if ( part == 'email' || part == 'img' )
205 						attribs[ 'bbcode' ] = part;
206 
207 					this.onTagOpen( tagName, attribs, CKEDITOR.dtd.$empty[ tagName ] );
208 				}
209 				// Closing tag
210 				else if ( parts[ 3 ] )
211 					this.onTagClose( bbcodeMap[ part ] );
212 			}
213 
214 			if ( bbcode.length > lastIndex )
215 				this.onText( bbcode.substring( lastIndex, bbcode.length ), 1 );
216 		}
217 	};
218 
219 	/**
220 	 * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.
221 	 * @param {String} source The HTML to be parsed, filling the fragment.
222 	 * @param {Number} [fixForBody=false] Wrap body with specified element if needed.
223 	 * @returns CKEDITOR.htmlParser.fragment The fragment created.
224 	 * @example
225 	 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );
226 	 * alert( fragment.children[0].name );  "b"
227 	 * alert( fragment.children[1].value );  " Text"
228 	 */
229 	CKEDITOR.htmlParser.fragment.fromBBCode = function( source )
230 	{
231 		var parser = new CKEDITOR.BBCodeParser(),
232 			fragment = new CKEDITOR.htmlParser.fragment(),
233 			pendingInline = [],
234 			pendingBrs = 0,
235 			currentNode = fragment,
236 			returnPoint;
237 
238 		function checkPending( newTagName )
239 		{
240 			if ( pendingInline.length > 0 )
241 			{
242 				for ( var i = 0 ; i < pendingInline.length ; i++ )
243 				{
244 					var pendingElement = pendingInline[ i ],
245 						pendingName = pendingElement.name,
246 						pendingDtd = CKEDITOR.dtd[ pendingName ],
247 						currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ];
248 
249 					if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) )
250 					{
251 						// Get a clone for the pending element.
252 						pendingElement = pendingElement.clone();
253 
254 						// Add it to the current node and make it the current,
255 						// so the new element will be added inside of it.
256 						pendingElement.parent = currentNode;
257 						currentNode = pendingElement;
258 
259 						// Remove the pending element (back the index by one
260 						// to properly process the next entry).
261 						pendingInline.splice( i, 1 );
262 						i--;
263 					}
264 				}
265 			}
266 		}
267 
268 		function checkPendingBrs( tagName, closing )
269 		{
270 			var len = currentNode.children.length,
271 				previous = len > 0 && currentNode.children[ len - 1 ],
272 				lineBreakParent = !previous && BBCodeWriter.getRule( tagnameMap[ currentNode.name ], 'breakAfterOpen' ),
273 				lineBreakPrevious = previous && previous.type == CKEDITOR.NODE_ELEMENT && BBCodeWriter.getRule( tagnameMap[ previous.name ], 'breakAfterClose' ),
274 				lineBreakCurrent = tagName && BBCodeWriter.getRule( tagnameMap[ tagName ], closing ? 'breakBeforeClose' : 'breakBeforeOpen' );
275 
276 			if ( pendingBrs && ( lineBreakParent || lineBreakPrevious || lineBreakCurrent ) )
277 				pendingBrs--;
278 
279 			// 1. Either we're at the end of block, where it requires us to compensate the br filler
280 			// removing logic (from htmldataprocessor).
281 			// 2. Or we're at the end of pseudo block, where it requires us to compensate
282 			// the bogus br effect.
283 			if ( pendingBrs && tagName in blockLikeTags )
284 				pendingBrs++;
285 
286 			while ( pendingBrs && pendingBrs-- )
287 				currentNode.children.push( previous = new CKEDITOR.htmlParser.element( 'br' ) );
288 		}
289 
290 		function addElement( node, target )
291 		{
292 			checkPendingBrs( node.name, 1 );
293 
294 			target = target || currentNode || fragment;
295 
296 			var len = target.children.length,
297 				previous = len > 0 && target.children[ len - 1 ] || null;
298 
299 			node.previous = previous;
300 			node.parent = target;
301 
302 			target.children.push( node );
303 
304 			if ( node.returnPoint )
305 			{
306 				currentNode = node.returnPoint;
307 				delete node.returnPoint;
308 			}
309 		}
310 
311 		parser.onTagOpen = function( tagName, attributes, selfClosing )
312 		{
313 			var element = new CKEDITOR.htmlParser.element( tagName, attributes );
314 
315 			// This is a tag to be removed if empty, so do not add it immediately.
316 			if ( CKEDITOR.dtd.$removeEmpty[ tagName ] )
317 			{
318 				pendingInline.push( element );
319 				return;
320 			}
321 
322 			var currentName = currentNode.name;
323 
324 			var currentDtd = currentName
325 				&& ( CKEDITOR.dtd[ currentName ]
326 					|| ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) );
327 
328 			// If the element cannot be child of the current element.
329 			if ( currentDtd && !currentDtd[ tagName ] )
330 			{
331 				var reApply = false,
332 					addPoint;   // New position to start adding nodes.
333 
334 				// If the element name is the same as the current element name,
335 				// then just close the current one and append the new one to the
336 				// parent. This situation usually happens with <p>, <li>, <dt> and
337 				// <dd>, specially in IE. Do not enter in this if block in this case.
338 				if ( tagName == currentName )
339 					addElement( currentNode, currentNode.parent );
340 				else if ( tagName in CKEDITOR.dtd.$listItem )
341 				{
342 					parser.onTagOpen( 'ul', {} );
343 					addPoint = currentNode;
344 					reApply = true;
345 				}
346 				else
347 				{
348 					addElement( currentNode, currentNode.parent );
349 
350 					// The current element is an inline element, which
351 					// cannot hold the new one. Put it in the pending list,
352 					// and try adding the new one after it.
353 					pendingInline.unshift( currentNode );
354 					reApply = true;
355 				}
356 
357 				if ( addPoint )
358 					currentNode = addPoint;
359 				// Try adding it to the return point, or the parent element.
360 				else
361 					currentNode = currentNode.returnPoint || currentNode.parent;
362 
363 				if ( reApply )
364 				{
365 					parser.onTagOpen.apply( this, arguments );
366 					return;
367 				}
368 			}
369 
370 			checkPending( tagName );
371 			checkPendingBrs( tagName );
372 
373 			element.parent = currentNode;
374 			element.returnPoint = returnPoint;
375 			returnPoint = 0;
376 
377 			if ( element.isEmpty )
378 				addElement( element );
379 			else
380 				currentNode = element;
381 		};
382 
383 		parser.onTagClose = function( tagName )
384 		{
385 			// Check if there is any pending tag to be closed.
386 			for ( var i = pendingInline.length - 1 ; i >= 0 ; i-- )
387 			{
388 				// If found, just remove it from the list.
389 				if ( tagName == pendingInline[ i ].name )
390 				{
391 					pendingInline.splice( i, 1 );
392 					return;
393 				}
394 			}
395 
396 			var pendingAdd = [],
397 				newPendingInline = [],
398 				candidate = currentNode;
399 
400 			while ( candidate.type && candidate.name != tagName )
401 			{
402 				// If this is an inline element, add it to the pending list, if we're
403 				// really closing one of the parents element later, they will continue
404 				// after it.
405 				if ( !candidate._.isBlockLike )
406 					newPendingInline.unshift( candidate );
407 
408 				// This node should be added to it's parent at this point. But,
409 				// it should happen only if the closing tag is really closing
410 				// one of the nodes. So, for now, we just cache it.
411 				pendingAdd.push( candidate );
412 
413 				candidate = candidate.parent;
414 			}
415 
416 			if ( candidate.type )
417 			{
418 				// Add all elements that have been found in the above loop.
419 				for ( i = 0 ; i < pendingAdd.length ; i++ )
420 				{
421 					var node = pendingAdd[ i ];
422 					addElement( node, node.parent );
423 				}
424 
425 				currentNode = candidate;
426 
427 
428 				addElement( candidate, candidate.parent );
429 
430 				// The parent should start receiving new nodes now, except if
431 				// addElement changed the currentNode.
432 				if ( candidate == currentNode )
433 					currentNode = currentNode.parent;
434 
435 				pendingInline = pendingInline.concat( newPendingInline );
436 			}
437 		};
438 
439 		parser.onText = function( text )
440 		{
441 			var currentDtd = CKEDITOR.dtd[ currentNode.name ];
442 			if ( !currentDtd || currentDtd[ '#' ] )
443 			{
444 				checkPendingBrs();
445 				checkPending();
446 
447 				text.replace(/([\r\n])|[^\r\n]*/g, function( piece, lineBreak )
448 				{
449 					if ( lineBreak !== undefined && lineBreak.length )
450 						pendingBrs++;
451 					else if ( piece.length )
452 					{
453 						var lastIndex = 0;
454 
455 						// Create smiley from text emotion.
456 						piece.replace( smileyRegExp, function( match, index )
457 						{
458 							addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, index ) ), currentNode );
459 							addElement( new CKEDITOR.htmlParser.element( 'smiley', { 'desc': smileyReverseMap[ match ] } ), currentNode );
460 							lastIndex = index + match.length;
461 						});
462 
463 						if ( lastIndex != piece.length )
464 							addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, piece.length ) ), currentNode );
465 					}
466 				});
467 			}
468 		};
469 
470 		// Parse it.
471 		parser.parse( CKEDITOR.tools.htmlEncode( source ) );
472 
473 		// Close all hanging nodes.
474 		while ( currentNode.type )
475 		{
476 			var parent = currentNode.parent,
477 				node = currentNode;
478 
479 			addElement( node, parent );
480 			currentNode = parent;
481 		}
482 
483 		return fragment;
484 	};
485 
486 	CKEDITOR.htmlParser.BBCodeWriter = CKEDITOR.tools.createClass(
487 	{
488 		$ : function()
489 		{
490 			this._ =
491 			{
492 				output : [],
493 				rules : []
494 			};
495 
496 			// List and list item.
497 			this.setRules( 'list',
498 		   {
499 			   breakBeforeOpen : 1,
500 			   breakAfterOpen : 1,
501 			   breakBeforeClose : 1,
502 			   breakAfterClose : 1
503 		   } );
504 
505 			this.setRules( '*',
506 		   {
507 			   breakBeforeOpen : 1,
508 			   breakAfterOpen : 0,
509 			   breakBeforeClose : 1,
510 			   breakAfterClose : 0
511 		   } );
512 
513 			this.setRules( 'quote',
514 		   {
515 			   breakBeforeOpen : 1,
516 			   breakAfterOpen : 0,
517 			   breakBeforeClose : 0,
518 			   breakAfterClose : 1
519 		   } );
520 		},
521 
522 		proto :
523 		{
524 			/**
525 			 * Sets formatting rules for a given tag. The possible rules are:
526 			 * <ul>
527 			 *	<li><b>breakBeforeOpen</b>: break line before the opener tag for this element.</li>
528 			 *	<li><b>breakAfterOpen</b>: break line after the opener tag for this element.</li>
529 			 *	<li><b>breakBeforeClose</b>: break line before the closer tag for this element.</li>
530 			 *	<li><b>breakAfterClose</b>: break line after the closer tag for this element.</li>
531 			 * </ul>
532 			 *
533 			 * All rules default to "false". Each call to the function overrides
534 			 * already present rules, leaving the undefined untouched.
535 			 *
536 			 * @param {String} tagName The tag name to which set the rules.
537 			 * @param {Object} rules An object containing the element rules.
538 			 * @example
539 			 * // Break line before and after "img" tags.
540 			 * writer.setRules( 'list',
541 			 *     {
542 			 *         breakBeforeOpen : true
543 			 *         breakAfterOpen : true
544 			 *     });
545 			 */
546 			setRules : function( tagName, rules )
547 			{
548 				var currentRules = this._.rules[ tagName ];
549 
550 				if ( currentRules )
551 					CKEDITOR.tools.extend( currentRules, rules, true );
552 				else
553 					this._.rules[ tagName ] = rules;
554 			},
555 
556 			getRule : function( tagName, ruleName )
557 			{
558 				return this._.rules[ tagName ] && this._.rules[ tagName ][ ruleName ];
559 			},
560 
561 			openTag : function( tag, attributes )
562 			{
563 				if ( tag in bbcodeMap )
564 				{
565 					if ( this.getRule( tag, 'breakBeforeOpen' ) )
566 						this.lineBreak( 1 );
567 
568 					this.write( '[', tag );
569 				}
570 			},
571 
572 			openTagClose : function( tag )
573 			{
574 
575 				if ( tag == 'br' )
576 					this._.output.push( '\n' );
577 				else if ( tag in bbcodeMap )
578 				{
579 					this.write( ']' );
580 					if ( this.getRule( tag, 'breakAfterOpen' ) )
581 						this.lineBreak( 1 );
582 				}
583 			},
584 
585 			attribute : function( name, val )
586 			{
587 				if ( name == 'option' )
588 				{
589 					// Force simply ampersand in attributes.
590 					if ( typeof val == 'string' )
591 						val = val.replace( /&/g, '&' );
592 
593 					this.write( '=', val );
594 				}
595 			},
596 
597 			closeTag : function( tag )
598 			{
599 				if ( tag in bbcodeMap )
600 				{
601 					if ( this.getRule( tag, 'breakBeforeClose' ) )
602 						this.lineBreak( 1 );
603 
604 					tag != '*' && this.write( '[/', tag, ']' );
605 
606 					if ( this.getRule( tag, 'breakAfterClose' ) )
607 						this.lineBreak( 1 );
608 				}
609 			},
610 
611 			text : function( text )
612 			{
613 				this.write( text );
614 			},
615 
616 			/**
617 			 * Writes a comment.
618 			 * @param {String} comment The comment text.
619 			 * @example
620 			 * // Writes "<!-- My comment -->".
621 			 * writer.comment( ' My comment ' );
622 			 */
623 			comment : function() {},
624 
625 			/*
626 			* Output line-break for formatting.
627 			 */
628 			lineBreak : function()
629 			{
630 				// Avoid line break when:
631 				// 1) Previous tag already put one.
632 				// 2) We're at output start.
633 				if ( !this._.hasLineBreak && this._.output.length )
634 				{
635 					this.write( '\n' );
636 					this._.hasLineBreak = 1;
637 				}
638 			},
639 
640 			write : function()
641 			{
642 				this._.hasLineBreak = 0;
643 				var data = Array.prototype.join.call( arguments, '' );
644 				this._.output.push( data );
645 			},
646 
647 			reset : function()
648 			{
649 				this._.output = [];
650 				this._.hasLineBreak = 0;
651 			},
652 
653 			getHtml : function( reset )
654 			{
655 				var bbcode = this._.output.join( '' );
656 
657 				if ( reset )
658 					this.reset();
659 
660 				return decodeHtml ( bbcode );
661 			}
662 		}
663 	});
664 
665 	var BBCodeWriter = new CKEDITOR.htmlParser.BBCodeWriter();
666 
667 	CKEDITOR.plugins.add( 'bbcode',
668 	  {
669 		  requires : [ 'htmldataprocessor', 'entities' ],
670 		  beforeInit : function( editor )
671 		  {
672 			  // Adapt some critical editor configuration for better support
673 			  // of BBCode environment.
674 			  var config = editor.config;
675 			  CKEDITOR.tools.extend( config,
676 			  {
677 				  enterMode : CKEDITOR.ENTER_BR,
678 				  basicEntities: false,
679 				  entities : false,
680 				  fillEmptyBlocks : false
681 			  }, true );
682 		  },
683 		  init : function( editor )
684 		  {
685 			  var config = editor.config;
686 
687 			  function BBCodeToHtml( code )
688 			  {
689 				  var fragment = CKEDITOR.htmlParser.fragment.fromBBCode( code ),
690 						  writer = new CKEDITOR.htmlParser.basicWriter();
691 
692 				  fragment.writeHtml( writer, dataFilter );
693 				  return writer.getHtml( true );
694 			  }
695 
696 			  var dataFilter = new CKEDITOR.htmlParser.filter();
697 			  dataFilter.addRules(
698 			  {
699 				  elements :
700 				  {
701 					  'blockquote' : function( element )
702 					  {
703 						  var quoted = new CKEDITOR.htmlParser.element( 'div' );
704 						  quoted.children = element.children;
705 						  element.children = [ quoted ];
706 						  var citeText = element.attributes.cite;
707 						  if ( citeText )
708 						  {
709 							  var cite = new CKEDITOR.htmlParser.element( 'cite' );
710 							  cite.add( new CKEDITOR.htmlParser.text( citeText.replace( /^"|"$/g, '' ) ) );
711 							  delete element.attributes.cite;
712 							  element.children.unshift( cite );
713 						  }
714 					  },
715 					  'span' : function( element )
716 					  {
717 						  var bbcode;
718 						  if ( ( bbcode = element.attributes.bbcode ) )
719 						  {
720 							  if ( bbcode == 'img' )
721 							  {
722 								  element.name = 'img';
723 								  element.attributes.src = element.children[ 0 ].value;
724 								  element.children = [];
725 							  }
726 							  else if ( bbcode == 'email' )
727 							  {
728 								  element.name = 'a';
729 								  element.attributes.href = 'mailto:' + element.children[ 0 ].value;
730 							  }
731 
732 							  delete element.attributes.bbcode;
733 						  }
734 					  },
735 					  'ol' : function ( element )
736 					  {
737 						  if ( element.attributes.listType )
738 						  {
739 							  if ( element.attributes.listType != 'decimal' )
740 								  element.attributes.style = 'list-style-type:' + element.attributes.listType;
741 						  }
742 						  else
743 							  element.name = 'ul';
744 
745 						  delete element.attributes.listType;
746 					  },
747 					  a : function( element )
748 					  {
749 						  if ( !element.attributes.href )
750 							  element.attributes.href = element.children[ 0 ].value;
751 					  },
752 					  'smiley' : function( element )
753 					  {
754 							element.name = 'img';
755 
756 							var description = element.attributes.desc,
757 								image = config.smiley_images[ CKEDITOR.tools.indexOf( config.smiley_descriptions, description ) ],
758 								src = CKEDITOR.tools.htmlEncode( config.smiley_path + image );
759 
760 						  element.attributes =
761 						  {
762 							  src : src,
763 							  'data-cke-saved-src' : src,
764 							  title :  description,
765 							  alt : description
766 						  };
767 					  }
768 				  }
769 			  } );
770 
771 			  editor.dataProcessor.htmlFilter.addRules(
772 			  {
773 				elements :
774 				{
775 					$ : function( element )
776 					{
777 						var attributes = element.attributes,
778 								style = parseStyleText( attributes.style ),
779 								value;
780 
781 						var tagName = element.name;
782 						if ( tagName in convertMap )
783 							tagName = convertMap[ tagName ];
784 						else if ( tagName == 'span' )
785 						{
786 							if ( ( value = style.color ) )
787 							{
788 								tagName = 'color';
789 								value = RGBToHex( value );
790 							}
791 							else if ( ( value = style[ 'font-size' ] ) )
792 							{
793 								var percentValue = value.match( /(\d+)%$/ );
794 								if ( percentValue )
795 								{
796 									value = percentValue[ 1 ];
797 									tagName = 'size';
798 								}
799 							}
800 						}
801 						else if ( tagName == 'ol' || tagName == 'ul' )
802 						{
803 							if ( ( value = style[ 'list-style-type'] ) )
804 							{
805 								switch ( value )
806 								{
807 									case 'lower-alpha':
808 										value = 'a';
809 										break;
810 									case 'upper-alpha':
811 										value = 'A';
812 										break;
813 								}
814 							}
815 							else if ( tagName == 'ol' )
816 								value = 1;
817 
818 							tagName = 'list';
819 						}
820 						else if ( tagName == 'blockquote' )
821 						{
822 							try
823 							{
824 								var cite = element.children[ 0 ],
825 										quoted = element.children[ 1 ],
826 										citeText = cite.name == 'cite' && cite.children[ 0 ].value;
827 
828 								if ( citeText )
829 								{
830 									value = '"' + citeText + '"';
831 									element.children = quoted.children;
832 								}
833 
834 							}
835 							catch( er )
836 							{
837 							}
838 
839 							tagName = 'quote';
840 						}
841 						else if ( tagName == 'a' )
842 						{
843 							if ( ( value = attributes.href ) )
844 							{
845 								if ( value.indexOf( 'mailto:' ) !== -1 )
846 								{
847 									tagName = 'email';
848 									// [email] should have a single text child with email address.
849 									element.children = [ new CKEDITOR.htmlParser.text( value.replace( 'mailto:', '' ) ) ];
850 									value = '';
851 								}
852 								else
853 								{
854 									var singleton = element.children.length == 1 && element.children[ 0 ];
855 									if ( singleton
856 											&& singleton.type == CKEDITOR.NODE_TEXT
857 											&& singleton.value == value )
858 										value = '';
859 
860 									tagName = 'url';
861 								}
862 							}
863 						}
864 						else if ( tagName == 'img' )
865 						{
866 							element.isEmpty = 0;
867 
868 							// Translate smiley (image) to text emotion.
869 							var src = attributes[ 'data-cke-saved-src' ];
870 							if ( src && src.indexOf( editor.config.smiley_path ) != -1 )
871 								return new CKEDITOR.htmlParser.text( smileyMap[ attributes.alt ] );
872 							else
873 								element.children = [ new CKEDITOR.htmlParser.text( src ) ];
874 						}
875 
876 						element.name = tagName;
877 						value && ( element.attributes.option = value );
878 
879 						return null;
880 					},
881 
882 					// Remove any bogus br from the end of a pseudo block,
883 					// e.g. <div>some text<br /><p>paragraph</p></div>
884 					br : function( element )
885 					{
886 						var next = element.next;
887 						if ( next && next.name in blockLikeTags )
888 							return false;
889 					}
890 				}
891 			  }, 1 );
892 
893 			  editor.dataProcessor.writer = BBCodeWriter;
894 
895 			  editor.on( 'beforeSetMode', function( evt )
896 			  {
897 				  evt.removeListener();
898 				  var wysiwyg = editor._.modes[ 'wysiwyg' ];
899 				  wysiwyg.loadData = CKEDITOR.tools.override( wysiwyg.loadData, function( org )
900 				  {
901 					  return function( data )
902 					  {
903 						  return ( org.call( this, BBCodeToHtml( data ) ) );
904 					  };
905 				  } );
906 			  } );
907 		  },
908 
909 		  afterInit : function( editor )
910 		  {
911 			  var filters;
912 			  if ( editor._.elementsPath  )
913 			  {
914 				  // Eliminate irrelevant elements from displaying, e.g body and p.
915 				  if ( ( filters = editor._.elementsPath.filters ) )
916 					filters.push( function( element )
917 						{
918 							var htmlName = element.getName(),
919 								name = tagnameMap[ htmlName ] || false;
920 
921 							// Specialized anchor presents as email.
922 							if ( name == 'link' && element.getAttribute( 'href' ).indexOf( 'mailto:' ) === 0 )
923 								name = 'email';
924 							// Styled span could be either size or color.
925 							else if ( htmlName == 'span' )
926 							{
927 								if ( element.getStyle( 'font-size' ) )
928 									name = 'size';
929 								else if ( element.getStyle( 'color' ) )
930 									name = 'color';
931 							}
932 							else if ( name == 'img' )
933 							{
934 								var src = element.data( 'cke-saved-src' );
935 								if ( src && src.indexOf( editor.config.smiley_path ) === 0 )
936 									name = 'smiley';
937 							}
938 
939 							return name;
940 						});
941 			  }
942 		  }
943 	  } );
944 
945 })();
946