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 * Defines the FCKXHtml object, responsible for the XHTML operations.
22 */
23
24var FCKXHtml = new Object() ;
25
26FCKXHtml.CurrentJobNum = 0 ;
27
28FCKXHtml.GetXHTML = function( node, includeNode, format )
29{
30	FCKDomTools.CheckAndRemovePaddingNode( node.ownerDocument, FCKConfig.EnterMode ) ;
31	FCKXHtmlEntities.Initialize() ;
32
33	// Set the correct entity to use for empty blocks.
34	this._NbspEntity = ( FCKConfig.ProcessHTMLEntities? 'nbsp' : '#160' ) ;
35
36	// Save the current IsDirty state. The XHTML processor may change the
37	// original HTML, dirtying it.
38	var bIsDirty = FCK.IsDirty() ;
39
40	// Special blocks are blocks of content that remain untouched during the
41	// process. It is used for SCRIPTs and STYLEs.
42	FCKXHtml.SpecialBlocks = new Array() ;
43
44	// Create the XML DOMDocument object.
45	this.XML = FCKTools.CreateXmlObject( 'DOMDocument' ) ;
46
47	// Add a root element that holds all child nodes.
48	this.MainNode = this.XML.appendChild( this.XML.createElement( 'xhtml' ) ) ;
49
50	FCKXHtml.CurrentJobNum++ ;
51
52//	var dTimer = new Date() ;
53
54	if ( includeNode )
55		this._AppendNode( this.MainNode, node ) ;
56	else
57		this._AppendChildNodes( this.MainNode, node, false ) ;
58
59	// Get the resulting XHTML as a string.
60	var sXHTML = this._GetMainXmlString() ;
61
62//	alert( 'Time: ' + ( ( ( new Date() ) - dTimer ) ) + ' ms' ) ;
63
64	this.XML = null ;
65
66	// Safari adds xmlns="http://www.w3.org/1999/xhtml" to the root node (#963)
67	if ( FCKBrowserInfo.IsSafari )
68		sXHTML = sXHTML.replace( /^<xhtml.*?>/, '<xhtml>' ) ;
69
70	// Strip the "XHTML" root node.
71	sXHTML = sXHTML.substr( 7, sXHTML.length - 15 ).Trim() ;
72
73	// Add a space in the tags with no closing tags, like <br/> -> <br />
74	sXHTML = sXHTML.replace( FCKRegexLib.SpaceNoClose, ' />');
75
76	if ( FCKConfig.ForceSimpleAmpersand )
77		sXHTML = sXHTML.replace( FCKRegexLib.ForceSimpleAmpersand, '&' ) ;
78
79	if ( format )
80		sXHTML = FCKCodeFormatter.Format( sXHTML ) ;
81
82	// Now we put back the SpecialBlocks contents.
83	for ( var i = 0 ; i < FCKXHtml.SpecialBlocks.length ; i++ )
84	{
85		var oRegex = new RegExp( '___FCKsi___' + i ) ;
86		sXHTML = sXHTML.replace( oRegex, FCKXHtml.SpecialBlocks[i] ) ;
87	}
88
89	// Replace entities marker with the ampersand.
90	sXHTML = sXHTML.replace( FCKRegexLib.GeckoEntitiesMarker, '&' ) ;
91
92	// Restore the IsDirty state if it was not dirty.
93	if ( !bIsDirty )
94		FCK.ResetIsDirty() ;
95
96	FCKDomTools.EnforcePaddingNode( FCKTools.GetElementDocument( node ), FCKConfig.EnterMode ) ;
97	return sXHTML ;
98}
99
100FCKXHtml._AppendAttribute = function( xmlNode, attributeName, attributeValue )
101{
102	try
103	{
104		if ( attributeValue == undefined || attributeValue == null )
105			attributeValue = '' ;
106		else if ( attributeValue.replace )
107		{
108			if ( FCKConfig.ForceSimpleAmpersand )
109				attributeValue = attributeValue.replace( /&/g, '___FCKAmp___' ) ;
110
111			// Entities must be replaced in the attribute values.
112			attributeValue = attributeValue.replace( FCKXHtmlEntities.EntitiesRegex, FCKXHtml_GetEntity ) ;
113		}
114
115		// Create the attribute.
116		var oXmlAtt = this.XML.createAttribute( attributeName ) ;
117		oXmlAtt.value = attributeValue ;
118
119		// Set the attribute in the node.
120		xmlNode.attributes.setNamedItem( oXmlAtt ) ;
121	}
122	catch (e)
123	{}
124}
125
126FCKXHtml._AppendChildNodes = function( xmlNode, htmlNode, isBlockElement )
127{
128	var oNode = htmlNode.firstChild ;
129
130	while ( oNode )
131	{
132		this._AppendNode( xmlNode, oNode ) ;
133		oNode = oNode.nextSibling ;
134	}
135
136	// Trim block elements. This is also needed to avoid Firefox leaving extra
137	// BRs at the end of them.
138	if ( isBlockElement && htmlNode.tagName && htmlNode.tagName.toLowerCase() != 'pre' )
139	{
140		FCKDomTools.TrimNode( xmlNode ) ;
141
142		if ( FCKConfig.FillEmptyBlocks )
143		{
144			var lastChild = xmlNode.lastChild ;
145			if ( lastChild && lastChild.nodeType == 1 && lastChild.nodeName == 'br' )
146				this._AppendEntity( xmlNode, this._NbspEntity ) ;
147		}
148	}
149
150	// If the resulting node is empty.
151	if ( xmlNode.childNodes.length == 0 )
152	{
153		if ( isBlockElement && FCKConfig.FillEmptyBlocks )
154		{
155			this._AppendEntity( xmlNode, this._NbspEntity ) ;
156			return xmlNode ;
157		}
158
159		var sNodeName = xmlNode.nodeName ;
160
161		// Some inline elements are required to have something inside (span, strong, etc...).
162		if ( FCKListsLib.InlineChildReqElements[ sNodeName ] )
163			return null ;
164
165		// We can't use short representation of empty elements that are not marked
166		// as empty in th XHTML DTD.
167		if ( !FCKListsLib.EmptyElements[ sNodeName ] )
168			xmlNode.appendChild( this.XML.createTextNode('') ) ;
169	}
170
171	return xmlNode ;
172}
173
174FCKXHtml._AppendNode = function( xmlNode, htmlNode )
175{
176	if ( !htmlNode )
177		return false ;
178
179	switch ( htmlNode.nodeType )
180	{
181		// Element Node.
182		case 1 :
183			// If we detect a <br> inside a <pre> in Gecko, turn it into a line break instead.
184			// This is a workaround for the Gecko bug here: https://bugzilla.mozilla.org/show_bug.cgi?id=92921
185			if ( FCKBrowserInfo.IsGecko
186					&& htmlNode.tagName.toLowerCase() == 'br'
187					&& htmlNode.parentNode.tagName.toLowerCase() == 'pre' )
188			{
189				var val = '\r' ;
190				if ( htmlNode == htmlNode.parentNode.firstChild )
191					val += '\r' ;
192				return FCKXHtml._AppendNode( xmlNode, this.XML.createTextNode( val ) ) ;
193			}
194
195			// Here we found an element that is not the real element, but a
196			// fake one (like the Flash placeholder image), so we must get the real one.
197			if ( htmlNode.getAttribute('_fckfakelement') )
198				return FCKXHtml._AppendNode( xmlNode, FCK.GetRealElement( htmlNode ) ) ;
199
200			// Ignore bogus BR nodes in the DOM.
201			if ( FCKBrowserInfo.IsGecko &&
202					( htmlNode.hasAttribute('_moz_editor_bogus_node') || htmlNode.getAttribute( 'type' ) == '_moz' ) )
203				return false ;
204
205			// This is for elements that are instrumental to FCKeditor and
206			// must be removed from the final HTML.
207			if ( htmlNode.getAttribute('_fcktemp') )
208				return false ;
209
210			// Get the element name.
211			var sNodeName = htmlNode.tagName.toLowerCase()  ;
212
213			if ( FCKBrowserInfo.IsIE )
214			{
215				// IE doens't include the scope name in the nodeName. So, add the namespace.
216				if ( htmlNode.scopeName && htmlNode.scopeName != 'HTML' && htmlNode.scopeName != 'FCK' )
217					sNodeName = htmlNode.scopeName.toLowerCase() + ':' + sNodeName ;
218			}
219			else
220			{
221				if ( sNodeName.StartsWith( 'fck:' ) )
222					sNodeName = sNodeName.Remove( 0,4 ) ;
223			}
224
225			// Check if the node name is valid, otherwise ignore this tag.
226			// If the nodeName starts with a slash, it is a orphan closing tag.
227			// On some strange cases, the nodeName is empty, even if the node exists.
228			if ( !FCKRegexLib.ElementName.test( sNodeName ) )
229				return false ;
230
231			// The already processed nodes must be marked to avoid then to be duplicated (bad formatted HTML).
232			// So here, the "mark" is checked... if the element is Ok, then mark it.
233			if ( htmlNode._fckxhtmljob && htmlNode._fckxhtmljob == FCKXHtml.CurrentJobNum )
234				return false ;
235
236			var oNode = this.XML.createElement( sNodeName ) ;
237
238			// Add all attributes.
239			FCKXHtml._AppendAttributes( xmlNode, htmlNode, oNode, sNodeName ) ;
240
241			htmlNode._fckxhtmljob = FCKXHtml.CurrentJobNum ;
242
243			// Tag specific processing.
244			var oTagProcessor = FCKXHtml.TagProcessors[ sNodeName ] ;
245
246			if ( oTagProcessor )
247				oNode = oTagProcessor( oNode, htmlNode, xmlNode ) ;
248			else
249				oNode = this._AppendChildNodes( oNode, htmlNode, Boolean( FCKListsLib.NonEmptyBlockElements[ sNodeName ] ) ) ;
250
251			if ( !oNode )
252				return false ;
253
254			xmlNode.appendChild( oNode ) ;
255
256			break ;
257
258		// Text Node.
259		case 3 :
260			if ( htmlNode.parentNode && htmlNode.parentNode.nodeName.IEquals( 'pre' ) )
261				return this._AppendTextNode( xmlNode, htmlNode.nodeValue ) ;
262			return this._AppendTextNode( xmlNode, htmlNode.nodeValue.ReplaceNewLineChars(' ') ) ;
263
264		// Comment
265		case 8 :
266			// IE catches the <!DOTYPE ... > as a comment, but it has no
267			// innerHTML, so we can catch it, and ignore it.
268			if ( FCKBrowserInfo.IsIE && !htmlNode.innerHTML )
269				break ;
270
271			try { xmlNode.appendChild( this.XML.createComment( htmlNode.nodeValue ) ) ; }
272			catch (e) { /* Do nothing... probably this is a wrong format comment. */ }
273			break ;
274
275		// Unknown Node type.
276		default :
277			xmlNode.appendChild( this.XML.createComment( "Element not supported - Type: " + htmlNode.nodeType + " Name: " + htmlNode.nodeName ) ) ;
278			break ;
279	}
280	return true ;
281}
282
283// Append an item to the SpecialBlocks array and returns the tag to be used.
284FCKXHtml._AppendSpecialItem = function( item )
285{
286	return '___FCKsi___' + FCKXHtml.SpecialBlocks.AddItem( item ) ;
287}
288
289FCKXHtml._AppendEntity = function( xmlNode, entity )
290{
291	xmlNode.appendChild( this.XML.createTextNode( '#?-:' + entity + ';' ) ) ;
292}
293
294FCKXHtml._AppendTextNode = function( targetNode, textValue )
295{
296	var bHadText = textValue.length > 0 ;
297	if ( bHadText )
298		targetNode.appendChild( this.XML.createTextNode( textValue.replace( FCKXHtmlEntities.EntitiesRegex, FCKXHtml_GetEntity ) ) ) ;
299	return bHadText ;
300}
301
302// Retrieves a entity (internal format) for a given character.
303function FCKXHtml_GetEntity( character )
304{
305	// We cannot simply place the entities in the text, because the XML parser
306	// will translate & to &amp;. So we use a temporary marker which is replaced
307	// in the end of the processing.
308	var sEntity = FCKXHtmlEntities.Entities[ character ] || ( '#' + character.charCodeAt(0) ) ;
309	return '#?-:' + sEntity + ';' ;
310}
311
312// An object that hold tag specific operations.
313FCKXHtml.TagProcessors =
314{
315	img : function( node, htmlNode )
316	{
317		// The "ALT" attribute is required in XHTML.
318		if ( ! node.attributes.getNamedItem( 'alt' ) )
319			FCKXHtml._AppendAttribute( node, 'alt', '' ) ;
320
321		var sSavedUrl = htmlNode.getAttribute( '_fcksavedurl' ) ;
322		if ( sSavedUrl != null )
323			FCKXHtml._AppendAttribute( node, 'src', sSavedUrl ) ;
324
325		return node ;
326	},
327
328	a : function( node, htmlNode )
329	{
330		// Firefox may create empty tags when deleting the selection in some special cases (SF-BUG 1556878).
331		if ( htmlNode.innerHTML.Trim().length == 0 && !htmlNode.name )
332			return false ;
333
334		var sSavedUrl = htmlNode.getAttribute( '_fcksavedurl' ) ;
335		if ( sSavedUrl != null )
336			FCKXHtml._AppendAttribute( node, 'href', sSavedUrl ) ;
337
338
339		// Anchors with content has been marked with an additional class, now we must remove it.
340		if ( FCKBrowserInfo.IsIE )
341		{
342			// Buggy IE, doesn't copy the name of changed anchors.
343			if ( htmlNode.name )
344				FCKXHtml._AppendAttribute( node, 'name', htmlNode.name ) ;
345		}
346
347		node = FCKXHtml._AppendChildNodes( node, htmlNode, false ) ;
348
349		return node ;
350	},
351
352	script : function( node, htmlNode )
353	{
354		// The "TYPE" attribute is required in XHTML.
355		if ( ! node.attributes.getNamedItem( 'type' ) )
356			FCKXHtml._AppendAttribute( node, 'type', 'text/javascript' ) ;
357
358		node.appendChild( FCKXHtml.XML.createTextNode( FCKXHtml._AppendSpecialItem( htmlNode.text ) ) ) ;
359
360		return node ;
361	},
362
363	style : function( node, htmlNode )
364	{
365		// The "TYPE" attribute is required in XHTML.
366		if ( ! node.attributes.getNamedItem( 'type' ) )
367			FCKXHtml._AppendAttribute( node, 'type', 'text/css' ) ;
368
369		var cssText = htmlNode.innerHTML ;
370		if ( FCKBrowserInfo.IsIE )	// Bug #403 : IE always appends a \r\n to the beginning of StyleNode.innerHTML
371			cssText = cssText.replace( /^(\r\n|\n|\r)/, '' ) ;
372
373		node.appendChild( FCKXHtml.XML.createTextNode( FCKXHtml._AppendSpecialItem( cssText ) ) ) ;
374
375		return node ;
376	},
377
378	pre : function ( node, htmlNode )
379	{
380		for ( var i = 0 ; i < htmlNode.childNodes.length ; i++ )
381		{
382			var item = htmlNode.childNodes[i] ;
383			var val = item.nodeValue ;
384			if ( item.nodeType == 3 && i == 0 )
385				node.appendChild( FCKXHtml.XML.createTextNode( FCKXHtml._AppendSpecialItem( '\r\n' + val ) ) ) ;
386			else
387				FCKXHtml._AppendNode( node, item ) ;
388		}
389		return node ;
390	},
391
392	title : function( node, htmlNode )
393	{
394		node.appendChild( FCKXHtml.XML.createTextNode( FCK.EditorDocument.title ) ) ;
395
396		return node ;
397	},
398
399	// Fix nested <ul> and <ol>.
400	ol : function( node, htmlNode, targetNode )
401	{
402		if ( htmlNode.innerHTML.Trim().length == 0 )
403			return false ;
404
405		var ePSibling = targetNode.lastChild ;
406
407		if ( ePSibling && ePSibling.nodeType == 3 )
408			ePSibling = ePSibling.previousSibling ;
409
410		if ( ePSibling && ePSibling.nodeName.toUpperCase() == 'LI' )
411		{
412			htmlNode._fckxhtmljob = null ;
413			FCKXHtml._AppendNode( ePSibling, htmlNode ) ;
414			return false ;
415		}
416
417		node = FCKXHtml._AppendChildNodes( node, htmlNode ) ;
418
419		return node ;
420	},
421
422	span : function( node, htmlNode )
423	{
424		// Firefox may create empty tags when deleting the selection in some special cases (SF-BUG 1084404).
425		if ( htmlNode.innerHTML.length == 0 )
426			return false ;
427
428		node = FCKXHtml._AppendChildNodes( node, htmlNode, false ) ;
429
430		return node ;
431	},
432
433	// IE loses contents of iframes, and Gecko does give it back HtmlEncoded
434	// Note: Opera does lose the content and doesn't provide it in the innerHTML string
435	iframe : function( node, htmlNode )
436	{
437		var sHtml = htmlNode.innerHTML ;
438
439		// Gecko does give back the encoded html
440		if ( FCKBrowserInfo.IsGecko )
441			sHtml = FCKTools.HTMLDecode( sHtml );
442
443		// Remove the saved urls here as the data won't be processed as nodes
444		sHtml = sHtml.replace( /\s_fcksavedurl="[^"]*"/g, '' ) ;
445
446		node.appendChild( FCKXHtml.XML.createTextNode( FCKXHtml._AppendSpecialItem( sHtml ) ) ) ;
447
448		return node ;
449	},
450
451	body : function( node, htmlNode )
452	{
453		node = FCKXHtml._AppendChildNodes( node, htmlNode, false ) ;
454		// Remove spellchecker attributes added for Firefox when converting to HTML code (Bug #1351).
455		node.removeAttribute( 'spellcheck' ) ;
456		return node ;
457	}
458} ;
459
460FCKXHtml.TagProcessors.ul = FCKXHtml.TagProcessors.ol ;
461