JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.1
[ckeditor.git] / _source / plugins / styles / plugin.js
1 /*\r
2 Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license\r
4 */\r
5 \r
6 CKEDITOR.plugins.add( 'styles',\r
7 {\r
8         requires : [ 'selection' ]\r
9 });\r
10 \r
11 /**\r
12  * Registers a function to be called whenever a style changes its state in the\r
13  * editing area. The current state is passed to the function. The possible\r
14  * states are {@link CKEDITOR.TRISTATE_ON} and {@link CKEDITOR.TRISTATE_OFF}.\r
15  * @param {CKEDITOR.style} The style to be watched.\r
16  * @param {Function} The function to be called when the style state changes.\r
17  * @example\r
18  * // Create a style object for the <b> element.\r
19  * var style = new CKEDITOR.style( { element : 'b' } );\r
20  * var editor = CKEDITOR.instances.editor1;\r
21  * editor.attachStyleStateChange( style, function( state )\r
22  *     {\r
23  *         if ( state == CKEDITOR.TRISTATE_ON )\r
24  *             alert( 'The current state for the B element is ON' );\r
25  *         else\r
26  *             alert( 'The current state for the B element is OFF' );\r
27  *     });\r
28  */\r
29 CKEDITOR.editor.prototype.attachStyleStateChange = function( style, callback )\r
30 {\r
31         // Try to get the list of attached callbacks.\r
32         var styleStateChangeCallbacks = this._.styleStateChangeCallbacks;\r
33 \r
34         // If it doesn't exist, it means this is the first call. So, let's create\r
35         // all the structure to manage the style checks and the callback calls.\r
36         if ( !styleStateChangeCallbacks )\r
37         {\r
38                 // Create the callbacks array.\r
39                 styleStateChangeCallbacks = this._.styleStateChangeCallbacks = [];\r
40 \r
41                 // Attach to the selectionChange event, so we can check the styles at\r
42                 // that point.\r
43                 this.on( 'selectionChange', function( ev )\r
44                         {\r
45                                 // Loop throw all registered callbacks.\r
46                                 for ( var i = 0 ; i < styleStateChangeCallbacks.length ; i++ )\r
47                                 {\r
48                                         var callback = styleStateChangeCallbacks[ i ];\r
49 \r
50                                         // Check the current state for the style defined for that\r
51                                         // callback.\r
52                                         var currentState = callback.style.checkActive( ev.data.path ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF;\r
53 \r
54                                         // If the state changed since the last check.\r
55                                         if ( callback.state !== currentState )\r
56                                         {\r
57                                                 // Call the callback function, passing the current\r
58                                                 // state to it.\r
59                                                 callback.fn.call( this, currentState );\r
60 \r
61                                                 // Save the current state, so it can be compared next\r
62                                                 // time.\r
63                                                 callback.state !== currentState;\r
64                                         }\r
65                                 }\r
66                         });\r
67         }\r
68 \r
69         // Save the callback info, so it can be checked on the next occurence of\r
70         // selectionChange.\r
71         styleStateChangeCallbacks.push( { style : style, fn : callback } );\r
72 };\r
73 \r
74 CKEDITOR.STYLE_BLOCK = 1;\r
75 CKEDITOR.STYLE_INLINE = 2;\r
76 CKEDITOR.STYLE_OBJECT = 3;\r
77 \r
78 (function()\r
79 {\r
80         var blockElements       = { address:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,p:1,pre:1 };\r
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 };\r
82 \r
83         var semicolonFixRegex = /\s*(?:;\s*|$)/;\r
84 \r
85         CKEDITOR.style = function( styleDefinition, variablesValues )\r
86         {\r
87                 if ( variablesValues )\r
88                 {\r
89                         styleDefinition = CKEDITOR.tools.clone( styleDefinition );\r
90 \r
91                         replaceVariables( styleDefinition.attributes, variablesValues );\r
92                         replaceVariables( styleDefinition.styles, variablesValues );\r
93                 }\r
94 \r
95                 var element = this.element = ( styleDefinition.element || '*' ).toLowerCase();\r
96 \r
97                 this.type =\r
98                         ( element == '#' || blockElements[ element ] ) ?\r
99                                 CKEDITOR.STYLE_BLOCK\r
100                         : objectElements[ element ] ?\r
101                                 CKEDITOR.STYLE_OBJECT\r
102                         :\r
103                                 CKEDITOR.STYLE_INLINE;\r
104 \r
105                 this._ =\r
106                 {\r
107                         definition : styleDefinition\r
108                 };\r
109         };\r
110 \r
111         CKEDITOR.style.prototype =\r
112         {\r
113                 apply : function( document )\r
114                 {\r
115                         applyStyle.call( this, document, false );\r
116                 },\r
117 \r
118                 remove : function( document )\r
119                 {\r
120                         applyStyle.call( this, document, true );\r
121                 },\r
122 \r
123                 applyToRange : function( range )\r
124                 {\r
125                         return ( this.applyToRange =\r
126                                                 this.type == CKEDITOR.STYLE_INLINE ?\r
127                                                         applyInlineStyle\r
128                                                 : this.type == CKEDITOR.STYLE_BLOCK ?\r
129                                                         applyBlockStyle\r
130                                                 : null ).call( this, range );\r
131                 },\r
132 \r
133                 removeFromRange : function( range )\r
134                 {\r
135                         return ( this.removeFromRange =\r
136                                                 this.type == CKEDITOR.STYLE_INLINE ?\r
137                                                         removeInlineStyle\r
138                                                 : null ).call( this, range );\r
139                 },\r
140 \r
141                 applyToObject : function( element )\r
142                 {\r
143                         setupElement( element, this );\r
144                 },\r
145 \r
146                 /**\r
147                  * Get the style state inside an element path. Returns "true" if the\r
148                  * element is active in the path.\r
149                  */\r
150                 checkActive : function( elementPath )\r
151                 {\r
152                         switch ( this.type )\r
153                         {\r
154                                 case CKEDITOR.STYLE_BLOCK :\r
155                                         return this.checkElementRemovable( elementPath.block || elementPath.blockLimit, true );\r
156 \r
157                                 case CKEDITOR.STYLE_INLINE :\r
158 \r
159                                         var elements = elementPath.elements;\r
160 \r
161                                         for ( var i = 0, element ; i < elements.length ; i++ )\r
162                                         {\r
163                                                 element = elements[i];\r
164 \r
165                                                 if ( element == elementPath.block || element == elementPath.blockLimit )\r
166                                                         continue;\r
167 \r
168                                                 if ( this.checkElementRemovable( element, true ) )\r
169                                                         return true;\r
170                                         }\r
171                         }\r
172                         return false;\r
173                 },\r
174 \r
175                 // Checks if an element, or any of its attributes, is removable by the\r
176                 // current style definition.\r
177                 checkElementRemovable : function( element, fullMatch )\r
178                 {\r
179                         if ( !element )\r
180                                 return false;\r
181 \r
182                         var def = this._.definition,\r
183                                 attribs;\r
184 \r
185                         // If the element name is the same as the style name.\r
186                         if ( element.getName() == this.element )\r
187                         {\r
188                                 // If no attributes are defined in the element.\r
189                                 if ( !fullMatch && !element.hasAttributes() )\r
190                                         return true;\r
191 \r
192                                 attribs = getAttributesForComparison( def );\r
193 \r
194                                 if ( attribs._length )\r
195                                 {\r
196                                         for ( var attName in attribs )\r
197                                         {\r
198                                                 if ( attName == '_length' )\r
199                                                         continue;\r
200 \r
201                                                 var elementAttr = element.getAttribute( attName ) || '';\r
202                                                 if ( attribs[ attName ] ==\r
203                                                          ( attName == 'style' ?\r
204                                                            normalizeCssText( elementAttr, false ) : elementAttr  ) )\r
205                                                 {\r
206                                                         if ( !fullMatch )\r
207                                                                 return true;\r
208                                                 }\r
209                                                 else if ( fullMatch )\r
210                                                                 return false;\r
211                                         }\r
212                                         if( fullMatch )\r
213                                                 return true;\r
214                                 }\r
215                                 else\r
216                                         return true;\r
217                         }\r
218 \r
219                         // Check if the element can be somehow overriden.\r
220                         var override = getOverrides( this )[ element.getName() ] ;\r
221                         if ( override )\r
222                         {\r
223                                 // If no attributes have been defined, remove the element.\r
224                                 if ( !( attribs = override.attributes ) )\r
225                                         return true;\r
226 \r
227                                 for ( var i = 0 ; i < attribs.length ; i++ )\r
228                                 {\r
229                                         attName = attribs[i][0];\r
230                                         var actualAttrValue = element.getAttribute( attName );\r
231                                         if ( actualAttrValue )\r
232                                         {\r
233                                                 var attValue = attribs[i][1];\r
234 \r
235                                                 // Remove the attribute if:\r
236                                                 //    - The override definition value is null;\r
237                                                 //    - The override definition value is a string that\r
238                                                 //      matches the attribute value exactly.\r
239                                                 //    - The override definition value is a regex that\r
240                                                 //      has matches in the attribute value.\r
241                                                 if ( attValue === null ||\r
242                                                                 ( typeof attValue == 'string' && actualAttrValue == attValue ) ||\r
243                                                                 attValue.test( actualAttrValue ) )\r
244                                                         return true;\r
245                                         }\r
246                                 }\r
247                         }\r
248                         return false;\r
249                 }\r
250         };\r
251 \r
252         // Build the cssText based on the styles definition.\r
253         CKEDITOR.style.getStyleText = function( styleDefinition )\r
254         {\r
255                 // If we have already computed it, just return it.\r
256                 var stylesDef = styleDefinition._ST;\r
257                 if ( stylesDef )\r
258                         return stylesDef;\r
259 \r
260                 stylesDef = styleDefinition.styles;\r
261 \r
262                 // Builds the StyleText.\r
263 \r
264                 var stylesText = ( styleDefinition.attributes && styleDefinition.attributes[ 'style' ] ) || '';\r
265 \r
266                 if ( stylesText.length )\r
267                         stylesText = stylesText.replace( semicolonFixRegex, ';' );\r
268 \r
269                 for ( var style in stylesDef )\r
270                         stylesText += ( style + ':' + stylesDef[ style ] ).replace( semicolonFixRegex, ';' );\r
271 \r
272                 // Browsers make some changes to the style when applying them. So, here\r
273                 // we normalize it to the browser format.\r
274                 if ( stylesText.length )\r
275                         stylesText = normalizeCssText( stylesText );\r
276 \r
277                 // Return it, saving it to the next request.\r
278                 return ( styleDefinition._ST = stylesText );\r
279         };\r
280 \r
281         function applyInlineStyle( range )\r
282         {\r
283                 var document = range.document;\r
284 \r
285                 if ( range.collapsed )\r
286                 {\r
287                         // Create the element to be inserted in the DOM.\r
288                         var collapsedElement = getElement( this, document );\r
289 \r
290                         // Insert the empty element into the DOM at the range position.\r
291                         range.insertNode( collapsedElement );\r
292 \r
293                         // Place the selection right inside the empty element.\r
294                         range.moveToPosition( collapsedElement, CKEDITOR.POSITION_BEFORE_END );\r
295 \r
296                         return;\r
297                 }\r
298 \r
299                 var elementName = this.element;\r
300                 var def = this._.definition;\r
301                 var isUnknownElement;\r
302 \r
303                 // Get the DTD definition for the element. Defaults to "span".\r
304                 var dtd = CKEDITOR.dtd[ elementName ] || ( isUnknownElement = true, CKEDITOR.dtd.span );\r
305 \r
306                 // Bookmark the range so we can re-select it after processing.\r
307                 var bookmark = range.createBookmark();\r
308 \r
309                 // Expand the range.\r
310                 range.enlarge( CKEDITOR.ENLARGE_ELEMENT );\r
311                 range.trim();\r
312 \r
313                 // Get the first node to be processed and the last, which concludes the\r
314                 // processing.\r
315                 var boundaryNodes = range.getBoundaryNodes();\r
316                 var firstNode = boundaryNodes.startNode;\r
317                 var lastNode = boundaryNodes.endNode.getNextSourceNode( true );\r
318 \r
319                 // Probably the document end is reached, we need a marker node.\r
320                 if ( !lastNode )\r
321                 {\r
322                                 var marker;\r
323                                 lastNode = marker = document.createText( '' );\r
324                                 lastNode.insertAfter( range.endContainer );\r
325                 }\r
326                 // The detection algorithm below skips the contents inside bookmark nodes, so\r
327                 // we'll need to make sure lastNode isn't the &nbsp; inside a bookmark node.\r
328                 var lastParent = lastNode.getParent();\r
329                 if ( lastParent && lastParent.getAttribute( '_fck_bookmark' ) )\r
330                         lastNode = lastParent;\r
331 \r
332                 if ( lastNode.equals( firstNode ) )\r
333                 {\r
334                         // If the last node is the same as the the first one, we must move\r
335                         // it to the next one, otherwise the first one will not be\r
336                         // processed.\r
337                         lastNode = lastNode.getNextSourceNode( true );\r
338 \r
339                         // It may happen that there are no more nodes after it (the end of\r
340                         // the document), so we must add something there to make our code\r
341                         // simpler.\r
342                         if ( !lastNode )\r
343                         {\r
344                                 lastNode = marker = document.createText( '' );\r
345                                 lastNode.insertAfter( firstNode );\r
346                         }\r
347                 }\r
348 \r
349                 var currentNode = firstNode;\r
350 \r
351                 var styleRange;\r
352 \r
353                 // Indicates that that some useful inline content has been found, so\r
354                 // the style should be applied.\r
355                 var hasContents;\r
356 \r
357                 while ( currentNode )\r
358                 {\r
359                         var applyStyle = false;\r
360 \r
361                         if ( currentNode.equals( lastNode ) )\r
362                         {\r
363                                 currentNode = null;\r
364                                 applyStyle = true;\r
365                         }\r
366                         else\r
367                         {\r
368                                 var nodeType = currentNode.type;\r
369                                 var nodeName = nodeType == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null;\r
370 \r
371                                 if ( nodeName && currentNode.getAttribute( '_fck_bookmark' ) )\r
372                                 {\r
373                                         currentNode = currentNode.getNextSourceNode( true );\r
374                                         continue;\r
375                                 }\r
376 \r
377                                 // Check if the current node can be a child of the style element.\r
378                                 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 ) ) )\r
379                                 {\r
380                                         var currentParent = currentNode.getParent();\r
381 \r
382                                         // Check if the style element can be a child of the current\r
383                                         // node parent or if the element is not defined in the DTD.\r
384                                         if ( currentParent && ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) )\r
385                                         {\r
386                                                 // This node will be part of our range, so if it has not\r
387                                                 // been started, place its start right before the node.\r
388                                                 // In the case of an element node, it will be included\r
389                                                 // only if it is entirely inside the range.\r
390                                                 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 ) ) )\r
391                                                 {\r
392                                                         styleRange = new CKEDITOR.dom.range( document );\r
393                                                         styleRange.setStartBefore( currentNode );\r
394                                                 }\r
395 \r
396                                                 // Non element nodes, or empty elements can be added\r
397                                                 // completely to the range.\r
398                                                 if ( nodeType == CKEDITOR.NODE_TEXT || ( nodeType == CKEDITOR.NODE_ELEMENT && !currentNode.getChildCount() ) )\r
399                                                 {\r
400                                                         var includedNode = currentNode;\r
401                                                         var parentNode;\r
402 \r
403                                                         // This node is about to be included completelly, but,\r
404                                                         // if this is the last node in its parent, we must also\r
405                                                         // check if the parent itself can be added completelly\r
406                                                         // to the range.\r
407                                                         while ( !includedNode.$.nextSibling\r
408                                                                 && ( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] )\r
409                                                                 && ( parentNode.getPosition( firstNode ) | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_FOLLOWING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) )\r
410                                                         {\r
411                                                                 includedNode = parentNode;\r
412                                                         }\r
413 \r
414                                                         styleRange.setEndAfter( includedNode );\r
415 \r
416                                                         // If the included node still is the last node in its\r
417                                                         // parent, it means that the parent can't be included\r
418                                                         // in this style DTD, so apply the style immediately.\r
419                                                         if ( !includedNode.$.nextSibling )\r
420                                                                 applyStyle = true;\r
421 \r
422                                                         if ( !hasContents )\r
423                                                                 hasContents = ( nodeType != CKEDITOR.NODE_TEXT || (/[^\s\ufeff]/).test( currentNode.getText() ) );\r
424                                                 }\r
425                                         }\r
426                                         else\r
427                                                 applyStyle = true;\r
428                                 }\r
429                                 else\r
430                                         applyStyle = true;\r
431 \r
432                                 // Get the next node to be processed.\r
433                                 currentNode = currentNode.getNextSourceNode();\r
434                         }\r
435 \r
436                         // Apply the style if we have something to which apply it.\r
437                         if ( applyStyle && hasContents && styleRange && !styleRange.collapsed )\r
438                         {\r
439                                 // Build the style element, based on the style object definition.\r
440                                 var styleNode = getElement( this, document );\r
441 \r
442                                 // Get the element that holds the entire range.\r
443                                 var parent = styleRange.getCommonAncestor();\r
444 \r
445                                 // Loop through the parents, removing the redundant attributes\r
446                                 // from the element to be applied.\r
447                                 while ( styleNode && parent )\r
448                                 {\r
449                                         if ( parent.getName() == elementName )\r
450                                         {\r
451                                                 for ( var attName in def.attributes )\r
452                                                 {\r
453                                                         if ( styleNode.getAttribute( attName ) == parent.getAttribute( attName ) )\r
454                                                                 styleNode.removeAttribute( attName );\r
455                                                 }\r
456 \r
457                                                 for ( var styleName in def.styles )\r
458                                                 {\r
459                                                         if ( styleNode.getStyle( styleName ) == parent.getStyle( styleName ) )\r
460                                                                 styleNode.removeStyle( styleName );\r
461                                                 }\r
462 \r
463                                                 if ( !styleNode.hasAttributes() )\r
464                                                 {\r
465                                                         styleNode = null;\r
466                                                         break;\r
467                                                 }\r
468                                         }\r
469 \r
470                                         parent = parent.getParent();\r
471                                 }\r
472 \r
473                                 if ( styleNode )\r
474                                 {\r
475                                         // Move the contents of the range to the style element.\r
476                                         styleRange.extractContents().appendTo( styleNode );\r
477 \r
478                                         // Here we do some cleanup, removing all duplicated\r
479                                         // elements from the style element.\r
480                                         removeFromInsideElement( this, styleNode );\r
481 \r
482                                         // Insert it into the range position (it is collapsed after\r
483                                         // extractContents.\r
484                                         styleRange.insertNode( styleNode );\r
485 \r
486                                         // Let's merge our new style with its neighbors, if possible.\r
487                                         mergeSiblings( styleNode );\r
488 \r
489                                         // As the style system breaks text nodes constantly, let's normalize\r
490                                         // things for performance.\r
491                                         // With IE, some paragraphs get broken when calling normalize()\r
492                                         // repeatedly. Also, for IE, we must normalize body, not documentElement.\r
493                                         // IE is also known for having a "crash effect" with normalize().\r
494                                         // We should try to normalize with IE too in some way, somewhere.\r
495                                         if ( !CKEDITOR.env.ie )\r
496                                                 styleNode.$.normalize();\r
497                                 }\r
498 \r
499                                 // Style applied, let's release the range, so it gets\r
500                                 // re-initialization in the next loop.\r
501                                 styleRange = null;\r
502                         }\r
503                 }\r
504 \r
505                 // Remove the temporary marking node.(#4111)\r
506                 marker && marker.remove();\r
507                 range.moveToBookmark( bookmark );\r
508         }\r
509 \r
510         function removeInlineStyle( range )\r
511         {\r
512                 /*\r
513                  * Make sure our range has included all "collpased" parent inline nodes so\r
514                  * that our operation logic can be simpler.\r
515                  */\r
516                 range.enlarge( CKEDITOR.ENLARGE_ELEMENT );\r
517 \r
518                 var bookmark = range.createBookmark(),\r
519                         startNode = bookmark.startNode;\r
520 \r
521                 if ( range.collapsed )\r
522                 {\r
523 \r
524                         var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ),\r
525                                 // The topmost element in elementspatch which we should jump out of.\r
526                                 boundaryElement;\r
527 \r
528 \r
529                         for ( var i = 0, element ; i < startPath.elements.length\r
530                                         && ( element = startPath.elements[i] ) ; i++ )\r
531                         {\r
532                                 /*\r
533                                  * 1. If it's collaped inside text nodes, try to remove the style from the whole element.\r
534                                  *\r
535                                  * 2. Otherwise if it's collapsed on element boundaries, moving the selection\r
536                                  *  outside the styles instead of removing the whole tag,\r
537                                  *  also make sure other inner styles were well preserverd.(#3309)\r
538                                  */\r
539                                 if ( element == startPath.block || element == startPath.blockLimit )\r
540                                         break;\r
541 \r
542                                 if ( this.checkElementRemovable( element ) )\r
543                                 {\r
544                                         var endOfElement = range.checkBoundaryOfElement( element, CKEDITOR.END ),\r
545                                                         startOfElement = !endOfElement && range.checkBoundaryOfElement( element, CKEDITOR.START );\r
546                                         if ( startOfElement || endOfElement )\r
547                                         {\r
548                                                 boundaryElement = element;\r
549                                                 boundaryElement.match = startOfElement ? 'start' : 'end';\r
550                                         }\r
551                                         else\r
552                                         {\r
553                                                 /*\r
554                                                  * Before removing the style node, there may be a sibling to the style node\r
555                                                  * that's exactly the same to the one to be removed. To the user, it makes\r
556                                                  * no difference that they're separate entities in the DOM tree. So, merge\r
557                                                  * them before removal.\r
558                                                  */\r
559                                                 mergeSiblings( element );\r
560                                                 removeFromElement( this, element );\r
561 \r
562                                         }\r
563                                 }\r
564                         }\r
565 \r
566                         // Re-create the style tree after/before the boundary element,\r
567                         // the replication start from bookmark start node to define the\r
568                         // new range.\r
569                         if ( boundaryElement )\r
570                         {\r
571                                 var clonedElement = startNode;\r
572                                 for ( i = 0 ;; i++ )\r
573                                 {\r
574                                         var newElement = startPath.elements[ i ];\r
575                                         if ( newElement.equals( boundaryElement ) )\r
576                                                 break;\r
577                                         // Avoid copying any matched element.\r
578                                         else if( newElement.match )\r
579                                                 continue;\r
580                                         else\r
581                                                 newElement = newElement.clone();\r
582                                         newElement.append( clonedElement );\r
583                                         clonedElement = newElement;\r
584                                 }\r
585                                 clonedElement[ boundaryElement.match == 'start' ?\r
586                                                         'insertBefore' : 'insertAfter' ]( boundaryElement );\r
587                         }\r
588                 }\r
589                 else\r
590                 {\r
591                         /*\r
592                          * Now our range isn't collapsed. Lets walk from the start node to the end\r
593                          * node via DFS and remove the styles one-by-one.\r
594                          */\r
595                         var endNode = bookmark.endNode,\r
596                                 me = this;\r
597 \r
598                         /*\r
599                          * Find out the style ancestor that needs to be broken down at startNode\r
600                          * and endNode.\r
601                          */\r
602                         function breakNodes()\r
603                         {\r
604                                 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ),\r
605                                         endPath = new CKEDITOR.dom.elementPath( endNode.getParent() ),\r
606                                         breakStart = null,\r
607                                         breakEnd = null;\r
608                                 for ( var i = 0 ; i < startPath.elements.length ; i++ )\r
609                                 {\r
610                                         var element = startPath.elements[ i ];\r
611 \r
612                                         if ( element == startPath.block || element == startPath.blockLimit )\r
613                                                 break;\r
614 \r
615                                         if ( me.checkElementRemovable( element ) )\r
616                                                 breakStart = element;\r
617                                 }\r
618                                 for ( i = 0 ; i < endPath.elements.length ; i++ )\r
619                                 {\r
620                                         element = endPath.elements[ i ];\r
621 \r
622                                         if ( element == endPath.block || element == endPath.blockLimit )\r
623                                                 break;\r
624 \r
625                                         if ( me.checkElementRemovable( element ) )\r
626                                                 breakEnd = element;\r
627                                 }\r
628 \r
629                                 if ( breakEnd )\r
630                                         endNode.breakParent( breakEnd );\r
631                                 if ( breakStart )\r
632                                         startNode.breakParent( breakStart );\r
633                         }\r
634                         breakNodes();\r
635 \r
636                         // Now, do the DFS walk.\r
637                         var currentNode = startNode.getNext();\r
638                         while ( !currentNode.equals( endNode ) )\r
639                         {\r
640                                 /*\r
641                                  * Need to get the next node first because removeFromElement() can remove\r
642                                  * the current node from DOM tree.\r
643                                  */\r
644                                 var nextNode = currentNode.getNextSourceNode();\r
645                                 if ( currentNode.type == CKEDITOR.NODE_ELEMENT && this.checkElementRemovable( currentNode ) )\r
646                                 {\r
647                                         // Remove style from element or overriding element.\r
648                                         if( currentNode.getName() == this.element )\r
649                                                 removeFromElement( this, currentNode );\r
650                                         else\r
651                                                 removeOverrides( currentNode, getOverrides( this )[ currentNode.getName() ] );\r
652 \r
653                                         /*\r
654                                          * removeFromElement() may have merged the next node with something before\r
655                                          * the startNode via mergeSiblings(). In that case, the nextNode would\r
656                                          * contain startNode and we'll have to call breakNodes() again and also\r
657                                          * reassign the nextNode to something after startNode.\r
658                                          */\r
659                                         if ( nextNode.type == CKEDITOR.NODE_ELEMENT && nextNode.contains( startNode ) )\r
660                                         {\r
661                                                 breakNodes();\r
662                                                 nextNode = startNode.getNext();\r
663                                         }\r
664                                 }\r
665                                 currentNode = nextNode;\r
666                         }\r
667                 }\r
668 \r
669                 range.moveToBookmark( bookmark );\r
670 }\r
671 \r
672         function applyBlockStyle( range )\r
673         {\r
674                 // Serializible bookmarks is needed here since\r
675                 // elements may be merged.\r
676                 var bookmark = range.createBookmark( true );\r
677 \r
678                 var iterator = range.createIterator();\r
679                 iterator.enforceRealBlocks = true;\r
680 \r
681                 var block;\r
682                 var doc = range.document;\r
683                 var previousPreBlock;\r
684 \r
685                 while( ( block = iterator.getNextParagraph() ) )                // Only one =\r
686                 {\r
687                         var newBlock = getElement( this, doc );\r
688                         replaceBlock( block, newBlock );\r
689                 }\r
690 \r
691                 range.moveToBookmark( bookmark );\r
692         }\r
693 \r
694         // Replace the original block with new one, with special treatment\r
695         // for <pre> blocks to make sure content format is well preserved, and merging/splitting adjacent\r
696         // when necessary.(#3188)\r
697         function replaceBlock( block, newBlock )\r
698         {\r
699                 var newBlockIsPre       = newBlock.is( 'pre' );\r
700                 var blockIsPre          = block.is( 'pre' );\r
701 \r
702                 var isToPre     = newBlockIsPre && !blockIsPre;\r
703                 var isFromPre   = !newBlockIsPre && blockIsPre;\r
704 \r
705                 if ( isToPre )\r
706                         newBlock = toPre( block, newBlock );\r
707                 else if ( isFromPre )\r
708                         // Split big <pre> into pieces before start to convert.\r
709                         newBlock = fromPres( splitIntoPres( block ), newBlock );\r
710                 else\r
711                         block.moveChildren( newBlock );\r
712 \r
713                 newBlock.replace( block );\r
714 \r
715                 if ( newBlockIsPre )\r
716                 {\r
717                         // Merge previous <pre> blocks.\r
718                         mergePre( newBlock );\r
719                 }\r
720         }\r
721 \r
722         /**\r
723          * Merge a <pre> block with a previous sibling if available.\r
724          */\r
725         function mergePre( preBlock )\r
726         {\r
727                 var previousBlock;\r
728                 if ( !( ( previousBlock = preBlock.getPreviousSourceNode( true, CKEDITOR.NODE_ELEMENT ) )\r
729                                  && previousBlock.is\r
730                                  && previousBlock.is( 'pre') ) )\r
731                         return;\r
732 \r
733                 // Merge the previous <pre> block contents into the current <pre>\r
734                 // block.\r
735                 //\r
736                 // Another thing to be careful here is that currentBlock might contain\r
737                 // a '\n' at the beginning, and previousBlock might contain a '\n'\r
738                 // towards the end. These new lines are not normally displayed but they\r
739                 // become visible after merging.\r
740                 var mergedHtml = replace( previousBlock.getHtml(), /\n$/, '' ) + '\n\n' +\r
741                                 replace( preBlock.getHtml(), /^\n/, '' ) ;\r
742 \r
743                 // Krugle: IE normalizes innerHTML from <pre>, breaking whitespaces.\r
744                 if ( CKEDITOR.env.ie )\r
745                         preBlock.$.outerHTML = '<pre>' + mergedHtml + '</pre>';\r
746                 else\r
747                         preBlock.setHtml( mergedHtml );\r
748 \r
749                 previousBlock.remove();\r
750         }\r
751 \r
752         /**\r
753          * Split into multiple <pre> blocks separated by double line-break.\r
754          * @param preBlock\r
755          */\r
756         function splitIntoPres( preBlock )\r
757         {\r
758                 // Exclude the ones at header OR at tail,\r
759                 // and ignore bookmark content between them.\r
760                 var duoBrRegex = /(\S\s*)\n(?:\s|(<span[^>]+_fck_bookmark.*?\/span>))*\n(?!$)/gi,\r
761                         blockName = preBlock.getName(),\r
762                         splitedHtml = replace( preBlock.getOuterHtml(),\r
763                                 duoBrRegex,\r
764                                 function( match, charBefore, bookmark )\r
765                                 {\r
766                                   return charBefore + '</pre>' + bookmark + '<pre>';\r
767                                 } );\r
768 \r
769                 var pres = [];\r
770                 splitedHtml.replace( /<pre>([\s\S]*?)<\/pre>/gi, function( match, preContent ){\r
771                         pres.push( preContent );\r
772                 } );\r
773                 return pres;\r
774         }\r
775 \r
776         // Wrapper function of String::replace without considering of head/tail bookmarks nodes.\r
777         function replace( str, regexp, replacement )\r
778         {\r
779                 var headBookmark = '',\r
780                         tailBookmark = '';\r
781 \r
782                 str = str.replace( /(^<span[^>]+_fck_bookmark.*?\/span>)|(<span[^>]+_fck_bookmark.*?\/span>$)/gi,\r
783                         function( str, m1, m2 ){\r
784                                         m1 && ( headBookmark = m1 );\r
785                                         m2 && ( tailBookmark = m2 );\r
786                                 return '';\r
787                         } );\r
788                 return headBookmark + str.replace( regexp, replacement ) + tailBookmark;\r
789         }\r
790         /**\r
791          * Converting a list of <pre> into blocks with format well preserved.\r
792          */\r
793         function fromPres( preHtmls, newBlock )\r
794         {\r
795                 var docFrag = new CKEDITOR.dom.documentFragment( newBlock.getDocument() );\r
796                 for ( var i = 0 ; i < preHtmls.length ; i++ )\r
797                 {\r
798                         var blockHtml = preHtmls[ i ];\r
799 \r
800                         // 1. Trim the first and last line-breaks immediately after and before <pre>,\r
801                         // they're not visible.\r
802                          blockHtml =  blockHtml.replace( /(\r\n|\r)/g, '\n' ) ;\r
803                          blockHtml = replace(  blockHtml, /^[ \t]*\n/, '' ) ;\r
804                          blockHtml = replace(  blockHtml, /\n$/, '' ) ;\r
805                         // 2. Convert spaces or tabs at the beginning or at the end to &nbsp;\r
806                          blockHtml = replace(  blockHtml, /^[ \t]+|[ \t]+$/g, function( match, offset, s )\r
807                                         {\r
808                                                 if ( match.length == 1 )        // one space, preserve it\r
809                                                         return '&nbsp;' ;\r
810                                                 else if ( !offset )             // beginning of block\r
811                                                         return CKEDITOR.tools.repeat( '&nbsp;', match.length - 1 ) + ' ';\r
812                                                 else                            // end of block\r
813                                                         return ' ' + CKEDITOR.tools.repeat( '&nbsp;', match.length - 1 );\r
814                                         } ) ;\r
815 \r
816                         // 3. Convert \n to <BR>.\r
817                         // 4. Convert contiguous (i.e. non-singular) spaces or tabs to &nbsp;\r
818                          blockHtml =  blockHtml.replace( /\n/g, '<br>' ) ;\r
819                          blockHtml =  blockHtml.replace( /[ \t]{2,}/g,\r
820                                         function ( match )\r
821                                         {\r
822                                                 return CKEDITOR.tools.repeat( '&nbsp;', match.length - 1 ) + ' ' ;\r
823                                         } ) ;\r
824 \r
825                         var newBlockClone = newBlock.clone();\r
826                         newBlockClone.setHtml(  blockHtml );\r
827                         docFrag.append( newBlockClone );\r
828                 }\r
829                 return docFrag;\r
830         }\r
831 \r
832         /**\r
833          * Converting from a non-PRE block to a PRE block in formatting operations.\r
834          */\r
835         function toPre( block, newBlock )\r
836         {\r
837                 // First trim the block content.\r
838                 var preHtml = block.getHtml();\r
839 \r
840                 // 1. Trim head/tail spaces, they're not visible.\r
841                 preHtml = replace( preHtml, /(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, '' );\r
842                 // 2. Delete ANSI whitespaces immediately before and after <BR> because\r
843                 //    they are not visible.\r
844                 preHtml = preHtml.replace( /[ \t\r\n]*(<br[^>]*>)[ \t\r\n]*/gi, '$1' );\r
845                 // 3. Compress other ANSI whitespaces since they're only visible as one\r
846                 //    single space previously.\r
847                 // 4. Convert &nbsp; to spaces since &nbsp; is no longer needed in <PRE>.\r
848                 preHtml = preHtml.replace( /([ \t\n\r]+|&nbsp;)/g, ' ' );\r
849                 // 5. Convert any <BR /> to \n. This must not be done earlier because\r
850                 //    the \n would then get compressed.\r
851                 preHtml = preHtml.replace( /<br\b[^>]*>/gi, '\n' );\r
852 \r
853                 // Krugle: IE normalizes innerHTML to <pre>, breaking whitespaces.\r
854                 if ( CKEDITOR.env.ie )\r
855                 {\r
856                         var temp = block.getDocument().createElement( 'div' );\r
857                         temp.append( newBlock );\r
858                         newBlock.$.outerHTML =  '<pre>' + preHtml + '</pre>';\r
859                         newBlock = temp.getFirst().remove();\r
860                 }\r
861                 else\r
862                         newBlock.setHtml( preHtml );\r
863 \r
864                 return newBlock;\r
865         }\r
866 \r
867         // Removes a style from an element itself, don't care about its subtree.\r
868         function removeFromElement( style, element )\r
869         {\r
870                 var def = style._.definition,\r
871                         attributes = def.attributes,\r
872                         styles = def.styles,\r
873                         overrides = getOverrides( style );\r
874 \r
875                 function removeAttrs()\r
876                 {\r
877                         for ( var attName in attributes )\r
878                         {\r
879                                 // The 'class' element value must match (#1318).\r
880                                 if ( attName == 'class' && element.getAttribute( attName ) != attributes[ attName ] )\r
881                                         continue;\r
882                                 element.removeAttribute( attName );\r
883                         }\r
884                 }\r
885 \r
886                 // Remove definition attributes/style from the elemnt.\r
887                 removeAttrs();\r
888                 for ( var styleName in styles )\r
889                         element.removeStyle( styleName );\r
890 \r
891                 // Now remove override styles on the element.\r
892                 attributes = overrides[ element.getName() ];\r
893                 if( attributes )\r
894                         removeAttrs();\r
895                 removeNoAttribsElement( element );\r
896         }\r
897 \r
898         // Removes a style from inside an element.\r
899         function removeFromInsideElement( style, element )\r
900         {\r
901                 var def = style._.definition,\r
902                         attribs = def.attributes,\r
903                         styles = def.styles,\r
904                         overrides = getOverrides( style );\r
905 \r
906                 var innerElements = element.getElementsByTag( style.element );\r
907 \r
908                 for ( var i = innerElements.count(); --i >= 0 ; )\r
909                         removeFromElement( style,  innerElements.getItem( i ) );\r
910 \r
911                 // Now remove any other element with different name that is\r
912                 // defined to be overriden.\r
913                 for ( var overrideElement in overrides )\r
914                 {\r
915                         if ( overrideElement != style.element )\r
916                         {\r
917                                 innerElements = element.getElementsByTag( overrideElement ) ;\r
918                                 for ( i = innerElements.count() - 1 ; i >= 0 ; i-- )\r
919                                 {\r
920                                         var innerElement = innerElements.getItem( i );\r
921                                         removeOverrides( innerElement, overrides[ overrideElement ] ) ;\r
922                                 }\r
923                         }\r
924                 }\r
925 \r
926         }\r
927 \r
928         /**\r
929          *  Remove overriding styles/attributes from the specific element.\r
930          *  Note: Remove the element if no attributes remain.\r
931          * @param {Object} element\r
932          * @param {Object} overrides\r
933          */\r
934         function removeOverrides( element, overrides )\r
935         {\r
936                 var attributes = overrides && overrides.attributes ;\r
937 \r
938                 if ( attributes )\r
939                 {\r
940                         for ( var i = 0 ; i < attributes.length ; i++ )\r
941                         {\r
942                                 var attName = attributes[i][0], actualAttrValue ;\r
943 \r
944                                 if ( ( actualAttrValue = element.getAttribute( attName ) ) )\r
945                                 {\r
946                                         var attValue = attributes[i][1] ;\r
947 \r
948                                         // Remove the attribute if:\r
949                                         //    - The override definition value is null ;\r
950                                         //    - The override definition valie is a string that\r
951                                         //      matches the attribute value exactly.\r
952                                         //    - The override definition value is a regex that\r
953                                         //      has matches in the attribute value.\r
954                                         if ( attValue === null ||\r
955                                                         ( attValue.test && attValue.test( actualAttrValue ) ) ||\r
956                                                         ( typeof attValue == 'string' && actualAttrValue == attValue ) )\r
957                                                 element.removeAttribute( attName ) ;\r
958                                 }\r
959                         }\r
960                 }\r
961 \r
962                 removeNoAttribsElement( element );\r
963         }\r
964 \r
965         // If the element has no more attributes, remove it.\r
966         function removeNoAttribsElement( element )\r
967         {\r
968                 // If no more attributes remained in the element, remove it,\r
969                 // leaving its children.\r
970                 if ( !element.hasAttributes() )\r
971                 {\r
972                         // Removing elements may open points where merging is possible,\r
973                         // so let's cache the first and last nodes for later checking.\r
974                         var firstChild  = element.getFirst();\r
975                         var lastChild   = element.getLast();\r
976 \r
977                         element.remove( true );\r
978 \r
979                         if ( firstChild )\r
980                         {\r
981                                 // Check the cached nodes for merging.\r
982                                 mergeSiblings( firstChild );\r
983 \r
984                                 if ( lastChild && !firstChild.equals( lastChild ) )\r
985                                         mergeSiblings( lastChild );\r
986                         }\r
987                 }\r
988         }\r
989 \r
990         function mergeSiblings( element )\r
991         {\r
992                 if ( !element || element.type != CKEDITOR.NODE_ELEMENT || !CKEDITOR.dtd.$removeEmpty[ element.getName() ] )\r
993                         return;\r
994 \r
995                 mergeElements( element, element.getNext(), true );\r
996                 mergeElements( element, element.getPrevious() );\r
997         }\r
998 \r
999         function mergeElements( element, sibling, isNext )\r
1000         {\r
1001                 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT )\r
1002                 {\r
1003                         var hasBookmark = sibling.getAttribute( '_fck_bookmark' );\r
1004 \r
1005                         if ( hasBookmark )\r
1006                                 sibling = isNext ? sibling.getNext() : sibling.getPrevious();\r
1007 \r
1008                         if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && element.isIdentical( sibling ) )\r
1009                         {\r
1010                                 // Save the last child to be checked too, to merge things like\r
1011                                 // <b><i></i></b><b><i></i></b> => <b><i></i></b>\r
1012                                 var innerSibling = isNext ? element.getLast() : element.getFirst();\r
1013 \r
1014                                 if ( hasBookmark )\r
1015                                         ( isNext ? sibling.getPrevious() : sibling.getNext() ).move( element, !isNext );\r
1016 \r
1017                                 sibling.moveChildren( element, !isNext );\r
1018                                 sibling.remove();\r
1019 \r
1020                                 // Now check the last inner child (see two comments above).\r
1021                                 if ( innerSibling )\r
1022                                         mergeSiblings( innerSibling );\r
1023                         }\r
1024                 }\r
1025         }\r
1026 \r
1027         function getElement( style, targetDocument )\r
1028         {\r
1029                 var el;\r
1030 \r
1031                 var def = style._.definition;\r
1032 \r
1033                 var elementName = style.element;\r
1034 \r
1035                 // The "*" element name will always be a span for this function.\r
1036                 if ( elementName == '*' )\r
1037                         elementName = 'span';\r
1038 \r
1039                 // Create the element.\r
1040                 el = new CKEDITOR.dom.element( elementName, targetDocument );\r
1041 \r
1042                 return setupElement( el, style );\r
1043         }\r
1044 \r
1045         function setupElement( el, style )\r
1046         {\r
1047                 var def = style._.definition;\r
1048                 var attributes = def.attributes;\r
1049                 var styles = CKEDITOR.style.getStyleText( def );\r
1050 \r
1051                 // Assign all defined attributes.\r
1052                 if ( attributes )\r
1053                 {\r
1054                         for ( var att in attributes )\r
1055                         {\r
1056                                 el.setAttribute( att, attributes[ att ] );\r
1057                         }\r
1058                 }\r
1059 \r
1060                 // Assign all defined styles.\r
1061                 if ( styles )\r
1062                         el.setAttribute( 'style', styles );\r
1063 \r
1064                 return el;\r
1065         }\r
1066 \r
1067         var varRegex = /#\((.+?)\)/g;\r
1068         function replaceVariables( list, variablesValues )\r
1069         {\r
1070                 for ( var item in list )\r
1071                 {\r
1072                         list[ item ] = list[ item ].replace( varRegex, function( match, varName )\r
1073                                 {\r
1074                                         return variablesValues[ varName ];\r
1075                                 });\r
1076                 }\r
1077         }\r
1078 \r
1079 \r
1080         // Returns an object that can be used for style matching comparison.\r
1081         // Attributes names and values are all lowercased, and the styles get\r
1082         // merged with the style attribute.\r
1083         function getAttributesForComparison( styleDefinition )\r
1084         {\r
1085                 // If we have already computed it, just return it.\r
1086                 var attribs = styleDefinition._AC;\r
1087                 if ( attribs )\r
1088                         return attribs;\r
1089 \r
1090                 attribs = {};\r
1091 \r
1092                 var length = 0;\r
1093 \r
1094                 // Loop through all defined attributes.\r
1095                 var styleAttribs = styleDefinition.attributes;\r
1096                 if ( styleAttribs )\r
1097                 {\r
1098                         for ( var styleAtt in styleAttribs )\r
1099                         {\r
1100                                 length++;\r
1101                                 attribs[ styleAtt ] = styleAttribs[ styleAtt ];\r
1102                         }\r
1103                 }\r
1104 \r
1105                 // Includes the style definitions.\r
1106                 var styleText = CKEDITOR.style.getStyleText( styleDefinition );\r
1107                 if ( styleText )\r
1108                 {\r
1109                         if ( !attribs[ 'style' ] )\r
1110                                 length++;\r
1111                         attribs[ 'style' ] = styleText;\r
1112                 }\r
1113 \r
1114                 // Appends the "length" information to the object.\r
1115                 attribs._length = length;\r
1116 \r
1117                 // Return it, saving it to the next request.\r
1118                 return ( styleDefinition._AC = attribs );\r
1119         }\r
1120 \r
1121         /**\r
1122          * Get the the collection used to compare the elements and attributes,\r
1123          * defined in this style overrides, with other element. All information in\r
1124          * it is lowercased.\r
1125          * @param {CKEDITOR.style} style\r
1126          */\r
1127         function getOverrides( style )\r
1128         {\r
1129                 if( style._.overrides )\r
1130                         return style._.overrides;\r
1131 \r
1132                 var overrides = ( style._.overrides = {} ),\r
1133                         definition = style._.definition.overrides;\r
1134 \r
1135                 if ( definition )\r
1136                 {\r
1137                         // The override description can be a string, object or array.\r
1138                         // Internally, well handle arrays only, so transform it if needed.\r
1139                         if ( !CKEDITOR.tools.isArray( definition ) )\r
1140                                 definition = [ definition ];\r
1141 \r
1142                         // Loop through all override definitions.\r
1143                         for ( var i = 0 ; i < definition.length ; i++ )\r
1144                         {\r
1145                                 var override = definition[i];\r
1146                                 var elementName;\r
1147                                 var overrideEl;\r
1148                                 var attrs;\r
1149 \r
1150                                 // If can be a string with the element name.\r
1151                                 if ( typeof override == 'string' )\r
1152                                         elementName = override.toLowerCase();\r
1153                                 // Or an object.\r
1154                                 else\r
1155                                 {\r
1156                                         elementName = override.element ? override.element.toLowerCase() : style.element;\r
1157                                         attrs = override.attributes;\r
1158                                 }\r
1159 \r
1160                                 // We can have more than one override definition for the same\r
1161                                 // element name, so we attempt to simply append information to\r
1162                                 // it if it already exists.\r
1163                                 overrideEl = overrides[ elementName ] || ( overrides[ elementName ] = {} );\r
1164 \r
1165                                 if ( attrs )\r
1166                                 {\r
1167                                         // The returning attributes list is an array, because we\r
1168                                         // could have different override definitions for the same\r
1169                                         // attribute name.\r
1170                                         var overrideAttrs = ( overrideEl.attributes = overrideEl.attributes || new Array() );\r
1171                                         for ( var attName in attrs )\r
1172                                         {\r
1173                                                 // Each item in the attributes array is also an array,\r
1174                                                 // where [0] is the attribute name and [1] is the\r
1175                                                 // override value.\r
1176                                                 overrideAttrs.push( [ attName.toLowerCase(), attrs[ attName ] ] );\r
1177                                         }\r
1178                                 }\r
1179                         }\r
1180                 }\r
1181 \r
1182                 return overrides;\r
1183         }\r
1184 \r
1185         function normalizeCssText( unparsedCssText, nativeNormalize )\r
1186         {\r
1187                 var styleText;\r
1188                 if ( nativeNormalize !== false )\r
1189                 {\r
1190                         // Injects the style in a temporary span object, so the browser parses it,\r
1191                         // retrieving its final format.\r
1192                         var temp = new CKEDITOR.dom.element( 'span' );\r
1193                         temp.setAttribute( 'style', unparsedCssText );\r
1194                         styleText = temp.getAttribute( 'style' );\r
1195                 }\r
1196                 else\r
1197                         styleText = unparsedCssText;\r
1198 \r
1199                 // Shrinking white-spaces around colon and semi-colon (#4147).\r
1200                 // Compensate tail semi-colon.\r
1201                 return styleText.replace( /\s*([;:])\s*/, '$1' )\r
1202                                                          .replace( /([^\s;])$/, '$1;')\r
1203                                                          .replace( /,\s+/g, ',' ) // Trimming spaces after comma (e.g. font-family name)(#4107).\r
1204                                                          .toLowerCase();\r
1205         }\r
1206 \r
1207         function applyStyle( document, remove )\r
1208         {\r
1209                 // Get all ranges from the selection.\r
1210                 var selection = document.getSelection();\r
1211                 var ranges = selection.getRanges();\r
1212                 var func = remove ? this.removeFromRange : this.applyToRange;\r
1213 \r
1214                 // Apply the style to the ranges.\r
1215                 for ( var i = 0 ; i < ranges.length ; i++ )\r
1216                         func.call( this, ranges[ i ] );\r
1217 \r
1218                 // Select the ranges again.\r
1219                 selection.selectRanges( ranges );\r
1220         }\r
1221 })();\r
1222 \r
1223 CKEDITOR.styleCommand = function( style )\r
1224 {\r
1225         this.style = style;\r
1226 };\r
1227 \r
1228 CKEDITOR.styleCommand.prototype.exec = function( editor )\r
1229 {\r
1230         editor.focus();\r
1231 \r
1232         var doc = editor.document;\r
1233 \r
1234         if ( doc )\r
1235         {\r
1236                 if ( this.state == CKEDITOR.TRISTATE_OFF )\r
1237                         this.style.apply( doc );\r
1238                 else if ( this.state == CKEDITOR.TRISTATE_ON )\r
1239                         this.style.remove( doc );\r
1240         }\r
1241 \r
1242         return !!doc;\r
1243 };\r