dojo.provide("dojox.editor.plugins.NormalizeStyle");

dojo.require("dijit._editor._Plugin");
dojo.require("dijit._editor.html");

dojo.experimental("dojox.editor.plugins.NormalizeStyle");

dojo.declare("dojox.editor.plugins.NormalizeStyle",dijit._editor._Plugin,{
	//	summary:
	//		This plugin provides NormalizeStyle cabability to the editor.  It is
	//		a headless plugin that tries to normalize how content is styled when
	//		it comes out of th editor ('b' or css).   It also auto-converts
	//		incoming content to the proper one expected by the browser as well so 
	//		that the native styling buttons work.

	// mode [public] String
	//		A String variable indicating if it should use semantic tags 'b', 'i', etc, or 
	//		CSS styling.  The default is semantic.
	mode: "semantic",

	// condenseSpans [public] Boolean
	//		A boolean variable indicating if it should try to condense 
	//		'span''span''span' styles  when in css mode 
	//		The default is true, it will try to combine where it can.
	condenseSpans: true,

	setEditor: function(editor){
		// summary:
		//		Over-ride for the setting of the editor.
		// editor: Object
		//		The editor to configure for this plugin to use.
		this.editor = editor;
		editor.customUndo = true;

		if(this.mode === "semantic"){
			this.editor.contentDomPostFilters.push(dojo.hitch(this, this._convertToSemantic));
		}else if(this.mode === "css"){
			this.editor.contentDomPostFilters.push(dojo.hitch(this, this._convertToCss));
		}

		// Pre DOM filters are usually based on what browser, as they all use different ways to
		// apply styles with actions and modify them.
		if(dojo.isIE){
			// IE still uses sematic tags most of the time, so convert to that.
			this.editor.contentDomPreFilters.push(dojo.hitch(this, this._convertToSemantic));
			this._browserFilter = this._convertToSemantic;
		}else if(dojo.isWebKit){
			this.editor.contentDomPreFilters.push(dojo.hitch(this, this._convertToCss));
			this._browserFilter = this._convertToCss;
		}else if(dojo.isMoz){
			//Editor currently forces Moz into semantic mode, so we need to match.  Ideally
			//editor could get rid of that and just use CSS mode, whitch would work cleaner
			//That's why this is split out, to make it easy to change later.
			this.editor.contentDomPreFilters.push(dojo.hitch(this, this._convertToSemantic));
			this._browserFilter = this._convertToSemantic;
		}else{
			this.editor.contentDomPreFilters.push(dojo.hitch(this, this._convertToSemantic));
			this._browserFilter = this._convertToSemantic;
		}

		// Set up the inserthtml impl over-ride.  This catches semi-paste events and
		// tries to normalize them too.
		if(this.editor._inserthtmlImpl){
			this.editor._oldInsertHtmlImpl = this.editor._inserthtmlImpl;
		}
		this.editor._inserthtmlImpl = dojo.hitch(this, this._inserthtmlImpl);
	},

	_convertToSemantic: function(node){
		// summary:
		//		A function to convert the HTML structure of 'node' into 
		//		sematic tags where possible.
		// node: DOMNode
		//		The node to process.
		// tags:
		//		private
		if(node){
			var w = this.editor.window;
			var self = this;
			var convertNode = function(cNode){
				if(cNode.nodeType == 1){
					if(cNode.id !== "dijitEditorBody"){
						var style = cNode.style;
						var tag = cNode.tagName?cNode.tagName.toLowerCase():"";
						var sTag;
						if(style && tag != "table" && tag != "ul" && tag != "ol"){
							// Avoid wrapper blocks that have specific underlying structure, as injecting
							// spans/etc there is invalid.
							// Lets check and convert certain node/style types.
							var fw = style.fontWeight? style.fontWeight.toLowerCase() : "";
							var fs = style.fontStyle? style.fontStyle.toLowerCase() : "";
							var td = style.textDecoration? style.textDecoration.toLowerCase() : "";
							var s = style.fontSize?style.fontSize.toLowerCase() : "";
							var bc = style.backgroundColor?style.backgroundColor.toLowerCase() : "";
							var c = style.color?style.color.toLowerCase() : "";
	
							var wrapNodes = function(wrap, pNode){
								if(wrap){
									while(pNode.firstChild){
										wrap.appendChild(pNode.firstChild);
									}
									if(tag == "span" && !pNode.style.cssText){
										// A styler tag with nothing extra in it, so lets remove it.
										dojo.place(wrap, pNode, "before");
										pNode.parentNode.removeChild(pNode);
										pNode = wrap;
									}else{
										pNode.appendChild(wrap);
									}
								}
								return pNode;
							};
							switch(fw){
								case "bold":
								case "bolder":
								case "700":
								case "800":
								case "900":
									sTag = dojo.withGlobal(w, "create", dojo, ["b", {}] );
									cNode.style.fontWeight = "";
									break;
							}
							cNode = wrapNodes(sTag, cNode);
							sTag = null;
							if(fs == "italic"){
								sTag = dojo.withGlobal(w, "create", dojo, ["i", {}] );
								cNode.style.fontStyle = "";
							}
							cNode = wrapNodes(sTag, cNode);
							sTag = null;
							if(td){
								var da = td.split(" ");
								var count = 0;
								dojo.forEach(da, function(s){
									switch(s){
										case "underline":
											sTag = dojo.withGlobal(w, "create", dojo, ["u", {}] );
											break;
										case "line-through": 
											sTag = dojo.withGlobal(w, "create", dojo, ["strike", {}] );
											break;
									}
									count++;
									if(count == da.length){
										// Last one, clear the decor and see if we can span strip on wrap.
										cNode.style.textDecoration = "";
									}
									cNode = wrapNodes(sTag, cNode);
									sTag = null;
								});
								
							}
							if(s){
								var sizeMap = {
									"xx-small": 1,
									"x-small": 2,
									"small": 3,
									"medium": 4,
									"large": 5,
									"x-large": 6,
									"xx-large": 7,
									"-webkit-xxx-large": 7
								};

								// Convert point or px size to size 
								// to something roughly mappable.
								if(s.indexOf("pt") > 0){
									s = s.substring(0,s.indexOf("pt"));
									s = parseInt(s);
									if(s < 5){
										s = "xx-small";
									}else if(s < 10){
										s = "x-small";
									}else if(s < 15){
										s = "small";
									}else if(s < 20){
										s = "medium";
									}else if(s < 25){
										s = "large";
									}else if(s < 30){
										s = "x-large";
									}else if(s > 30){
										s = "xx-large";
									}
								}else if(s.indexOf("px") > 0){
									s = s.substring(0,s.indexOf("px"));
									s = parseInt(s);
									if(s < 5){
										s = "xx-small";
									}else if(s < 10){
										s = "x-small";
									}else if(s < 15){
										s = "small";
									}else if(s < 20){
										s = "medium";
									}else if(s < 25){
										s = "large";
									}else if(s < 30){
										s = "x-large";
									}else if(s > 30){
										s = "xx-large";
									}
								}
								var size = sizeMap[s];
								if(!size){
									size = 3;
								}
								sTag = dojo.withGlobal(w, "create", dojo, ["font", {size: size}] );
								cNode.style.fontSize = "";
							}
							cNode = wrapNodes(sTag, cNode);
							sTag = null;
							if(bc && tag !== "font" && self._isInline(tag)){
								// IE doesn't like non-font background color crud.
								// Also, don't move it in if the background color is set on a block style node, 
								// as it won't color properly once put on inline font.
								bc = new dojo.Color(bc).toHex();
								sTag = dojo.withGlobal(w, "create", dojo, ["font", {style: {backgroundColor: bc}}] );
								cNode.style.backgroundColor = "";
							}
							if(c && tag !== "font"){
								// IE doesn't like non-font background color crud.
								c = new dojo.Color(c).toHex();
								sTag = dojo.withGlobal(w, "create", dojo, ["font", {color: c}] );
								cNode.style.color = "";
							}
							cNode = wrapNodes(sTag, cNode);
							sTag = null;
						}
					}
					if(cNode.childNodes){
						// Clone it, since we may alter its position
						var nodes = [];
						dojo.forEach(cNode.childNodes, function(n){ nodes.push(n);});
						dojo.forEach(nodes, convertNode);
					}
				}
				return cNode;
			};
			return this._normalizeTags(convertNode(node));
		}
		return node;
	},
	
	_normalizeTags: function(node){
		// summary: 
		//		A function to handle normalizing certain tag types contained under 'node'
		// node:
		//		The node to search from.
		// tags:
		//		Protected.
		var w = this.editor.window;
		var nodes = dojo.withGlobal(w, function() {
			return dojo.query("em,s,strong", node);
		});
		if(nodes && nodes.length){
			dojo.forEach(nodes, function(n){
				if(n){
					var tag = n.tagName?n.tagName.toLowerCase():"";
					var tTag;
					switch(tag){
						case "s":
								tTag = "strike";
								break;
						case "em":
								tTag = "i";
								break;
						case "strong":
								tTag = "b";
								break;
					}
					if(tTag){
						var nNode = dojo.withGlobal(w, "create", dojo, [tTag, null, n, "before"] );	 
						while(n.firstChild){
							nNode.appendChild(n.firstChild);
						}
						n.parentNode.removeChild(n);
					}
				}
			});
		}
		return node;
	},

	_convertToCss: function(node){
		// summary:
		//		A function to convert the HTML structure of 'node' into 
		//		css span styles around text instead of sematic tags.
		//		Note:  It does not do compression of dpans together.
		// node: DOMNode
		//		The node to process
		// tags:
		//		private
		if(node){
			var w = this.editor.window;
			var convertNode = function(cNode) {
				if(cNode.nodeType == 1){
					if(cNode.id !== "dijitEditorBody"){
						var tag = cNode.tagName?cNode.tagName.toLowerCase():"";
						if(tag){
							var span;
							switch(tag){
								case "b":
								case "strong": // Mainly IE
									span = dojo.withGlobal(w, "create", dojo, ["span", {style: {"fontWeight": "bold"}}] );
									break;
								case "i":
								case "em": // Mainly IE
									span = dojo.withGlobal(w, "create", dojo, ["span", {style: {"fontStyle": "italic"}}] );
									break;
								case "u":
									span = dojo.withGlobal(w, "create", dojo, ["span", {style: {"textDecoration": "underline"}}] );
									break;
								case "strike":
								case "s": // Mainly WebKit.
									span = dojo.withGlobal(w, "create", dojo, ["span", {style: {"textDecoration": "line-through"}}] );
									break;
								case "font": // Try to deal with colors
									var styles = {};
									if(dojo.attr(cNode, "color")){
										styles.color = dojo.attr(cNode, "color");
									}
									if(dojo.attr(cNode, "face")){
										styles.fontFace = dojo.attr(cNode, "face");
									}
									if(cNode.style && cNode.style.backgroundColor){
										styles.backgroundColor = cNode.style.backgroundColor;
									}
									if(cNode.style && cNode.style.color){
										styles.color = cNode.style.color;
									}
									var sizeMap = {
										1: "xx-small",
										2: "x-small",
										3: "small",
										4: "medium",
										5: "large",
										6: "x-large",
										7: "xx-large"
									};
									if(dojo.attr(cNode, "size")){
										styles.fontSize = sizeMap[dojo.attr(cNode, "size")];
									}
									span = dojo.withGlobal(w, "create", dojo, ["span", {style: styles}] );
									break;
							}
							if(span){
								while(cNode.firstChild){
									span.appendChild(cNode.firstChild);
								}
								dojo.place(span, cNode, "before");
								cNode.parentNode.removeChild(cNode);
								cNode = span;
							}
						}
					}
					if(cNode.childNodes){
						// Clone it, since we may alter its position
						var nodes = [];
						dojo.forEach(cNode.childNodes, function(n){ nodes.push(n);});
						dojo.forEach(nodes, convertNode);
					}
				}
				return cNode;
			};
			node = convertNode(node);
			if(this.condenseSpans){
				this._condenseSpans(node);
			}
		}
		return node;
	},

	_condenseSpans: function(node){
		// summary:
		//		Method to condense spans if you end up with multi-wrapping from
		//		from converting b, i, u, to span nodes.
		// node:
		//		The node (and its children), to process.
		// tags:
		//		private
		var compressSpans = function(node){
			// Okay, span with no class or id and it has styles.
			// So, merge the styles, then collapse.  Merge requires determining
			// all the common/different styles and anything that overlaps the style, 
			// but a different value can't be merged.
			var genStyleMap = function(styleText){
				var m;
				if(styleText){
					m = {};
					var styles = styleText.toLowerCase().split(";");
					dojo.forEach(styles, function(s){
						if(s){
							var ss = s.split(":");
							var key = ss[0] ? dojo.trim(ss[0]): "";
							var val = ss[1] ? dojo.trim(ss[1]): "";
							if(key && val){
								var i;
								var nKey = "";
								for(i = 0; i < key.length; i++){
									var ch = key.charAt(i);
									if(ch == "-"){
										i++;
										ch = key.charAt(i);
										nKey += ch.toUpperCase();
									}else{
										nKey += ch;
									}
								}
								m[nKey] = val;
							}
						}
					});
				}
				return m;
			};
			if(node && node.nodeType == 1){
				var tag = node.tagName? node.tagName.toLowerCase() : "";
				if(tag === "span" && node.childNodes && node.childNodes.length === 1){
					// Okay, a possibly compressable span
					var c = node.firstChild;
					while(c && c.nodeType == 1 && c.tagName && c.tagName.toLowerCase() == "span"){
						if(!dojo.attr(c, "class") && !dojo.attr(c, "id") && c.style){
							var s1 = genStyleMap(node.style.cssText);
							var s2 = genStyleMap(c.style.cssText);
							if(s1 && s2){
								// Maps, so lets see if we can combine them.
								var combinedMap = {};
								var i;
								for(i in s1){
									if(!s1[i] || !s2[i] || s1[i] == s2[i]){
										combinedMap[i] = s1[i];
										delete s2[i];
									}else if(s1[i] != s2[i]){
										// Collision, cannot merge.
										// IE does not handle combined uderline strikethrough text
										// decoraations on a single span.
										if(i == "textDecoration"){
											combinedMap[i] = s1[i] + " " + s2[i];
											delete s2[i];
										}else{
											combinedMap = null;	
										}
										break;
									}else{
										combinedMap = null;
										break;
									}
								}
								if(combinedMap){
									for(i in s2){
										combinedMap[i] = s2[i];
									}
									dojo.style(node, combinedMap);
									while(c.firstChild){
										node.appendChild(c.firstChild);
									}
									var t = c.nextSibling;
									c.parentNode.removeChild(c);
									c = t;
								}else{
									c = c.nextSibling;
								}
							}else{
								c = c.nextSibling;
							}
						}else{
							c = c.nextSibling;
						}
					}
				}
			}
			if(node.childNodes && node.childNodes.length){
				dojo.forEach(node.childNodes, compressSpans);
			}
		};
		compressSpans(node);
	},
	
	_isInline: function(tag){
		// summary:
		//		Function to determine if the current tag is an inline
		//		element that does formatting, as we don't want to 
		//		try to combine inlines with divs on styles.
		// tag:
		//		The tag to examine
		// tags:
		//		private
		switch(tag){
			case "a":
			case "b":
			case "strong":
			case "s":
			case "strike":
			case "i":
			case "u":
			case "em":
			case "sup":
			case "sub":
			case "span":
			case "font":
			case "big":
			case "cite":
			case "q":
			case "img":
			case "small":
				return true;
			default:
				return false;
		}
	},

	_inserthtmlImpl: function(html){
		// summary:
		//		Function to trap and over-ride the editor inserthtml implementation
		//		to try and filter it to match the editor's internal styling mode.
		//		Helpful for plugins like PasteFromWord, in that it extra-filters
		//		and normalizes the input if it can.
		// html:
		//		The HTML string to insert.
		// tags:
		//		private
		if(html){
			var div = this.editor.document.createElement("div");
			div.innerHTML = html;
			div = this._browserFilter(div);
			html = dijit._editor.getChildrenHtml(div);
			div.innerHTML = "";

			// Call the over-ride, or if not available, just execute it.
			if(this.editor._oldInsertHtmlImpl){
				return this.editor._oldInsertHtmlImpl(html);
			}else{
				return this.editor.execCommand("inserthtml", html);
			}
		}
		return false;
	}
});

// Register this plugin.
dojo.subscribe(dijit._scopeName + ".Editor.getPlugin",null,function(o){
	if(o.plugin){ return; }
	var name = o.args.name.toLowerCase();
	if(name === "normalizestyle"){
		o.plugin = new dojox.editor.plugins.NormalizeStyle({
			mode: ("mode" in o.args)?o.args.mode:"semantic",
			condenseSpans: ("condenseSpans" in o.args)?o.args.condenseSpans:true
		});
	}
});
