1 /*
  2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.
  3 For licensing, see LICENSE.html or http://ckeditor.com/license
  4 */
  5
  6 CKEDITOR.plugins.add( 'styles',
  7 {
  8 	requires : [ 'selection' ]
  9 });
 10
 11 /**
 12  * Registers a function to be called whenever a style changes its state in the
 13  * editing area. The current state is passed to the function. The possible
 14  * states are {@link CKEDITOR.TRISTATE_ON} and {@link CKEDITOR.TRISTATE_OFF}.
 15  * @param {CKEDITOR.style} The style to be watched.
 16  * @param {Function} The function to be called when the style state changes.
 17  * @example
 18  * // Create a style object for the <b> element.
 19  * var style = new CKEDITOR.style( { element : 'b' } );
 20  * var editor = CKEDITOR.instances.editor1;
 21  * editor.attachStyleStateChange( style, function( state )
 22  *     {
 23  *         if ( state == CKEDITOR.TRISTATE_ON )
 24  *             alert( 'The current state for the B element is ON' );
 25  *         else
 26  *             alert( 'The current state for the B element is OFF' );
 27  *     });
 28  */
 29 CKEDITOR.editor.prototype.attachStyleStateChange = function( style, callback )
 30 {
 31 	// Try to get the list of attached callbacks.
 32 	var styleStateChangeCallbacks = this._.styleStateChangeCallbacks;
 33
 34 	// If it doesn't exist, it means this is the first call. So, let's create
 35 	// all the structure to manage the style checks and the callback calls.
 36 	if ( !styleStateChangeCallbacks )
 37 	{
 38 		// Create the callbacks array.
 39 		styleStateChangeCallbacks = this._.styleStateChangeCallbacks = [];
 40
 41 		// Attach to the selectionChange event, so we can check the styles at
 42 		// that point.
 43 		this.on( 'selectionChange', function( ev )
 44 			{
 45 				// Loop throw all registered callbacks.
 46 				for ( var i = 0 ; i < styleStateChangeCallbacks.length ; i++ )
 47 				{
 48 					var callback = styleStateChangeCallbacks[ i ];
 49
 50 					// Check the current state for the style defined for that
 51 					// callback.
 52 					var currentState = callback.style.checkActive( ev.data.path ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF;
 53
 54 					// If the state changed since the last check.
 55 					if ( callback.state !== currentState )
 56 					{
 57 						// Call the callback function, passing the current
 58 						// state to it.
 59 						callback.fn.call( this, currentState );
 60
 61 						// Save the current state, so it can be compared next
 62 						// time.
 63 						callback.state !== currentState;
 64 					}
 65 				}
 66 			});
 67 	}
 68
 69 	// Save the callback info, so it can be checked on the next occurence of
 70 	// selectionChange.
 71 	styleStateChangeCallbacks.push( { style : style, fn : callback } );
 72 };
 73
 74 CKEDITOR.STYLE_BLOCK = 1;
 75 CKEDITOR.STYLE_INLINE = 2;
 76 CKEDITOR.STYLE_OBJECT = 3;
 77
 78 (function()
 79 {
 80 	var blockElements	= { address:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,p:1,pre:1 };
 81 	var objectElements	= { a:1,embed:1,hr:1,img:1,li:1,object:1,ol:1,table:1,td:1,tr:1,ul:1 };
 82
 83 	var semicolonFixRegex = /\s*(?:;\s*|$)/;
 84
 85 	CKEDITOR.style = function( styleDefinition, variablesValues )
 86 	{
 87 		if ( variablesValues )
 88 		{
 89 			styleDefinition = CKEDITOR.tools.clone( styleDefinition );
 90
 91 			replaceVariables( styleDefinition.attributes, variablesValues );
 92 			replaceVariables( styleDefinition.styles, variablesValues );
 93 		}
 94
 95 		var element = this.element = ( styleDefinition.element || '*' ).toLowerCase();
 96
 97 		this.type =
 98 			( element == '#' || blockElements[ element ] ) ?
 99 				CKEDITOR.STYLE_BLOCK
100 			: objectElements[ element ] ?
101 				CKEDITOR.STYLE_OBJECT
102 			:
103 				CKEDITOR.STYLE_INLINE;
104
105 		this._ =
106 		{
107 			definition : styleDefinition
108 		};
109 	};
110
111 	CKEDITOR.style.prototype =
112 	{
113 		apply : function( document )
114 		{
115 			applyStyle.call( this, document, false );
116 		},
117
118 		remove : function( document )
119 		{
120 			applyStyle.call( this, document, true );
121 		},
122
123 		applyToRange : function( range )
124 		{
125 			return ( this.applyToRange =
126 						this.type == CKEDITOR.STYLE_INLINE ?
127 							applyInlineStyle
128 						: this.type == CKEDITOR.STYLE_BLOCK ?
129 							applyBlockStyle
130 						: null ).call( this, range );
131 		},
132
133 		removeFromRange : function( range )
134 		{
135 			return ( this.removeFromRange =
136 						this.type == CKEDITOR.STYLE_INLINE ?
137 							removeInlineStyle
138 						: null ).call( this, range );
139 		},
140
141 		applyToObject : function( element )
142 		{
143 			setupElement( element, this );
144 		},
145
146 		/**
147 		 * Get the style state inside an element path. Returns "true" if the
148 		 * element is active in the path.
149 		 */
150 		checkActive : function( elementPath )
151 		{
152 			switch ( this.type )
153 			{
154 				case CKEDITOR.STYLE_BLOCK :
155 					return this.checkElementRemovable( elementPath.block || elementPath.blockLimit, true );
156
157 				case CKEDITOR.STYLE_INLINE :
158
159 					var elements = elementPath.elements;
160
161 					for ( var i = 0, element ; i < elements.length ; i++ )
162 					{
163 						element = elements[i];
164
165 						if ( element == elementPath.block || element == elementPath.blockLimit )
166 							continue;
167
168 						if ( this.checkElementRemovable( element, true ) )
169 							return true;
170 					}
171 			}
172 			return false;
173 		},
174
175 		// Checks if an element, or any of its attributes, is removable by the
176 		// current style definition.
177 		checkElementRemovable : function( element, fullMatch )
178 		{
179 			if ( !element || element.getName() != this.element )
180 				return false;
181
182 			var def = this._.definition,
183 				attribs;
184
185 			// If no attributes are defined in the element.
186 			if ( !fullMatch && !element.hasAttributes() )
187 				return true;
188
189 			attribs = getAttributesForComparison( def );
190
191 			if ( attribs._length )
192 			{
193 				for ( var attName in attribs )
194 				{
195 					if ( attName == '_length' )
196 						continue;
197
198 					if ( compareAttributeValues( attName, attribs[ attName ], element.getAttribute( attName ) ) )
199 					{
200 						if ( !fullMatch )
201 							return true;
202 					}
203 					else if ( fullMatch )
204 						return false;
205 				}
206 			}
207
208 			return true;
209 		}
210 	};
211
212 	// Build the cssText based on the styles definition.
213 	CKEDITOR.style.getStyleText = function( styleDefinition )
214 	{
215 		// If we have already computed it, just return it.
216 		var stylesDef = styleDefinition._ST;
217 		if ( stylesDef )
218 			return stylesDef;
219
220 		stylesDef = styleDefinition.styles;
221
222 		// Builds the StyleText.
223
224 		var stylesText = ( styleDefinition.attributes && styleDefinition.attributes[ 'style' ] ) || '';
225
226 		if ( stylesText.length )
227 			stylesText = stylesText.replace( semicolonFixRegex, ';' );
228
229 		for ( var style in stylesDef )
230 			stylesText += style + ':' + stylesDef[ style ] + ';';
231
232 		// Browsers make some changes to the style when applying them. So, here
233 		// we normalize it to the browser format.
234 		if ( stylesText.length )
235 		{
236 			stylesText = normalizeCssText( stylesText );
237
238 			if ( stylesText.length )
239 				stylesText = stylesText.replace( semicolonFixRegex, ';' );
240 		}
241
242 		// Return it, saving it to the next request.
243 		return ( styleDefinition._ST = stylesText );
244 	};
245
246 	function applyInlineStyle( range )
247 	{
248 		var document = range.document;
249
250 		if ( range.collapsed )
251 		{
252 			// Create the element to be inserted in the DOM.
253 			var collapsedElement = getElement( this, document );
254
255 			// Insert the empty element into the DOM at the range position.
256 			range.insertNode( collapsedElement );
257
258 			// Place the selection right inside the empty element.
259 			range.moveToPosition( collapsedElement, CKEDITOR.POSITION_BEFORE_END );
260
261 			return;
262 		}
263
264 		var elementName = this.element;
265 		var def = this._.definition;
266 		var isUnknownElement;
267
268 		// Get the DTD definition for the element. Defaults to "span".
269 		var dtd = CKEDITOR.dtd[ elementName ] || ( isUnknownElement = true, CKEDITOR.dtd.span );
270
271 		// Bookmark the range so we can re-select it after processing.
272 		var bookmark = range.createBookmark();
273
274 		// Expand the range.
275 		range.enlarge( CKEDITOR.ENLARGE_ELEMENT );
276 		range.trim();
277
278 		// Get the first node to be processed and the last, which concludes the
279 		// processing.
280 		var firstNode = range.startContainer.getChild( range.startOffset ) || range.startContainer.getNextSourceNode();
281 		var lastNode = range.endContainer.getChild( range.endOffset ) || ( range.endOffset ? range.endContainer.getNextSourceNode() : range.endContainer );
282
283 		if ( lastNode.equals( firstNode ) )
284 		{
285 			// If the last node is the same as the the first one, we must move
286 			// it to the next one, otherwise the first one will not be
287 			// processed.
288 			lastNode = lastNode.getNextSourceNode( true );
289
290 			// It may happen that there are no more nodes after it (the end of
291 			// the document), so we must add something there to make our code
292 			// simpler.
293 			if ( !lastNode )
294 			{
295 				lastNode = document.createText( '' );
296 				lastNode.insertAfter( firstNode );
297 			}
298 		}
299
300 		var currentNode = firstNode;
301
302 		var styleRange;
303
304 		// Indicates that that some useful inline content has been found, so
305 		// the style should be applied.
306 		var hasContents;
307
308 		while ( currentNode )
309 		{
310 			var applyStyle = false;
311
312 			if ( currentNode.equals( lastNode ) )
313 			{
314 				currentNode = null;
315 				applyStyle = true;
316 			}
317 			else
318 			{
319 				var nodeType = currentNode.type;
320 				var nodeName = nodeType == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null;
321
322 				if ( nodeName && currentNode.getAttribute( '_fck_bookmark' ) )
323 				{
324 					currentNode = currentNode.getNextSourceNode( true );
325 					continue;
326 				}
327
328 				// Check if the current node can be a child of the style element.
329 				if ( !nodeName || ( dtd[ nodeName ] && ( currentNode.getPosition( lastNode ) | CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) )
330 				{
331 					var currentParent = currentNode.getParent();
332
333 					// Check if the style element can be a child of the current
334 					// node parent or if the element is not defined in the DTD.
335 					if ( currentParent && ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) )
336 					{
337 						// This node will be part of our range, so if it has not
338 						// been started, place its start right before the node.
339 						// In the case of an element node, it will be included
340 						// only if it is entirely inside the range.
341 						if ( !styleRange && ( !nodeName || !CKEDITOR.dtd.$removeEmpty[ nodeName ] || ( currentNode.getPosition( lastNode ) | CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) )
342 						{
343 							styleRange = new CKEDITOR.dom.range( document );
344 							styleRange.setStartBefore( currentNode );
345 						}
346
347 						// Non element nodes, or empty elements can be added
348 						// completely to the range.
349 						if ( nodeType == CKEDITOR.NODE_TEXT || ( nodeType == CKEDITOR.NODE_ELEMENT && !currentNode.getChildCount() && currentNode.$.offsetWidth ) )
350 						{
351 							var includedNode = currentNode;
352 							var parentNode;
353
354 							// This node is about to be included completelly, but,
355 							// if this is the last node in its parent, we must also
356 							// check if the parent itself can be added completelly
357 							// to the range.
358 							while ( !includedNode.$.nextSibling
359 								&& ( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] )
360 								&& ( parentNode.getPosition( firstNode ) | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_FOLLOWING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) )
361 							{
362 								includedNode = parentNode;
363 							}
364
365 							styleRange.setEndAfter( includedNode );
366
367 							// If the included node still is the last node in its
368 							// parent, it means that the parent can't be included
369 							// in this style DTD, so apply the style immediately.
370 							if ( !includedNode.$.nextSibling )
371 								applyStyle = true;
372
373 							if ( !hasContents )
374 								hasContents = ( nodeType != CKEDITOR.NODE_TEXT || (/[^\s\ufeff]/).test( currentNode.getText() ) );
375 						}
376 					}
377 					else
378 						applyStyle = true;
379 				}
380 				else
381 					applyStyle = true;
382
383 				// Get the next node to be processed.
384 				currentNode = currentNode.getNextSourceNode();
385 			}
386
387 			// Apply the style if we have something to which apply it.
388 			if ( applyStyle && hasContents && styleRange && !styleRange.collapsed )
389 			{
390 				// Build the style element, based on the style object definition.
391 				var styleNode = getElement( this, document );
392
393 				// Get the element that holds the entire range.
394 				var parent = styleRange.getCommonAncestor();
395
396 				// Loop through the parents, removing the redundant attributes
397 				// from the element to be applied.
398 				while ( styleNode && parent )
399 				{
400 					if ( parent.getName() == elementName )
401 					{
402 						for ( var attName in def.attribs )
403 						{
404 							if ( styleNode.getAttribute( attName ) == parent.getAttribute( attName ) )
405 								styleNode.removeAttribute( attName );
406 						}
407
408 						for ( var styleName in def.styles )
409 						{
410 							if ( styleNode.getStyle( styleName ) == parent.getStyle( styleName ) )
411 								styleNode.removeStyle( styleName );
412 						}
413
414 						if ( !styleNode.hasAttributes() )
415 						{
416 							styleNode = null;
417 							break;
418 						}
419 					}
420
421 					parent = parent.getParent();
422 				}
423
424 				if ( styleNode )
425 				{
426 					// Move the contents of the range to the style element.
427 					styleRange.extractContents().appendTo( styleNode );
428
429 					// Here we do some cleanup, removing all duplicated
430 					// elements from the style element.
431 					removeFromInsideElement( this, styleNode );
432
433 					// Insert it into the range position (it is collapsed after
434 					// extractContents.
435 					styleRange.insertNode( styleNode );
436
437 					// Let's merge our new style with its neighbors, if possible.
438 					mergeSiblings( styleNode );
439
440 					// As the style system breaks text nodes constantly, let's normalize
441 					// things for performance.
442 					// With IE, some paragraphs get broken when calling normalize()
443 					// repeatedly. Also, for IE, we must normalize body, not documentElement.
444 					// IE is also known for having a "crash effect" with normalize().
445 					// We should try to normalize with IE too in some way, somewhere.
446 					if ( !CKEDITOR.env.ie )
447 						styleNode.$.normalize();
448 				}
449
450 				// Style applied, let's release the range, so it gets
451 				// re-initialization in the next loop.
452 				styleRange = null;
453 			}
454 		}
455
456 //		this._FixBookmarkStart( startNode );
457
458 		range.moveToBookmark( bookmark );
459 	}
460
461 	function removeInlineStyle( range )
462 	{
463 		/*
464 		 * Make sure our range has included all "collpased" parent inline nodes so
465 		 * that our operation logic can be simpler.
466 		 */
467 		range.enlarge( CKEDITOR.ENLARGE_ELEMENT );
468
469 		var bookmark = range.createBookmark( true ),
470 			startNode = range.document.getById( bookmark.startNode );
471
472 		if ( range.collapsed )
473 		{
474 			/*
475 			 * If the range is collapsed, try to remove the style from all ancestor
476 			 * elements, until a block boundary is reached.
477 			 */
478 			var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() );
479 			for ( var i = 0, element ; i < startPath.elements.length && ( element = startPath.elements[i] )  ; i++ )
480 			{
481 				if ( element == startPath.block || element == startPath.blockLimit )
482 					break;
483
484 				if ( this.checkElementRemovable( element ) )
485 				{
486 					/*
487 					 * Before removing the style node, there may be a sibling to the style node
488 					 * that's exactly the same to the one to be removed. To the user, it makes
489 					 * no difference that they're separate entities in the DOM tree. So, merge
490 					 * them before removal.
491 					 */
492 					mergeSiblings( element );
493 					removeFromElement( this, element );
494 				}
495 			}
496 		}
497 		else
498 		{
499 			/*
500 			 * Now our range isn't collapsed. Lets walk from the start node to the end
501 			 * node via DFS and remove the styles one-by-one.
502 			 */
503 			var endNode = range.document.getById( bookmark.endNode ),
504 				me = this;
505
506 			/*
507 			 * Find out the style ancestor that needs to be broken down at startNode
508 			 * and endNode.
509 			 */
510 			function breakNodes()
511 			{
512 				var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ),
513 					endPath = new CKEDITOR.dom.elementPath( endNode.getParent() ),
514 					breakStart = null,
515 					breakEnd = null;
516 				for ( var i = 0 ; i < startPath.elements.length ; i++ )
517 				{
518 					var element = startPath.elements[ i ];
519
520 					if ( element == startPath.block || element == startPath.blockLimit )
521 						break;
522
523 					if ( me.checkElementRemovable( element ) )
524 						breakStart = element;
525 				}
526 				for ( var i = 0 ; i < endPath.elements.length ; i++ )
527 				{
528 					var element = endPath.elements[ i ];
529
530 					if ( element == endPath.block || element == endPath.blockLimit )
531 						break;
532
533 					if ( me.checkElementRemovable( element ) )
534 						breakEnd = element;
535 				}
536
537 				if ( breakEnd )
538 					endNode.breakParent( breakEnd );
539 				if ( breakStart )
540 					startNode.breakParent( breakStart );
541 			}
542 			breakNodes();
543
544 			// Now, do the DFS walk.
545 			var currentNode = startNode.getNext();
546 			while ( !currentNode.equals( endNode ) )
547 			{
548 				/*
549 				 * Need to get the next node first because removeFromElement() can remove
550 				 * the current node from DOM tree.
551 				 */
552 				var nextNode = currentNode.getNextSourceNode();
553 				if ( currentNode.type == CKEDITOR.NODE_ELEMENT && this.checkElementRemovable( currentNode ) )
554 				{
555 					removeFromElement( this, currentNode );
556
557 					/*
558 					 * removeFromElement() may have merged the next node with something before
559 					 * the startNode via mergeSiblings(). In that case, the nextNode would
560 					 * contain startNode and we'll have to call breakNodes() again and also
561 					 * reassign the nextNode to something after startNode.
562 					 */
563 					if ( nextNode.type == CKEDITOR.NODE_ELEMENT && nextNode.contains( startNode ) )
564 					{
565 						breakNodes();
566 						nextNode = startNode.getNext();
567 					}
568 				}
569 				currentNode = nextNode;
570 			}
571 		}
572
573 		range.moveToBookmark( bookmark );
574 	}
575
576 	function applyBlockStyle( range )
577 	{
578 		// Bookmark the range so we can re-select it after processing.
579 		var bookmark = range.createBookmark();
580
581 		var iterator = range.createIterator();
582 		iterator.enforceRealBlocks = true;
583
584 		var block;
585 		var doc = range.document;
586 		var previousPreBlock;
587
588 		while( ( block = iterator.getNextParagraph() ) )		// Only one =
589 		{
590 			// Create the new node right before the current one.
591 			var newBlock = getElement( this, doc );
592
593 			// Check if we are changing from/to <pre>.
594 //			var newBlockIsPre	= newBlock.nodeName.IEquals( 'pre' );
595 //			var blockIsPre		= block.nodeName.IEquals( 'pre' );
596
597 //			var toPre	= newBlockIsPre && !blockIsPre;
598 //			var fromPre	= !newBlockIsPre && blockIsPre;
599
600 			// Move everything from the current node to the new one.
601 //			if ( toPre )
602 //				newBlock = this._ToPre( doc, block, newBlock );
603 //			else if ( fromPre )
604 //				newBlock = this._FromPre( doc, block, newBlock );
605 //			else	// Convering from a regular block to another regular block.
606 				block.moveChildren( newBlock );
607
608 			// Replace the current block.
609 			newBlock.insertBefore( block );
610 			block.remove();
611
612 			// Complete other tasks after inserting the node in the DOM.
613 //			if ( newBlockIsPre )
614 //			{
615 //				if ( previousPreBlock )
616 //					this._CheckAndMergePre( previousPreBlock, newBlock ) ;	// Merge successive <pre> blocks.
617 //				previousPreBlock = newBlock;
618 //			}
619 //			else if ( fromPre )
620 //				this._CheckAndSplitPre( newBlock ) ;	// Split <br><br> in successive <pre>s.
621 		}
622
623 		range.moveToBookmark( bookmark );
624 	}
625
626 	// Removes a style from an element itself, don't care about its subtree.
627 	function removeFromElement( style, element )
628 	{
629 		var def = style._.definition,
630 			attributes = def.attributes,
631 			styles = def.styles;
632
633 		for ( var attName in attributes )
634 		{
635 			// The 'class' element value must match (#1318).
636 			if ( attName == 'class' && element.getAttribute( attName ) != attributes[ attName ] )
637 				continue;
638 			element.removeAttribute( attName );
639 		}
640
641 		for ( var styleName in styles )
642 			element.removeStyle( styleName );
643
644 		removeNoAttribsElement( element );
645 	}
646
647 	// Removes a style from inside an element.
648 	function removeFromInsideElement( style, element )
649 	{
650 		var def = style._.definition;
651 		var attribs = def.attributes;
652 		var styles = def.styles;
653
654 		var innerElements = element.getElementsByTag( style.element );
655
656 		for ( var i = innerElements.count() ; --i >= 0 ; )
657 			removeFromElement( style, innerElements.getItem( i ) );
658 	}
659
660 	// If the element has no more attributes, remove it.
661 	function removeNoAttribsElement( element )
662 	{
663 		// If no more attributes remained in the element, remove it,
664 		// leaving its children.
665 		if ( !element.hasAttributes() )
666 		{
667 			// Removing elements may open points where merging is possible,
668 			// so let's cache the first and last nodes for later checking.
669 			var firstChild	= element.getFirst();
670 			var lastChild	= element.getLast();
671
672 			element.remove( true );
673
674 			if ( firstChild )
675 			{
676 				// Check the cached nodes for merging.
677 				mergeSiblings( firstChild );
678
679 				if ( lastChild && !firstChild.equals( lastChild ) )
680 					mergeSiblings( lastChild );
681 			}
682 		}
683 	}
684
685 	function mergeSiblings( element )
686 	{
687 		if ( !element || element.type != CKEDITOR.NODE_ELEMENT || !CKEDITOR.dtd.$removeEmpty[ element.getName() ] )
688 			return;
689
690 		mergeElements( element, element.getNext(), true );
691 		mergeElements( element, element.getPrevious() );
692 	}
693
694 	function mergeElements( element, sibling, isNext )
695 	{
696 		if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT )
697 		{
698 			var hasBookmark = sibling.getAttribute( '_fck_bookmark' );
699
700 			if ( hasBookmark )
701 				sibling = isNext ? sibling.getNext() : sibling.getPrevious();
702
703 			if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && element.isIdentical( sibling ) )
704 			{
705 				// Save the last child to be checked too, to merge things like
706 				// <b><i></i></b><b><i></i></b> => <b><i></i></b>
707 				var innerSibling = isNext ? element.getLast() : element.getFirst();
708
709 				if ( hasBookmark )
710 					( isNext ? sibling.getPrevious() : sibling.getNext() ).move( element, !isNext );
711
712 				sibling.moveChildren( element, !isNext );
713 				sibling.remove();
714
715 				// Now check the last inner child (see two comments above).
716 				if ( innerSibling )
717 					mergeSiblings( innerSibling );
718 			}
719 		}
720 	}
721
722 	function getElement( style, targetDocument )
723 	{
724 		var el;
725
726 		var def = style._.definition;
727
728 		var elementName = style.element;
729
730 		// The "*" element name will always be a span for this function.
731 		if ( elementName == '*' )
732 			elementName = 'span';
733
734 		// Create the element.
735 		el = new CKEDITOR.dom.element( elementName, targetDocument );
736
737 		return setupElement( el, style );
738 	}
739
740 	function setupElement( el, style )
741 	{
742 		var def = style._.definition;
743 		var attributes = def.attributes;
744 		var styles = CKEDITOR.style.getStyleText( def );
745
746 		// Assign all defined attributes.
747 		if ( attributes )
748 		{
749 			for ( var att in attributes )
750 			{
751 				el.setAttribute( att, attributes[ att ] );
752 			}
753 		}
754
755 		// Assign all defined styles.
756 		if ( styles )
757 			el.setAttribute( 'style', styles );
758
759 		return el;
760 	}
761
762 	var varRegex = /#\((.+?)\)/g;
763 	function replaceVariables( list, variablesValues )
764 	{
765 		for ( var item in list )
766 		{
767 			list[ item ] = list[ item ].replace( varRegex, function( match, varName )
768 				{
769 					return variablesValues[ varName ];
770 				});
771 		}
772 	}
773
774 	var spacesRegex = /\s+/g;
775
776 	// Returns an object that can be used for style matching comparison.
777 	// Attributes names and values are all lowercased, and the styles get
778 	// merged with the style attribute.
779 	function getAttributesForComparison( styleDefinition )
780 	{
781 		// If we have already computed it, just return it.
782 		var attribs = styleDefinition._AC;
783 		if ( attribs )
784 			return attribs;
785
786 		attribs = {};
787
788 		var length = 0;
789
790 		// Loop through all defined attributes.
791 		var styleAttribs = styleDefinition.attributes;
792 		if ( styleAttribs )
793 		{
794 			for ( var styleAtt in styleAttribs )
795 			{
796 				length++;
797 				attribs[ styleAtt.toLowerCase() ] = styleAttribs[ styleAtt ].toLowerCase();
798 			}
799 		}
800
801 		// Includes the style definitions.
802 		var styleText = CKEDITOR.style.getStyleText( styleDefinition );
803 		if ( styleText.length > 0 )
804 		{
805 			if ( !attribs[ 'style' ] )
806 				length++;
807
808 			attribs['style'] = styleText.replace( spacesRegex, '' ).toLowerCase();
809 		}
810
811 		// Appends the "length" information to the object.
812 		attribs._length = length;
813
814 		// Return it, saving it to the next request.
815 		return ( styleDefinition._AC = attribs );
816 	}
817
818 	function normalizeCssText( unparsedCssText )
819 	{
820 		// Injects the style in a temporary span object, so the browser parses it,
821 		// retrieving its final format.
822 		var tempSpan = document.createElement( 'span' );
823 		tempSpan.style.cssText = unparsedCssText;
824 		return tempSpan.style.cssText;
825 	}
826
827 	// valueA is our internal "for comparison" value.
828 	// valueB is the value retrieved from the element.
829 	function compareAttributeValues( attName, valueA, valueB )
830 	{
831 		if ( valueA == valueB || ( !valueA && !valueB ) )
832 			return true;
833 		else if ( !valueA || !valueB )
834 			return false;
835
836 		valueB = valueB.toLowerCase();
837
838 		if ( attName == 'style' )
839 		{
840 			valueB = valueB.replace( spacesRegex, '' );
841 			if ( valueB.charAt( valueB.length - 1 ) != ';' )
842 				valueB += ';';
843 		}
844
845 		// Return true if they match or if valueA is null and valueB is an empty string
846 		return ( valueA == valueB );
847 	}
848
849 	function applyStyle( document, remove )
850 	{
851 		// Get all ranges from the selection.
852 		var selection = document.getSelection();
853 		var ranges = selection.getRanges();
854 		var func = remove ? this.removeFromRange : this.applyToRange;
855
856 		// Apply the style to the ranges.
857 		for ( var i = 0 ; i < ranges.length ; i++ )
858 			func.call( this, ranges[ i ] );
859
860 		// Select the ranges again.
861 		selection.selectRanges( ranges );
862 	}
863 })();
864
865 CKEDITOR.styleCommand = function( style )
866 {
867 	this.style = style;
868 };
869
870 CKEDITOR.styleCommand.prototype.exec = function( editor )
871 {
872 	editor.focus();
873
874 	var doc = editor.document;
875
876 	if ( doc )
877 	{
878 		if ( this.state == CKEDITOR.TRISTATE_OFF )
879 			this.style.apply( doc );
880 		else if ( this.state == CKEDITOR.TRISTATE_ON )
881 			this.style.remove( doc );
882 	}
883
884 	return !!doc;
885 };
886