1/*
2 * FCKeditor - The text editor for Internet - http://www.fckeditor.net
3 * Copyright (C) 2003-2007 Frederico Caldeira Knabben
4 *
5 * == BEGIN LICENSE ==
6 *
7 * Licensed under the terms of any of the following licenses at your
8 * choice:
9 *
10 *  - GNU General Public License Version 2 or later (the "GPL")
11 *    http://www.gnu.org/licenses/gpl.html
12 *
13 *  - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
14 *    http://www.gnu.org/licenses/lgpl.html
15 *
16 *  - Mozilla Public License Version 1.1 or later (the "MPL")
17 *    http://www.mozilla.org/MPL/MPL-1.1.html
18 *
19 * == END LICENSE ==
20 *
21 * FCKStyle Class: contains a style definition, and all methods to work with
22 * the style in a document.
23 */
24
25/**
26 * @param {Object} styleDesc A "style descriptor" object, containing the raw
27 * style definition in the following format:
28 *		'<style name>' : {
29 *			Element : '<element name>',
30 *			Attributes : {
31 *				'<att name>' : '<att value>',
32 *				...
33 *			},
34 *			Styles : {
35 *				'<style name>' : '<style value>',
36 *				...
37 *			},
38 *			Overrides : '<element name>'|{
39 *				Element : '<element name>',
40 *				Attributes : {
41 *					'<att name>' : '<att value>'|/<att regex>/
42 *				},
43 *				Styles : {
44 *					'<style name>' : '<style value>'|/<style regex>/
45 *				},
46 *			}
47 *		}
48 */
49var FCKStyle = function( styleDesc )
50{
51	this.Element = ( styleDesc.Element || 'span' ).toLowerCase() ;
52	this._StyleDesc = styleDesc ;
53}
54
55FCKStyle.prototype =
56{
57	/**
58	 * Get the style type, based on its element name:
59	 *		- FCK_STYLE_BLOCK  (0): Block Style
60	 *		- FCK_STYLE_INLINE (1): Inline Style
61	 *		- FCK_STYLE_OBJECT (2): Object Style
62	 */
63	GetType : function()
64	{
65		var type = this.GetType_$ ;
66
67		if ( type != undefined )
68			return type ;
69
70		var elementName = this.Element ;
71
72		if ( elementName == '#' || FCKListsLib.StyleBlockElements[ elementName ] )
73			type = FCK_STYLE_BLOCK ;
74		else if ( FCKListsLib.StyleObjectElements[ elementName ] )
75			type = FCK_STYLE_OBJECT ;
76		else
77			type = FCK_STYLE_INLINE ;
78
79		return ( this.GetType_$ = type ) ;
80	},
81
82	/**
83	 * Apply the style to the current selection.
84	 */
85	ApplyToSelection : function( targetWindow )
86	{
87		// Create a range for the current selection.
88		var range = new FCKDomRange( targetWindow ) ;
89		range.MoveToSelection() ;
90
91		this.ApplyToRange( range, true ) ;
92	},
93
94	/**
95	 * Apply the style to a FCKDomRange.
96	 */
97	ApplyToRange : function( range, selectIt )
98	{
99		// ApplyToRange is not valid for FCK_STYLE_OBJECT types.
100		// Use ApplyToObject instead.
101
102		switch ( this.GetType() )
103		{
104			case FCK_STYLE_BLOCK :
105				this.ApplyToRange = this._ApplyBlockStyle ;
106				break ;
107			case FCK_STYLE_INLINE :
108				this.ApplyToRange = this._ApplyInlineStyle ;
109				break ;
110			default :
111				return ;
112		}
113
114		this.ApplyToRange( range, selectIt ) ;
115	},
116
117	/**
118	 * Apply the style to an object. Valid for FCK_STYLE_BLOCK types only.
119	 */
120	ApplyToObject : function( objectElement )
121	{
122		if ( !objectElement )
123			return ;
124
125		this.BuildElement( null, objectElement ) ;
126	},
127
128	/**
129	 * Remove the style from the current selection.
130	 */
131	RemoveFromSelection : function( targetWindow )
132	{
133		// Create a range for the current selection.
134		var range = new FCKDomRange( targetWindow ) ;
135		range.MoveToSelection() ;
136
137		this.RemoveFromRange( range, true ) ;
138	},
139
140	/**
141	 * Remove the style from a FCKDomRange. Block type styles will have no
142	 * effect.
143	 */
144	RemoveFromRange : function( range, selectIt )
145	{
146		var bookmark ;
147
148		// Create the attribute list to be used later for element comparisons.
149		var styleAttribs = this._GetAttribsForComparison() ;
150		var styleOverrides = this._GetOverridesForComparison() ;
151
152		// If collapsed, we are removing all conflicting styles from the range
153		// parent tree.
154		if ( range.CheckIsCollapsed() )
155		{
156			// Bookmark the range so we can re-select it after processing.
157			var bookmark = range.CreateBookmark( true ) ;
158
159			// Let's start from the bookmark <span> parent.
160			var bookmarkStart = range.GetBookmarkNode( bookmark, true ) ;
161
162			var path = new FCKElementPath( bookmarkStart.parentNode ) ;
163
164			// While looping through the path, we'll be saving references to
165			// parent elements if the range is in one of their boundaries. In
166			// this way, we are able to create a copy of those elements when
167			// removing a style if the range is in a boundary limit (see #1270).
168			var boundaryElements = [] ;
169
170			// Check if the range is in the boundary limits of an element
171			// (related to #1270).
172			var isBoundaryRight = !FCKDomTools.GetNextSibling( bookmarkStart ) ;
173			var isBoundary = isBoundaryRight || !FCKDomTools.GetPreviousSibling( bookmarkStart ) ;
174
175			// This is the last element to be removed in the boundary situation
176			// described at #1270.
177			var lastBoundaryElement ;
178			var boundaryLimitIndex = -1 ;
179
180			for ( var i = 0 ; i < path.Elements.length ; i++ )
181			{
182				var pathElement = path.Elements[i] ;
183				if ( this.CheckElementRemovable( pathElement ) )
184				{
185					if ( isBoundary
186						&& !FCKDomTools.CheckIsEmptyElement( pathElement,
187								function( el )
188								{
189									return ( el != bookmarkStart ) ;
190								} )
191						)
192					{
193						lastBoundaryElement = pathElement ;
194
195						// We'll be continuously including elements in the
196						// boundaryElements array, but only those added before
197						// setting lastBoundaryElement must be used later, so
198						// let's mark the current index here.
199						boundaryLimitIndex = boundaryElements.length - 1 ;
200					}
201					else
202					{
203						var pathElementName = pathElement.nodeName.toLowerCase() ;
204
205						if ( pathElementName == this.Element )
206						{
207							// Remove any attribute that conflict with this style, no
208							// matter their values.
209							for ( var att in styleAttribs )
210							{
211								if ( FCKDomTools.HasAttribute( pathElement, att ) )
212								{
213									switch ( att )
214									{
215										case 'style' :
216											this._RemoveStylesFromElement( pathElement ) ;
217											break ;
218
219										case 'class' :
220											// The 'class' element value must match (#1318).
221											if ( FCKDomTools.GetAttributeValue( pathElement, att ) != this.GetFinalAttributeValue( att ) )
222												continue ;
223
224										default :
225											FCKDomTools.RemoveAttribute( pathElement, att ) ;
226									}
227								}
228							}
229						}
230
231						// Remove overrides defined to the same element name.
232						this._RemoveOverrides( pathElement, styleOverrides[ pathElementName ] ) ;
233
234						// Remove the element if no more attributes are available.
235						this._RemoveNoAttribElement( pathElement ) ;
236					}
237				}
238				else if ( isBoundary )
239					boundaryElements.push( pathElement ) ;
240
241				// Check if we are still in a boundary (at the same side).
242				isBoundary = isBoundary && ( ( isBoundaryRight && !FCKDomTools.GetNextSibling( pathElement ) ) || ( !isBoundaryRight && !FCKDomTools.GetPreviousSibling( pathElement ) ) ) ;
243
244				// If we are in an element that is not anymore a boundary, or
245				// we are at the last element, let's move things outside the
246				// boundary (if available).
247				if ( lastBoundaryElement && ( !isBoundary || ( i == path.Elements.length - 1 ) ) )
248				{
249					// Remove the bookmark node from the DOM.
250					var currentElement = FCKDomTools.RemoveNode( bookmarkStart ) ;
251
252					// Build the collapsed group of elements that are not
253					// removed by this style, but share the boundary.
254					// (see comment 1 and 2 at #1270)
255					for ( var j = 0 ; j <= boundaryLimitIndex ; j++ )
256					{
257						var newElement = FCKDomTools.CloneElement( boundaryElements[j] ) ;
258						newElement.appendChild( currentElement ) ;
259						currentElement = newElement ;
260					}
261
262					// Re-insert the bookmark node (and the collapsed elements)
263					// in the DOM, in the new position next to the styled element.
264					if ( isBoundaryRight )
265						FCKDomTools.InsertAfterNode( lastBoundaryElement, currentElement ) ;
266					else
267						lastBoundaryElement.parentNode.insertBefore( currentElement, lastBoundaryElement ) ;
268
269					isBoundary = false ;
270					lastBoundaryElement = null ;
271				}
272			}
273
274				// Re-select the original range.
275			if ( selectIt )
276				range.SelectBookmark( bookmark ) ;
277
278			return ;
279		}
280
281		// Expand the range, if inside inline element boundaries.
282		range.Expand( 'inline_elements' ) ;
283
284		// Bookmark the range so we can re-select it after processing.
285		var bookmark = range.CreateBookmark( true ) ;
286
287		// The style will be applied within the bookmark boundaries.
288		var startNode	= range.GetBookmarkNode( bookmark, true ) ;
289		var endNode		= range.GetBookmarkNode( bookmark, false ) ;
290
291		range.Release( true ) ;
292
293		// We need to check the selection boundaries (bookmark spans) to break
294		// the code in a way that we can properly remove partially selected nodes.
295		// For example, removing a <b> style from
296		//		<b>This is [some text</b> to show <b>the] problem</b>
297		// ... where [ and ] represent the selection, must result:
298		//		<b>This is </b>[some text to show the]<b> problem</b>
299		// The strategy is simple, we just break the partial nodes before the
300		// removal logic, having something that could be represented this way:
301		//		<b>This is </b>[<b>some text</b> to show <b>the</b>]<b> problem</b>
302
303		// Let's start checking the start boundary.
304		var path = new FCKElementPath( startNode ) ;
305		var pathElements = path.Elements ;
306		var pathElement ;
307
308		for ( var i = 1 ; i < pathElements.length ; i++ )
309		{
310			pathElement = pathElements[i] ;
311
312			if ( pathElement == path.Block || pathElement == path.BlockLimit )
313				break ;
314
315			// If this element can be removed (even partially).
316			if ( this.CheckElementRemovable( pathElement ) )
317				FCKDomTools.BreakParent( startNode, pathElement, range ) ;
318		}
319
320		// Now the end boundary.
321		path = new FCKElementPath( endNode ) ;
322		pathElements = path.Elements ;
323
324		for ( var i = 1 ; i < pathElements.length ; i++ )
325		{
326			pathElement = pathElements[i] ;
327
328			if ( pathElement == path.Block || pathElement == path.BlockLimit )
329				break ;
330
331			elementName = pathElement.nodeName.toLowerCase() ;
332
333			// If this element can be removed (even partially).
334			if ( this.CheckElementRemovable( pathElement ) )
335				FCKDomTools.BreakParent( endNode, pathElement, range ) ;
336		}
337
338		// Navigate through all nodes between the bookmarks.
339		var currentNode = FCKDomTools.GetNextSourceNode( startNode, true ) ;
340
341		while ( currentNode )
342		{
343			// Cache the next node to be processed. Do it now, because
344			// currentNode may be removed.
345			var nextNode = FCKDomTools.GetNextSourceNode( currentNode ) ;
346
347			// Remove elements nodes that match with this style rules.
348			if ( currentNode.nodeType == 1 )
349			{
350				var elementName = currentNode.nodeName.toLowerCase() ;
351
352				var mayRemove = ( elementName == this.Element ) ;
353				if ( mayRemove )
354				{
355					// Remove any attribute that conflict with this style, no matter
356					// their values.
357					for ( var att in styleAttribs )
358					{
359						if ( FCKDomTools.HasAttribute( currentNode, att ) )
360						{
361							switch ( att )
362							{
363								case 'style' :
364									this._RemoveStylesFromElement( currentNode ) ;
365									break ;
366
367								case 'class' :
368									// The 'class' element value must match (#1318).
369									if ( FCKDomTools.GetAttributeValue( currentNode, att ) != this.GetFinalAttributeValue( att ) )
370										continue ;
371
372								default :
373									FCKDomTools.RemoveAttribute( currentNode, att ) ;
374							}
375						}
376					}
377				}
378				else
379					mayRemove = !!styleOverrides[ elementName ] ;
380
381				if ( mayRemove )
382				{
383					// Remove overrides defined to the same element name.
384					this._RemoveOverrides( currentNode, styleOverrides[ elementName ] ) ;
385
386					// Remove the element if no more attributes are available.
387					this._RemoveNoAttribElement( currentNode ) ;
388				}
389			}
390
391			// If we have reached the end of the selection, stop looping.
392			if ( nextNode == endNode )
393				break ;
394
395			currentNode = nextNode ;
396		}
397
398		this._FixBookmarkStart( startNode ) ;
399
400		// Re-select the original range.
401		if ( selectIt )
402			range.SelectBookmark( bookmark ) ;
403	},
404
405	/**
406	 * Checks if an element, or any of its attributes, is removable by the
407	 * current style definition.
408	 */
409	CheckElementRemovable : function( element, fullMatch )
410	{
411		if ( !element )
412			return false ;
413
414		var elementName = element.nodeName.toLowerCase() ;
415
416		// If the element name is the same as the style name.
417		if ( elementName == this.Element )
418		{
419			// If no attributes are defined in the element.
420			if ( !fullMatch && !FCKDomTools.HasAttributes( element ) )
421				return true ;
422
423			// If any attribute conflicts with the style attributes.
424			var attribs = this._GetAttribsForComparison() ;
425			var allMatched = ( attribs._length == 0 ) ;
426			for ( var att in attribs )
427			{
428				if ( att == '_length' )
429					continue ;
430
431				if ( this._CompareAttributeValues( att, FCKDomTools.GetAttributeValue( element, att ), ( this.GetFinalAttributeValue( att ) || '' ) ) )
432				{
433					allMatched = true ;
434					if ( !fullMatch )
435						break ;
436				}
437				else
438				{
439					allMatched = false ;
440					if ( fullMatch )
441						return false ;
442				}
443			}
444			if ( allMatched )
445				return true ;
446		}
447
448		// Check if the element can be somehow overriden.
449		var override = this._GetOverridesForComparison()[ elementName ] ;
450		if ( override )
451		{
452			// If no attributes have been defined, remove the element.
453			if ( !( attribs = override.Attributes ) ) // Only one "="
454				return true ;
455
456			for ( var i = 0 ; i < attribs.length ; i++ )
457			{
458				var attName = attribs[i][0] ;
459				if ( FCKDomTools.HasAttribute( element, attName ) )
460				{
461					var attValue = attribs[i][1] ;
462
463					// Remove the attribute if:
464					//    - The override definition value is null ;
465					//    - The override definition valie is a string that
466					//      matches the attribute value exactly.
467					//    - The override definition value is a regex that
468					//      has matches in the attribute value.
469					if ( attValue == null ||
470							( typeof attValue == 'string' && FCKDomTools.GetAttributeValue( element, attName ) == attValue ) ||
471							attValue.test( FCKDomTools.GetAttributeValue( element, attName ) ) )
472						return true ;
473				}
474			}
475		}
476
477		return false ;
478	},
479
480	/**
481	 * Get the style state for an element path. Returns "true" if the element
482	 * is active in the path.
483	 */
484	CheckActive : function( elementPath )
485	{
486		switch ( this.GetType() )
487		{
488			case FCK_STYLE_BLOCK :
489				return this.CheckElementRemovable( elementPath.Block || elementPath.BlockLimit ) ;
490
491			case FCK_STYLE_INLINE :
492
493				var elements = elementPath.Elements ;
494
495				for ( var i = 0 ; i < elements.length ; i++ )
496				{
497					var element = elements[i] ;
498
499					if ( element == elementPath.Block || element == elementPath.BlockLimit )
500						continue ;
501
502					if ( this.CheckElementRemovable( element, true ) )
503						return true ;
504				}
505		}
506		return false ;
507	},
508
509	/**
510	 * Removes an inline style from inside an element tree. The element node
511	 * itself is not checked or removed, only the child tree inside of it.
512	 */
513	RemoveFromElement : function( element )
514	{
515		var attribs = this._GetAttribsForComparison() ;
516		var overrides = this._GetOverridesForComparison() ;
517
518		// Get all elements with the same name.
519		var innerElements = element.getElementsByTagName( this.Element ) ;
520
521		for ( var i = innerElements.length - 1 ; i >= 0 ; i-- )
522		{
523			var innerElement = innerElements[i] ;
524
525			// Remove any attribute that conflict with this style, no matter
526			// their values.
527			for ( var att in attribs )
528			{
529				if ( FCKDomTools.HasAttribute( innerElement, att ) )
530				{
531					switch ( att )
532					{
533						case 'style' :
534							this._RemoveStylesFromElement( innerElement ) ;
535							break ;
536
537						case 'class' :
538							// The 'class' element value must match (#1318).
539							if ( FCKDomTools.GetAttributeValue( innerElement, att ) != this.GetFinalAttributeValue( att ) )
540								continue ;
541
542						default :
543							FCKDomTools.RemoveAttribute( innerElement, att ) ;
544					}
545				}
546			}
547
548			// Remove overrides defined to the same element name.
549			this._RemoveOverrides( innerElement, overrides[ this.Element ] ) ;
550
551			// Remove the element if no more attributes are available.
552			this._RemoveNoAttribElement( innerElement ) ;
553		}
554
555		// Now remove any other element with different name that is
556		// defined to be overriden.
557		for ( var overrideElement in overrides )
558		{
559			if ( overrideElement != this.Element )
560			{
561				// Get all elements.
562				innerElements = element.getElementsByTagName( overrideElement ) ;
563
564				for ( var i = innerElements.length - 1 ; i >= 0 ; i-- )
565				{
566					var innerElement = innerElements[i] ;
567					this._RemoveOverrides( innerElement, overrides[ overrideElement ] ) ;
568					this._RemoveNoAttribElement( innerElement ) ;
569				}
570			}
571		}
572	},
573
574	_RemoveStylesFromElement : function( element )
575	{
576		var elementStyle = element.style.cssText ;
577		var pattern = this.GetFinalStyleValue() ;
578
579		if ( elementStyle.length > 0 && pattern.length == 0 )
580			return ;
581
582		pattern = '(^|;)\\s*(' +
583			pattern.replace( /\s*([^ ]+):.*?(;|$)/g, '$1|' ).replace( /\|$/, '' ) +
584			'):[^;]+' ;
585
586		var regex = new RegExp( pattern, 'gi' ) ;
587
588		elementStyle = elementStyle.replace( regex, '' ).Trim() ;
589
590		if ( elementStyle.length == 0 || elementStyle == ';' )
591			FCKDomTools.RemoveAttribute( element, 'style' ) ;
592		else
593			element.style.cssText = elementStyle.replace( regex, '' ) ;
594	},
595
596	/**
597	 * Remove all attributes that are defined to be overriden,
598	 */
599	_RemoveOverrides : function( element, override )
600	{
601		var attributes = override && override.Attributes ;
602
603		if ( attributes )
604		{
605			for ( var i = 0 ; i < attributes.length ; i++ )
606			{
607				var attName = attributes[i][0] ;
608
609				if ( FCKDomTools.HasAttribute( element, attName ) )
610				{
611					var attValue	= attributes[i][1] ;
612
613					// Remove the attribute if:
614					//    - The override definition value is null ;
615					//    - The override definition valie is a string that
616					//      matches the attribute value exactly.
617					//    - The override definition value is a regex that
618					//      has matches in the attribute value.
619					if ( attValue == null ||
620							( attValue.test && attValue.test( FCKDomTools.GetAttributeValue( element, attName ) ) ) ||
621							( typeof attValue == 'string' && FCKDomTools.GetAttributeValue( element, attName ) == attValue ) )
622						FCKDomTools.RemoveAttribute( element, attName ) ;
623				}
624			}
625		}
626	},
627
628	/**
629	 * If the element has no more attributes, remove it.
630	 */
631	_RemoveNoAttribElement : function( element )
632	{
633		// If no more attributes remained in the element, remove it,
634		// leaving its children.
635		if ( !FCKDomTools.HasAttributes( element ) )
636		{
637			// Removing elements may open points where merging is possible,
638			// so let's cache the first and last nodes for later checking.
639			var firstChild	= element.firstChild ;
640			var lastChild	= element.lastChild ;
641
642			FCKDomTools.RemoveNode( element, true ) ;
643
644			// Check the cached nodes for merging.
645			this._MergeSiblings( firstChild ) ;
646
647			if ( firstChild != lastChild )
648				this._MergeSiblings( lastChild ) ;
649		}
650	},
651
652	/**
653	 * Creates a DOM element for this style object.
654	 */
655	BuildElement : function( targetDoc, element )
656	{
657		// Create the element.
658		var el = element || targetDoc.createElement( this.Element ) ;
659
660		// Assign all defined attributes.
661		var attribs	= this._StyleDesc.Attributes ;
662		var attValue ;
663		if ( attribs )
664		{
665			for ( var att in attribs )
666			{
667				attValue = this.GetFinalAttributeValue( att ) ;
668
669				if ( att.toLowerCase() == 'class' )
670					el.className = attValue ;
671				else
672					el.setAttribute( att, attValue ) ;
673			}
674		}
675
676		// Assign the style attribute.
677		if ( this._GetStyleText().length > 0 )
678			el.style.cssText = this.GetFinalStyleValue() ;
679
680		return el ;
681	},
682
683	_CompareAttributeValues : function( attName, valueA, valueB )
684	{
685		if ( attName == 'style' && valueA && valueB )
686		{
687			valueA = valueA.replace( /;$/, '' ).toLowerCase() ;
688			valueB = valueB.replace( /;$/, '' ).toLowerCase() ;
689		}
690
691		return ( valueA == valueB )
692	},
693
694	GetFinalAttributeValue : function( attName )
695	{
696		var attValue = this._StyleDesc.Attributes ;
697		var attValue = attValue ? attValue[ attName ] : null ;
698
699		if ( !attValue && attName == 'style' )
700			return this.GetFinalStyleValue() ;
701
702		if ( attValue && this._Variables )
703			// Using custom Replace() to guarantee the correct scope.
704			attValue = attValue.Replace( FCKRegexLib.StyleVariableAttName, this._GetVariableReplace, this ) ;
705
706		return attValue ;
707	},
708
709	GetFinalStyleValue : function()
710	{
711		var attValue = this._GetStyleText() ;
712
713		if ( attValue.length > 0 && this._Variables )
714		{
715			// Using custom Replace() to guarantee the correct scope.
716			attValue = attValue.Replace( FCKRegexLib.StyleVariableAttName, this._GetVariableReplace, this ) ;
717			attValue = FCKTools.NormalizeCssText( attValue ) ;
718		}
719
720		return attValue ;
721	},
722
723	_GetVariableReplace : function()
724	{
725		// The second group in the regex is the variable name.
726		return this._Variables[ arguments[2] ] || arguments[0] ;
727	},
728
729	/**
730	 * Set the value of a variable attribute or style, to be used when
731	 * appliying the style.
732	 */
733	SetVariable : function( name, value )
734	{
735		var variables = this._Variables ;
736
737		if ( !variables )
738			variables = this._Variables = {} ;
739
740		this._Variables[ name ] = value ;
741	},
742
743	/**
744	 * Apply an inline style to a FCKDomRange.
745	 *
746	 * TODO
747	 *	- Implement the "#" style handling.
748	 *	- Properly handle block containers like <div> and <blockquote>.
749	 */
750	_ApplyBlockStyle : function( range, selectIt )
751	{
752		// Bookmark the range so we can re-select it after processing.
753		var bookmark ;
754
755		if ( selectIt )
756			bookmark = range.CreateBookmark( true ) ;
757
758		var iterator = new FCKDomRangeIterator( range ) ;
759		iterator.EnforceRealBlocks = true ;
760
761		var block ;
762		while( ( block = iterator.GetNextParagraph() ) )		// Only one =
763		{
764			// Create the new node right before the current one.
765			var newBlock = block.parentNode.insertBefore( this.BuildElement( range.Window.document ), block ) ;
766
767			// Move everything from the current node to the new one.
768			FCKDomTools.MoveChildren( block, newBlock ) ;
769
770			// Delete the current node.
771			FCKDomTools.RemoveNode( block ) ;
772		}
773
774		// Re-select the original range.
775		if ( selectIt )
776			range.SelectBookmark( bookmark ) ;
777	},
778
779	/**
780	 * Apply an inline style to a FCKDomRange.
781	 *
782	 * TODO
783	 *	- Merge elements, when applying styles to similar elements that enclose
784	 *    the entire selection, outputing:
785	 *        <span style="color: #ff0000; background-color: #ffffff">XYZ</span>
786	 *    instead of:
787	 *        <span style="color: #ff0000;"><span style="background-color: #ffffff">XYZ</span></span>
788	 */
789	_ApplyInlineStyle : function( range, selectIt )
790	{
791		var doc = range.Window.document ;
792
793		if ( range.CheckIsCollapsed() )
794		{
795			// Create the element to be inserted in the DOM.
796			var collapsedElement = this.BuildElement( doc ) ;
797			range.InsertNode( collapsedElement ) ;
798			range.MoveToPosition( collapsedElement, 2 ) ;
799			range.Select() ;
800
801			return ;
802		}
803
804		// The general idea here is navigating through all nodes inside the
805		// current selection, working on distinct range blocks, defined by the
806		// DTD compatibility between the style element and the nodes inside the
807		// ranges.
808		//
809		// For example, suppose we have the following selection (where [ and ]
810		// are the boundaries), and we apply a <b> style there:
811		//
812		//		<p>Here we [have <b>some</b> text.<p>
813		//		<p>And some here] here.</p>
814		//
815		// Two different ranges will be detected:
816		//
817		//		"have <b>some</b> text."
818		//		"And some here"
819		//
820		// Both ranges will be extracted, moved to a <b> element, and
821		// re-inserted, resulting in the following output:
822		//
823		//		<p>Here we [<b>have some text.</b><p>
824		//		<p><b>And some here</b>] here.</p>
825		//
826		// Note that the <b> element at <b>some</b> is also removed because it
827		// is not needed anymore.
828
829		var elementName = this.Element ;
830
831		// Get the DTD definition for the element. Defaults to "span".
832		var elementDTD = FCK.DTD[ elementName ] || FCK.DTD.span ;
833
834		// Create the attribute list to be used later for element comparisons.
835		var styleAttribs = this._GetAttribsForComparison() ;
836		var styleNode ;
837
838		// Expand the range, if inside inline element boundaries.
839		range.Expand( 'inline_elements' ) ;
840
841		// Bookmark the range so we can re-select it after processing.
842		var bookmark = range.CreateBookmark( true ) ;
843
844		// The style will be applied within the bookmark boundaries.
845		var startNode	= range.GetBookmarkNode( bookmark, true ) ;
846		var endNode		= range.GetBookmarkNode( bookmark, false ) ;
847
848		// We'll be reusing the range to apply the styles. So, release it here
849		// to indicate that it has not been initialized.
850		range.Release( true ) ;
851
852		// Let's start the nodes lookup from the node right after the bookmark
853		// span.
854		var currentNode = FCKDomTools.GetNextSourceNode( startNode, true ) ;
855
856		while ( currentNode )
857		{
858			var applyStyle = false ;
859
860			var nodeType = currentNode.nodeType ;
861			var nodeName = nodeType == 1 ? currentNode.nodeName.toLowerCase() : null ;
862
863			// Check if the current node can be a child of the style element.
864			if ( !nodeName || elementDTD[ nodeName ] )
865			{
866				// Check if the style element can be a child of the current
867				// node parent.
868				if ( ( FCK.DTD[ currentNode.parentNode.nodeName.toLowerCase() ] || FCK.DTD.span )[ elementName ] )
869				{
870					// This node will be part of our range, so if it has not
871					// been started, place its start right before the node.
872					if ( !range.CheckHasRange() )
873						range.SetStart( currentNode, 3 ) ;
874
875					// Non element nodes, or empty elements can be added
876					// completely to the range.
877					if ( nodeType != 1 || currentNode.childNodes.length == 0 )
878					{
879						var includedNode = currentNode ;
880						var parentNode = includedNode.parentNode ;
881
882						// This node is about to be included completelly, but,
883						// if this is the last node in its parent, we must also
884						// check if the parent itself can be added completelly
885						// to the range.
886						while ( includedNode == parentNode.lastChild
887							&& elementDTD[ parentNode.nodeName.toLowerCase() ] )
888						{
889							includedNode = parentNode ;
890						}
891
892						range.SetEnd( includedNode, 4 ) ;
893
894						// If the included node is the last node in its parent
895						// and its parent can't be inside the style node, apply
896						// the style immediately.
897						if ( includedNode == includedNode.parentNode.lastChild && !elementDTD[ includedNode.parentNode.nodeName.toLowerCase() ] )
898							applyStyle = true ;
899					}
900					else
901					{
902						// Element nodes will not be added directly. We need to
903						// check their children because the selection could end
904						// inside the node, so let's place the range end right
905						// before the element.
906						range.SetEnd( currentNode, 3 ) ;
907					}
908				}
909				else
910					applyStyle = true ;
911			}
912			else
913				applyStyle = true ;
914
915			// Get the next node to be processed.
916			currentNode = FCKDomTools.GetNextSourceNode( currentNode ) ;
917
918			// If we have reached the end of the selection, just apply the
919			// style ot the range, and stop looping.
920			if ( currentNode == endNode )
921			{
922				currentNode = null ;
923				applyStyle = true ;
924			}
925
926			// Apply the style if we have something to which apply it.
927			if ( applyStyle && range.CheckHasRange() && !range.CheckIsCollapsed() )
928			{
929				// Build the style element, based on the style object definition.
930				styleNode = this.BuildElement( doc ) ;
931
932				// Move the contents of the range to the style element.
933				range.ExtractContents().AppendTo( styleNode ) ;
934
935				// If it is not empty.
936				if ( styleNode.innerHTML.RTrim().length > 0 )
937				{
938					// Insert it in the range position (it is collapsed after
939					// ExtractContents.
940					range.InsertNode( styleNode ) ;
941
942					// Here we do some cleanup, removing all duplicated
943					// elements from the style element.
944					this.RemoveFromElement( styleNode ) ;
945
946					// Let's merge our new style with its neighbors, if possible.
947					this._MergeSiblings( styleNode, this._GetAttribsForComparison() ) ;
948
949					// As the style system breaks text nodes constantly, let's normalize
950					// things for performance.
951					// With IE, some paragraphs get broken when calling normalize()
952					// repeatedly. Also, for IE, we must normalize body, not documentElement.
953					// IE is also known for having a "crash effect" with normalize().
954					// We should try to normalize with IE too in some way, somewhere.
955					if ( !FCKBrowserInfo.IsIE )
956						styleNode.normalize() ;
957				}
958
959				// Style applied, let's release the range, so it gets marked to
960				// re-initialization in the next loop.
961				range.Release( true ) ;
962			}
963		}
964
965		this._FixBookmarkStart( startNode ) ;
966
967		// Re-select the original range.
968		if ( selectIt )
969			range.SelectBookmark( bookmark ) ;
970	},
971
972	_FixBookmarkStart : function( startNode )
973	{
974		// After appliying or removing an inline style, the start boundary of
975		// the selection must be placed inside all inline elements it is
976		// bordering.
977		var startSibling ;
978		while ( ( startSibling = startNode.nextSibling ) )	// Only one "=".
979		{
980			if ( startSibling.nodeType == 1
981				&& FCKListsLib.InlineNonEmptyElements[ startSibling.nodeName.toLowerCase() ] )
982			{
983				// If it is an empty inline element, we can safely remove it.
984				if ( !startSibling.firstChild )
985					FCKDomTools.RemoveNode( startSibling ) ;
986				else
987					FCKDomTools.MoveNode( startNode, startSibling, true ) ;
988				continue ;
989			}
990
991			// Empty text nodes can be safely removed to not disturb.
992			if ( startSibling.nodeType == 3 && startSibling.length == 0 )
993			{
994				FCKDomTools.RemoveNode( startSibling ) ;
995				continue ;
996			}
997
998			break ;
999		}
1000	},
1001
1002	/**
1003	 * Merge an element with its similar siblings.
1004	 * "attribs" is and object computed with _CreateAttribsForComparison.
1005	 */
1006	_MergeSiblings : function( element, attribs )
1007	{
1008		if ( !element || element.nodeType != 1 || !FCKListsLib.InlineNonEmptyElements[ element.nodeName.toLowerCase() ] )
1009			return ;
1010
1011		this._MergeNextSibling( element, attribs ) ;
1012		this._MergePreviousSibling( element, attribs ) ;
1013	},
1014
1015	/**
1016	 * Merge an element with its similar siblings after it.
1017	 * "attribs" is and object computed with _CreateAttribsForComparison.
1018	 */
1019	_MergeNextSibling : function( element, attribs )
1020	{
1021		// Check the next sibling.
1022		var sibling = element.nextSibling ;
1023
1024		// Check if the next sibling is a bookmark element. In this case, jump it.
1025		var hasBookmark = ( sibling && sibling.nodeType == 1 && sibling.getAttribute( '_fck_bookmark' ) ) ;
1026		if ( hasBookmark )
1027			sibling = sibling.nextSibling ;
1028
1029		if ( sibling && sibling.nodeType == 1 && sibling.nodeName == element.nodeName )
1030		{
1031			if ( !attribs )
1032				attribs = this._CreateElementAttribsForComparison( element ) ;
1033
1034			if ( this._CheckAttributesMatch( sibling, attribs ) )
1035			{
1036				// Save the last child to be checked too (to merge things like <b><i></i></b><b><i></i></b>).
1037				var innerSibling = element.lastChild ;
1038
1039				if ( hasBookmark )
1040					FCKDomTools.MoveNode( element.nextSibling, element ) ;
1041
1042				// Move contents from the sibling.
1043				FCKDomTools.MoveChildren( sibling, element ) ;
1044				FCKDomTools.RemoveNode( sibling ) ;
1045
1046				// Now check the last inner child (see two comments above).
1047				if ( innerSibling )
1048					this._MergeNextSibling( innerSibling ) ;
1049			}
1050		}
1051	},
1052
1053	/**
1054	 * Merge an element with its similar siblings before it.
1055	 * "attribs" is and object computed with _CreateAttribsForComparison.
1056	 */
1057	_MergePreviousSibling : function( element, attribs )
1058	{
1059		// Check the previous sibling.
1060		var sibling = element.previousSibling ;
1061
1062		// Check if the previous sibling is a bookmark element. In this case, jump it.
1063		var hasBookmark = ( sibling && sibling.nodeType == 1 && sibling.getAttribute( '_fck_bookmark' ) ) ;
1064		if ( hasBookmark )
1065			sibling = sibling.previousSibling ;
1066
1067		if ( sibling && sibling.nodeType == 1 && sibling.nodeName == element.nodeName )
1068		{
1069			if ( !attribs )
1070				attribs = this._CreateElementAttribsForComparison( element ) ;
1071
1072			if ( this._CheckAttributesMatch( sibling, attribs ) )
1073			{
1074				// Save the first child to be checked too (to merge things like <b><i></i></b><b><i></i></b>).
1075				var innerSibling = element.firstChild ;
1076
1077				if ( hasBookmark )
1078					FCKDomTools.MoveNode( element.previousSibling, element, true ) ;
1079
1080				// Move contents to the sibling.
1081				FCKDomTools.MoveChildren( sibling, element, true ) ;
1082				FCKDomTools.RemoveNode( sibling ) ;
1083
1084				// Now check the first inner child (see two comments above).
1085				if ( innerSibling )
1086					this._MergePreviousSibling( innerSibling ) ;
1087			}
1088		}
1089	},
1090
1091	/**
1092	 * Build the cssText based on the styles definition.
1093	 */
1094	_GetStyleText : function()
1095	{
1096		var stylesDef = this._StyleDesc.Styles ;
1097
1098		// Builds the StyleText.
1099		var stylesText = ( this._StyleDesc.Attributes ? this._StyleDesc.Attributes['style'] || '' : '' ) ;
1100
1101		if ( stylesText.length > 0 )
1102			stylesText += ';' ;
1103
1104		for ( var style in stylesDef )
1105			stylesText += style + ':' + stylesDef[style] + ';' ;
1106
1107		// Browsers make some changes to the style when applying them. So, here
1108		// we normalize it to the browser format. We'll not do that if there
1109		// are variables inside the style.
1110		if ( stylesText.length > 0 && !( /#\(/.test( stylesText ) ) )
1111		{
1112			stylesText = FCKTools.NormalizeCssText( stylesText ) ;
1113		}
1114
1115		return (this._GetStyleText = function() { return stylesText ; })() ;
1116	},
1117
1118	/**
1119	 * Get the the collection used to compare the attributes defined in this
1120	 * style with attributes in an element. All information in it is lowercased.
1121	 */
1122	_GetAttribsForComparison : function()
1123	{
1124		// If we have already computed it, just return it.
1125		var attribs = this._GetAttribsForComparison_$ ;
1126		if ( attribs )
1127			return attribs ;
1128
1129		attribs = new Object() ;
1130
1131		// Loop through all defined attributes.
1132		var styleAttribs = this._StyleDesc.Attributes ;
1133		if ( styleAttribs )
1134		{
1135			for ( var styleAtt in styleAttribs )
1136			{
1137				attribs[ styleAtt.toLowerCase() ] = styleAttribs[ styleAtt ].toLowerCase() ;
1138			}
1139		}
1140
1141		// Includes the style definitions.
1142		if ( this._GetStyleText().length > 0 )
1143		{
1144			attribs['style'] = this._GetStyleText().toLowerCase() ;
1145		}
1146
1147		// Appends the "length" information to the object.
1148		FCKTools.AppendLengthProperty( attribs, '_length' ) ;
1149
1150		// Return it, saving it to the next request.
1151		return ( this._GetAttribsForComparison_$ = attribs ) ;
1152	},
1153
1154	/**
1155	 * Get the the collection used to compare the elements and attributes,
1156	 * defined in this style overrides, with other element. All information in
1157	 * it is lowercased.
1158	 */
1159	_GetOverridesForComparison : function()
1160	{
1161		// If we have already computed it, just return it.
1162		var overrides = this._GetOverridesForComparison_$ ;
1163		if ( overrides )
1164			return overrides ;
1165
1166		overrides = new Object() ;
1167
1168		var overridesDesc = this._StyleDesc.Overrides ;
1169
1170		if ( overridesDesc )
1171		{
1172			// The override description can be a string, object or array.
1173			// Internally, well handle arrays only, so transform it if needed.
1174			if ( !FCKTools.IsArray( overridesDesc ) )
1175				overridesDesc = [ overridesDesc ] ;
1176
1177			// Loop through all override definitions.
1178			for ( var i = 0 ; i < overridesDesc.length ; i++ )
1179			{
1180				var override = overridesDesc[i] ;
1181				var elementName ;
1182				var overrideEl ;
1183				var attrs ;
1184
1185				// If can be a string with the element name.
1186				if ( typeof override == 'string' )
1187					elementName = override.toLowerCase() ;
1188				// Or an object.
1189				else
1190				{
1191					elementName = override.Element ? override.Element.toLowerCase() : this.Element ;
1192					attrs = override.Attributes ;
1193				}
1194
1195				// We can have more than one override definition for the same
1196				// element name, so we attempt to simply append information to
1197				// it if it already exists.
1198				overrideEl = overrides[ elementName ] || ( overrides[ elementName ] = {} ) ;
1199
1200				if ( attrs )
1201				{
1202					// The returning attributes list is an array, because we
1203					// could have different override definitions for the same
1204					// attribute name.
1205					var overrideAttrs = ( overrideEl.Attributes = overrideEl.Attributes || new Array() ) ;
1206					for ( var attName in attrs )
1207					{
1208						// Each item in the attributes array is also an array,
1209						// where [0] is the attribute name and [1] is the
1210						// override value.
1211						overrideAttrs.push( [ attName.toLowerCase(), attrs[ attName ] ] ) ;
1212					}
1213				}
1214			}
1215		}
1216
1217		return ( this._GetOverridesForComparison_$ = overrides ) ;
1218	},
1219
1220	/*
1221	 * Create and object containing all attributes specified in an element,
1222	 * added by a "_length" property. All values are lowercased.
1223	 */
1224	_CreateElementAttribsForComparison : function( element )
1225	{
1226		var attribs = new Object() ;
1227		var attribsCount = 0 ;
1228
1229		for ( var i = 0 ; i < element.attributes.length ; i++ )
1230		{
1231			var att = element.attributes[i] ;
1232
1233			if ( att.specified )
1234			{
1235				attribs[ att.nodeName.toLowerCase() ] = FCKDomTools.GetAttributeValue( element, att ).toLowerCase() ;
1236				attribsCount++ ;
1237			}
1238		}
1239
1240		attribs._length = attribsCount ;
1241
1242		return attribs ;
1243	},
1244
1245	/**
1246	 * Checks is the element attributes have a perfect match with the style
1247	 * attributes.
1248	 */
1249	_CheckAttributesMatch : function( element, styleAttribs )
1250	{
1251		// Loop through all specified attributes. The same number of
1252		// attributes must be found and their values must match to
1253		// declare them as equal.
1254
1255		var elementAttrbs = element.attributes ;
1256		var matchCount = 0 ;
1257
1258		for ( var i = 0 ; i < elementAttrbs.length ; i++ )
1259		{
1260			var att = elementAttrbs[i] ;
1261			if ( att.specified )
1262			{
1263				var attName = att.nodeName.toLowerCase() ;
1264				var styleAtt = styleAttribs[ attName ] ;
1265
1266				// The attribute is not defined in the style.
1267				if ( !styleAtt )
1268					break ;
1269
1270				// The values are different.
1271				if ( styleAtt != FCKDomTools.GetAttributeValue( element, att ).toLowerCase() )
1272					break ;
1273
1274				matchCount++ ;
1275			}
1276		}
1277
1278		return ( matchCount == styleAttribs._length ) ;
1279	}
1280} ;
1281