src/myams/resources/js/ext/tinymce/dev/classes/Formatter.js
changeset 0 f05d7aea098a
equal deleted inserted replaced
-1:000000000000 0:f05d7aea098a
       
     1 /**
       
     2  * Formatter.js
       
     3  *
       
     4  * Copyright, Moxiecode Systems AB
       
     5  * Released under LGPL License.
       
     6  *
       
     7  * License: http://www.tinymce.com/license
       
     8  * Contributing: http://www.tinymce.com/contributing
       
     9  */
       
    10 
       
    11 /**
       
    12  * Text formatter engine class. This class is used to apply formats like bold, italic, font size
       
    13  * etc to the current selection or specific nodes. This engine was build to replace the browsers
       
    14  * default formatting logic for execCommand due to it's inconsistent and buggy behavior.
       
    15  *
       
    16  * @class tinymce.Formatter
       
    17  * @example
       
    18  *  tinymce.activeEditor.formatter.register('mycustomformat', {
       
    19  *    inline: 'span',
       
    20  *    styles: {color: '#ff0000'}
       
    21  *  });
       
    22  *
       
    23  *  tinymce.activeEditor.formatter.apply('mycustomformat');
       
    24  */
       
    25 define("tinymce/Formatter", [
       
    26 	"tinymce/dom/TreeWalker",
       
    27 	"tinymce/dom/RangeUtils",
       
    28 	"tinymce/dom/BookmarkManager",
       
    29 	"tinymce/dom/ElementUtils",
       
    30 	"tinymce/util/Tools",
       
    31 	"tinymce/fmt/Preview"
       
    32 ], function(TreeWalker, RangeUtils, BookmarkManager, ElementUtils, Tools, Preview) {
       
    33 	/**
       
    34 	 * Constructs a new formatter instance.
       
    35 	 *
       
    36 	 * @constructor Formatter
       
    37 	 * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
       
    38 	 */
       
    39 	return function(ed) {
       
    40 		var formats = {},
       
    41 			dom = ed.dom,
       
    42 			selection = ed.selection,
       
    43 			rangeUtils = new RangeUtils(dom),
       
    44 			isValid = ed.schema.isValidChild,
       
    45 			isBlock = dom.isBlock,
       
    46 			forcedRootBlock = ed.settings.forced_root_block,
       
    47 			nodeIndex = dom.nodeIndex,
       
    48 			INVISIBLE_CHAR = '\uFEFF',
       
    49 			MCE_ATTR_RE = /^(src|href|style)$/,
       
    50 			FALSE = false,
       
    51 			TRUE = true,
       
    52 			formatChangeData,
       
    53 			undef,
       
    54 			getContentEditable = dom.getContentEditable,
       
    55 			disableCaretContainer,
       
    56 			markCaretContainersBogus,
       
    57 			isBookmarkNode = BookmarkManager.isBookmarkNode;
       
    58 
       
    59 		var each = Tools.each,
       
    60 			grep = Tools.grep,
       
    61 			walk = Tools.walk,
       
    62 			extend = Tools.extend;
       
    63 
       
    64 		function isTextBlock(name) {
       
    65 			if (name.nodeType) {
       
    66 				name = name.nodeName;
       
    67 			}
       
    68 
       
    69 			return !!ed.schema.getTextBlockElements()[name.toLowerCase()];
       
    70 		}
       
    71 
       
    72 		function isTableCell(node) {
       
    73 			return /^(TH|TD)$/.test(node.nodeName);
       
    74 		}
       
    75 
       
    76 		function getParents(node, selector) {
       
    77 			return dom.getParents(node, selector, dom.getRoot());
       
    78 		}
       
    79 
       
    80 		function isCaretNode(node) {
       
    81 			return node.nodeType === 1 && node.id === '_mce_caret';
       
    82 		}
       
    83 
       
    84 		function defaultFormats() {
       
    85 			register({
       
    86 				valigntop: [
       
    87 					{selector: 'td,th', styles: {'verticalAlign': 'top'}}
       
    88 				],
       
    89 
       
    90 				valignmiddle: [
       
    91 					{selector: 'td,th', styles: {'verticalAlign': 'middle'}}
       
    92 				],
       
    93 
       
    94 				valignbottom: [
       
    95 					{selector: 'td,th', styles: {'verticalAlign': 'bottom'}}
       
    96 				],
       
    97 
       
    98 				alignleft: [
       
    99 					{selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}, defaultBlock: 'div'},
       
   100 					{selector: 'img,table', collapsed: false, styles: {'float': 'left'}}
       
   101 				],
       
   102 
       
   103 				aligncenter: [
       
   104 					{selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'center'}, defaultBlock: 'div'},
       
   105 					{selector: 'img', collapsed: false, styles: {display: 'block', marginLeft: 'auto', marginRight: 'auto'}},
       
   106 					{selector: 'table', collapsed: false, styles: {marginLeft: 'auto', marginRight: 'auto'}}
       
   107 				],
       
   108 
       
   109 				alignright: [
       
   110 					{selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'right'}, defaultBlock: 'div'},
       
   111 					{selector: 'img,table', collapsed: false, styles: {'float': 'right'}}
       
   112 				],
       
   113 
       
   114 				alignjustify: [
       
   115 					{selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'justify'}, defaultBlock: 'div'}
       
   116 				],
       
   117 
       
   118 				bold: [
       
   119 					{inline: 'strong', remove: 'all'},
       
   120 					{inline: 'span', styles: {fontWeight: 'bold'}},
       
   121 					{inline: 'b', remove: 'all'}
       
   122 				],
       
   123 
       
   124 				italic: [
       
   125 					{inline: 'em', remove: 'all'},
       
   126 					{inline: 'span', styles: {fontStyle: 'italic'}},
       
   127 					{inline: 'i', remove: 'all'}
       
   128 				],
       
   129 
       
   130 				underline: [
       
   131 					{inline: 'span', styles: {textDecoration: 'underline'}, exact: true},
       
   132 					{inline: 'u', remove: 'all'}
       
   133 				],
       
   134 
       
   135 				strikethrough: [
       
   136 					{inline: 'span', styles: {textDecoration: 'line-through'}, exact: true},
       
   137 					{inline: 'strike', remove: 'all'}
       
   138 				],
       
   139 
       
   140 				forecolor: {inline: 'span', styles: {color: '%value'}, links: true, remove_similar: true},
       
   141 				hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, links: true, remove_similar: true},
       
   142 				fontname: {inline: 'span', styles: {fontFamily: '%value'}},
       
   143 				fontsize: {inline: 'span', styles: {fontSize: '%value'}},
       
   144 				fontsize_class: {inline: 'span', attributes: {'class': '%value'}},
       
   145 				blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'},
       
   146 				subscript: {inline: 'sub'},
       
   147 				superscript: {inline: 'sup'},
       
   148 				code: {inline: 'code'},
       
   149 
       
   150 				link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true,
       
   151 					onmatch: function() {
       
   152 						return true;
       
   153 					},
       
   154 
       
   155 					onformat: function(elm, fmt, vars) {
       
   156 						each(vars, function(value, key) {
       
   157 							dom.setAttrib(elm, key, value);
       
   158 						});
       
   159 					}
       
   160 				},
       
   161 
       
   162 				removeformat: [
       
   163 					{
       
   164 						selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins',
       
   165 						remove: 'all',
       
   166 						split: true,
       
   167 						expand: false,
       
   168 						block_expand: true,
       
   169 						deep: true
       
   170 					},
       
   171 					{selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true},
       
   172 					{selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true}
       
   173 				]
       
   174 			});
       
   175 
       
   176 			// Register default block formats
       
   177 			each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) {
       
   178 				register(name, {block: name, remove: 'all'});
       
   179 			});
       
   180 
       
   181 			// Register user defined formats
       
   182 			register(ed.settings.formats);
       
   183 		}
       
   184 
       
   185 		function addKeyboardShortcuts() {
       
   186 			// Add some inline shortcuts
       
   187 			ed.addShortcut('meta+b', 'bold_desc', 'Bold');
       
   188 			ed.addShortcut('meta+i', 'italic_desc', 'Italic');
       
   189 			ed.addShortcut('meta+u', 'underline_desc', 'Underline');
       
   190 
       
   191 			// BlockFormat shortcuts keys
       
   192 			for (var i = 1; i <= 6; i++) {
       
   193 				ed.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]);
       
   194 			}
       
   195 
       
   196 			ed.addShortcut('access+7', '', ['FormatBlock', false, 'p']);
       
   197 			ed.addShortcut('access+8', '', ['FormatBlock', false, 'div']);
       
   198 			ed.addShortcut('access+9', '', ['FormatBlock', false, 'address']);
       
   199 		}
       
   200 
       
   201 		// Public functions
       
   202 
       
   203 		/**
       
   204 		 * Returns the format by name or all formats if no name is specified.
       
   205 		 *
       
   206 		 * @method get
       
   207 		 * @param {String} name Optional name to retrive by.
       
   208 		 * @return {Array/Object} Array/Object with all registred formats or a specific format.
       
   209 		 */
       
   210 		function get(name) {
       
   211 			return name ? formats[name] : formats;
       
   212 		}
       
   213 
       
   214 		/**
       
   215 		 * Registers a specific format by name.
       
   216 		 *
       
   217 		 * @method register
       
   218 		 * @param {Object/String} name Name of the format for example "bold".
       
   219 		 * @param {Object/Array} format Optional format object or array of format variants
       
   220 		 * can only be omitted if the first arg is an object.
       
   221 		 */
       
   222 		function register(name, format) {
       
   223 			if (name) {
       
   224 				if (typeof name !== 'string') {
       
   225 					each(name, function(format, name) {
       
   226 						register(name, format);
       
   227 					});
       
   228 				} else {
       
   229 					// Force format into array and add it to internal collection
       
   230 					format = format.length ? format : [format];
       
   231 
       
   232 					each(format, function(format) {
       
   233 						// Set deep to false by default on selector formats this to avoid removing
       
   234 						// alignment on images inside paragraphs when alignment is changed on paragraphs
       
   235 						if (format.deep === undef) {
       
   236 							format.deep = !format.selector;
       
   237 						}
       
   238 
       
   239 						// Default to true
       
   240 						if (format.split === undef) {
       
   241 							format.split = !format.selector || format.inline;
       
   242 						}
       
   243 
       
   244 						// Default to true
       
   245 						if (format.remove === undef && format.selector && !format.inline) {
       
   246 							format.remove = 'none';
       
   247 						}
       
   248 
       
   249 						// Mark format as a mixed format inline + block level
       
   250 						if (format.selector && format.inline) {
       
   251 							format.mixed = true;
       
   252 							format.block_expand = true;
       
   253 						}
       
   254 
       
   255 						// Split classes if needed
       
   256 						if (typeof format.classes === 'string') {
       
   257 							format.classes = format.classes.split(/\s+/);
       
   258 						}
       
   259 					});
       
   260 
       
   261 					formats[name] = format;
       
   262 				}
       
   263 			}
       
   264 		}
       
   265 
       
   266 		/**
       
   267 		 * Unregister a specific format by name.
       
   268 		 *
       
   269 		 * @method unregister
       
   270 		 * @param {String} name Name of the format for example "bold".
       
   271 		 */
       
   272 		function unregister(name) {
       
   273 			if (name && formats[name]) {
       
   274 				delete formats[name];
       
   275 			}
       
   276 
       
   277 			return formats;
       
   278 		}
       
   279 
       
   280 		function getTextDecoration(node) {
       
   281 			var decoration;
       
   282 
       
   283 			ed.dom.getParent(node, function(n) {
       
   284 				decoration = ed.dom.getStyle(n, 'text-decoration');
       
   285 				return decoration && decoration !== 'none';
       
   286 			});
       
   287 
       
   288 			return decoration;
       
   289 		}
       
   290 
       
   291 		function processUnderlineAndColor(node) {
       
   292 			var textDecoration;
       
   293 			if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
       
   294 				textDecoration = getTextDecoration(node.parentNode);
       
   295 				if (ed.dom.getStyle(node, 'color') && textDecoration) {
       
   296 					ed.dom.setStyle(node, 'text-decoration', textDecoration);
       
   297 				} else if (ed.dom.getStyle(node, 'text-decoration') === textDecoration) {
       
   298 					ed.dom.setStyle(node, 'text-decoration', null);
       
   299 				}
       
   300 			}
       
   301 		}
       
   302 
       
   303 		/**
       
   304 		 * Applies the specified format to the current selection or specified node.
       
   305 		 *
       
   306 		 * @method apply
       
   307 		 * @param {String} name Name of format to apply.
       
   308 		 * @param {Object} vars Optional list of variables to replace within format before applying it.
       
   309 		 * @param {Node} node Optional node to apply the format to defaults to current selection.
       
   310 		 */
       
   311 		function apply(name, vars, node) {
       
   312 			var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed();
       
   313 
       
   314 			function setElementFormat(elm, fmt) {
       
   315 				fmt = fmt || format;
       
   316 
       
   317 				if (elm) {
       
   318 					if (fmt.onformat) {
       
   319 						fmt.onformat(elm, fmt, vars, node);
       
   320 					}
       
   321 
       
   322 					each(fmt.styles, function(value, name) {
       
   323 						dom.setStyle(elm, name, replaceVars(value, vars));
       
   324 					});
       
   325 
       
   326 					// Needed for the WebKit span spam bug
       
   327 					// TODO: Remove this once WebKit/Blink fixes this
       
   328 					if (fmt.styles) {
       
   329 						var styleVal = dom.getAttrib(elm, 'style');
       
   330 
       
   331 						if (styleVal) {
       
   332 							elm.setAttribute('data-mce-style', styleVal);
       
   333 						}
       
   334 					}
       
   335 
       
   336 					each(fmt.attributes, function(value, name) {
       
   337 						dom.setAttrib(elm, name, replaceVars(value, vars));
       
   338 					});
       
   339 
       
   340 					each(fmt.classes, function(value) {
       
   341 						value = replaceVars(value, vars);
       
   342 
       
   343 						if (!dom.hasClass(elm, value)) {
       
   344 							dom.addClass(elm, value);
       
   345 						}
       
   346 					});
       
   347 				}
       
   348 			}
       
   349 
       
   350 			function adjustSelectionToVisibleSelection() {
       
   351 				function findSelectionEnd(start, end) {
       
   352 					var walker = new TreeWalker(end);
       
   353 					for (node = walker.current(); node; node = walker.prev()) {
       
   354 						if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') {
       
   355 							return node;
       
   356 						}
       
   357 					}
       
   358 				}
       
   359 
       
   360 				// Adjust selection so that a end container with a end offset of zero is not included in the selection
       
   361 				// as this isn't visible to the user.
       
   362 				var rng = ed.selection.getRng();
       
   363 				var start = rng.startContainer;
       
   364 				var end = rng.endContainer;
       
   365 
       
   366 				if (start != end && rng.endOffset === 0) {
       
   367 					var newEnd = findSelectionEnd(start, end);
       
   368 					var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length;
       
   369 
       
   370 					rng.setEnd(newEnd, endOffset);
       
   371 				}
       
   372 
       
   373 				return rng;
       
   374 			}
       
   375 
       
   376 			function applyRngStyle(rng, bookmark, node_specific) {
       
   377 				var newWrappers = [], wrapName, wrapElm, contentEditable = true;
       
   378 
       
   379 				// Setup wrapper element
       
   380 				wrapName = format.inline || format.block;
       
   381 				wrapElm = dom.create(wrapName);
       
   382 				setElementFormat(wrapElm);
       
   383 
       
   384 				rangeUtils.walk(rng, function(nodes) {
       
   385 					var currentWrapElm;
       
   386 
       
   387 					/**
       
   388 					 * Process a list of nodes wrap them.
       
   389 					 */
       
   390 					function process(node) {
       
   391 						var nodeName, parentName, found, hasContentEditableState, lastContentEditable;
       
   392 
       
   393 						lastContentEditable = contentEditable;
       
   394 						nodeName = node.nodeName.toLowerCase();
       
   395 						parentName = node.parentNode.nodeName.toLowerCase();
       
   396 
       
   397 						// Node has a contentEditable value
       
   398 						if (node.nodeType === 1 && getContentEditable(node)) {
       
   399 							lastContentEditable = contentEditable;
       
   400 							contentEditable = getContentEditable(node) === "true";
       
   401 							hasContentEditableState = true; // We don't want to wrap the container only it's children
       
   402 						}
       
   403 
       
   404 						// Stop wrapping on br elements
       
   405 						if (isEq(nodeName, 'br')) {
       
   406 							currentWrapElm = 0;
       
   407 
       
   408 							// Remove any br elements when we wrap things
       
   409 							if (format.block) {
       
   410 								dom.remove(node);
       
   411 							}
       
   412 
       
   413 							return;
       
   414 						}
       
   415 
       
   416 						// If node is wrapper type
       
   417 						if (format.wrapper && matchNode(node, name, vars)) {
       
   418 							currentWrapElm = 0;
       
   419 							return;
       
   420 						}
       
   421 
       
   422 						// Can we rename the block
       
   423 						// TODO: Break this if up, too complex
       
   424 						if (contentEditable && !hasContentEditableState && format.block &&
       
   425 							!format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) {
       
   426 							node = dom.rename(node, wrapName);
       
   427 							setElementFormat(node);
       
   428 							newWrappers.push(node);
       
   429 							currentWrapElm = 0;
       
   430 							return;
       
   431 						}
       
   432 
       
   433 						// Handle selector patterns
       
   434 						if (format.selector) {
       
   435 							// Look for matching formats
       
   436 							each(formatList, function(format) {
       
   437 								// Check collapsed state if it exists
       
   438 								if ('collapsed' in format && format.collapsed !== isCollapsed) {
       
   439 									return;
       
   440 								}
       
   441 
       
   442 								if (dom.is(node, format.selector) && !isCaretNode(node)) {
       
   443 									setElementFormat(node, format);
       
   444 									found = true;
       
   445 								}
       
   446 							});
       
   447 
       
   448 							// Continue processing if a selector match wasn't found and a inline element is defined
       
   449 							if (!format.inline || found) {
       
   450 								currentWrapElm = 0;
       
   451 								return;
       
   452 							}
       
   453 						}
       
   454 
       
   455 						// Is it valid to wrap this item
       
   456 						// TODO: Break this if up, too complex
       
   457 						if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
       
   458 								!(!node_specific && node.nodeType === 3 &&
       
   459 								node.nodeValue.length === 1 &&
       
   460 								node.nodeValue.charCodeAt(0) === 65279) &&
       
   461 								!isCaretNode(node) &&
       
   462 								(!format.inline || !isBlock(node))) {
       
   463 							// Start wrapping
       
   464 							if (!currentWrapElm) {
       
   465 								// Wrap the node
       
   466 								currentWrapElm = dom.clone(wrapElm, FALSE);
       
   467 								node.parentNode.insertBefore(currentWrapElm, node);
       
   468 								newWrappers.push(currentWrapElm);
       
   469 							}
       
   470 
       
   471 							currentWrapElm.appendChild(node);
       
   472 						} else {
       
   473 							// Start a new wrapper for possible children
       
   474 							currentWrapElm = 0;
       
   475 
       
   476 							each(grep(node.childNodes), process);
       
   477 
       
   478 							if (hasContentEditableState) {
       
   479 								contentEditable = lastContentEditable; // Restore last contentEditable state from stack
       
   480 							}
       
   481 
       
   482 							// End the last wrapper
       
   483 							currentWrapElm = 0;
       
   484 						}
       
   485 					}
       
   486 
       
   487 					// Process siblings from range
       
   488 					each(nodes, process);
       
   489 				});
       
   490 
       
   491 				// Apply formats to links as well to get the color of the underline to change as well
       
   492 				if (format.links === true) {
       
   493 					each(newWrappers, function(node) {
       
   494 						function process(node) {
       
   495 							if (node.nodeName === 'A') {
       
   496 								setElementFormat(node, format);
       
   497 							}
       
   498 
       
   499 							each(grep(node.childNodes), process);
       
   500 						}
       
   501 
       
   502 						process(node);
       
   503 					});
       
   504 				}
       
   505 
       
   506 				// Cleanup
       
   507 				each(newWrappers, function(node) {
       
   508 					var childCount;
       
   509 
       
   510 					function getChildCount(node) {
       
   511 						var count = 0;
       
   512 
       
   513 						each(node.childNodes, function(node) {
       
   514 							if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) {
       
   515 								count++;
       
   516 							}
       
   517 						});
       
   518 
       
   519 						return count;
       
   520 					}
       
   521 
       
   522 					function mergeStyles(node) {
       
   523 						var child, clone;
       
   524 
       
   525 						each(node.childNodes, function(node) {
       
   526 							if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
       
   527 								child = node;
       
   528 								return FALSE; // break loop
       
   529 							}
       
   530 						});
       
   531 
       
   532 						// If child was found and of the same type as the current node
       
   533 						if (child && !isBookmarkNode(child) && matchName(child, format)) {
       
   534 							clone = dom.clone(child, FALSE);
       
   535 							setElementFormat(clone);
       
   536 
       
   537 							dom.replace(clone, node, TRUE);
       
   538 							dom.remove(child, 1);
       
   539 						}
       
   540 
       
   541 						return clone || node;
       
   542 					}
       
   543 
       
   544 					childCount = getChildCount(node);
       
   545 
       
   546 					// Remove empty nodes but only if there is multiple wrappers and they are not block
       
   547 					// elements so never remove single <h1></h1> since that would remove the
       
   548 					// currrent empty block element where the caret is at
       
   549 					if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
       
   550 						dom.remove(node, 1);
       
   551 						return;
       
   552 					}
       
   553 
       
   554 					if (format.inline || format.wrapper) {
       
   555 						// Merges the current node with it's children of similar type to reduce the number of elements
       
   556 						if (!format.exact && childCount === 1) {
       
   557 							node = mergeStyles(node);
       
   558 						}
       
   559 
       
   560 						// Remove/merge children
       
   561 						each(formatList, function(format) {
       
   562 							// Merge all children of similar type will move styles from child to parent
       
   563 							// this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
       
   564 							// will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
       
   565 							each(dom.select(format.inline, node), function(child) {
       
   566 								if (isBookmarkNode(child)) {
       
   567 									return;
       
   568 								}
       
   569 
       
   570 								removeFormat(format, vars, child, format.exact ? child : null);
       
   571 							});
       
   572 						});
       
   573 
       
   574 						// Remove child if direct parent is of same type
       
   575 						if (matchNode(node.parentNode, name, vars)) {
       
   576 							dom.remove(node, 1);
       
   577 							node = 0;
       
   578 							return TRUE;
       
   579 						}
       
   580 
       
   581 						// Look for parent with similar style format
       
   582 						if (format.merge_with_parents) {
       
   583 							dom.getParent(node.parentNode, function(parent) {
       
   584 								if (matchNode(parent, name, vars)) {
       
   585 									dom.remove(node, 1);
       
   586 									node = 0;
       
   587 									return TRUE;
       
   588 								}
       
   589 							});
       
   590 						}
       
   591 
       
   592 						// Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
       
   593 						if (node && format.merge_siblings !== false) {
       
   594 							node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
       
   595 							node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
       
   596 						}
       
   597 					}
       
   598 				});
       
   599 			}
       
   600 
       
   601 			if (format) {
       
   602 				if (node) {
       
   603 					if (node.nodeType) {
       
   604 						rng = dom.createRng();
       
   605 						rng.setStartBefore(node);
       
   606 						rng.setEndAfter(node);
       
   607 						applyRngStyle(expandRng(rng, formatList), null, true);
       
   608 					} else {
       
   609 						applyRngStyle(node, null, true);
       
   610 					}
       
   611 				} else {
       
   612 					if (!isCollapsed || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) {
       
   613 						// Obtain selection node before selection is unselected by applyRngStyle()
       
   614 						var curSelNode = ed.selection.getNode();
       
   615 
       
   616 						// If the formats have a default block and we can't find a parent block then
       
   617 						// start wrapping it with a DIV this is for forced_root_blocks: false
       
   618 						// It's kind of a hack but people should be using the default block type P since all desktop editors work that way
       
   619 						if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) {
       
   620 							apply(formatList[0].defaultBlock);
       
   621 						}
       
   622 
       
   623 						// Apply formatting to selection
       
   624 						ed.selection.setRng(adjustSelectionToVisibleSelection());
       
   625 						bookmark = selection.getBookmark();
       
   626 						applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
       
   627 
       
   628 						// Colored nodes should be underlined so that the color of the underline matches the text color.
       
   629 						if (format.styles && (format.styles.color || format.styles.textDecoration)) {
       
   630 							walk(curSelNode, processUnderlineAndColor, 'childNodes');
       
   631 							processUnderlineAndColor(curSelNode);
       
   632 						}
       
   633 
       
   634 						selection.moveToBookmark(bookmark);
       
   635 						moveStart(selection.getRng(TRUE));
       
   636 						ed.nodeChanged();
       
   637 					} else {
       
   638 						performCaretAction('apply', name, vars);
       
   639 					}
       
   640 				}
       
   641 			}
       
   642 		}
       
   643 
       
   644 		/**
       
   645 		 * Removes the specified format from the current selection or specified node.
       
   646 		 *
       
   647 		 * @method remove
       
   648 		 * @param {String} name Name of format to remove.
       
   649 		 * @param {Object} vars Optional list of variables to replace within format before removing it.
       
   650 		 * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection.
       
   651 		 */
       
   652 		function remove(name, vars, node, similar) {
       
   653 			var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true;
       
   654 
       
   655 			// Merges the styles for each node
       
   656 			function process(node) {
       
   657 				var children, i, l, lastContentEditable, hasContentEditableState;
       
   658 
       
   659 				// Node has a contentEditable value
       
   660 				if (node.nodeType === 1 && getContentEditable(node)) {
       
   661 					lastContentEditable = contentEditable;
       
   662 					contentEditable = getContentEditable(node) === "true";
       
   663 					hasContentEditableState = true; // We don't want to wrap the container only it's children
       
   664 				}
       
   665 
       
   666 				// Grab the children first since the nodelist might be changed
       
   667 				children = grep(node.childNodes);
       
   668 
       
   669 				// Process current node
       
   670 				if (contentEditable && !hasContentEditableState) {
       
   671 					for (i = 0, l = formatList.length; i < l; i++) {
       
   672 						if (removeFormat(formatList[i], vars, node, node)) {
       
   673 							break;
       
   674 						}
       
   675 					}
       
   676 				}
       
   677 
       
   678 				// Process the children
       
   679 				if (format.deep) {
       
   680 					if (children.length) {
       
   681 						for (i = 0, l = children.length; i < l; i++) {
       
   682 							process(children[i]);
       
   683 						}
       
   684 
       
   685 						if (hasContentEditableState) {
       
   686 							contentEditable = lastContentEditable; // Restore last contentEditable state from stack
       
   687 						}
       
   688 					}
       
   689 				}
       
   690 			}
       
   691 
       
   692 			function findFormatRoot(container) {
       
   693 				var formatRoot;
       
   694 
       
   695 				// Find format root
       
   696 				each(getParents(container.parentNode).reverse(), function(parent) {
       
   697 					var format;
       
   698 
       
   699 					// Find format root element
       
   700 					if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
       
   701 						// Is the node matching the format we are looking for
       
   702 						format = matchNode(parent, name, vars, similar);
       
   703 						if (format && format.split !== false) {
       
   704 							formatRoot = parent;
       
   705 						}
       
   706 					}
       
   707 				});
       
   708 
       
   709 				return formatRoot;
       
   710 			}
       
   711 
       
   712 			function wrapAndSplit(formatRoot, container, target, split) {
       
   713 				var parent, clone, lastClone, firstClone, i, formatRootParent;
       
   714 
       
   715 				// Format root found then clone formats and split it
       
   716 				if (formatRoot) {
       
   717 					formatRootParent = formatRoot.parentNode;
       
   718 
       
   719 					for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
       
   720 						clone = dom.clone(parent, FALSE);
       
   721 
       
   722 						for (i = 0; i < formatList.length; i++) {
       
   723 							if (removeFormat(formatList[i], vars, clone, clone)) {
       
   724 								clone = 0;
       
   725 								break;
       
   726 							}
       
   727 						}
       
   728 
       
   729 						// Build wrapper node
       
   730 						if (clone) {
       
   731 							if (lastClone) {
       
   732 								clone.appendChild(lastClone);
       
   733 							}
       
   734 
       
   735 							if (!firstClone) {
       
   736 								firstClone = clone;
       
   737 							}
       
   738 
       
   739 							lastClone = clone;
       
   740 						}
       
   741 					}
       
   742 
       
   743 					// Never split block elements if the format is mixed
       
   744 					if (split && (!format.mixed || !isBlock(formatRoot))) {
       
   745 						container = dom.split(formatRoot, container);
       
   746 					}
       
   747 
       
   748 					// Wrap container in cloned formats
       
   749 					if (lastClone) {
       
   750 						target.parentNode.insertBefore(lastClone, target);
       
   751 						firstClone.appendChild(target);
       
   752 					}
       
   753 				}
       
   754 
       
   755 				return container;
       
   756 			}
       
   757 
       
   758 			function splitToFormatRoot(container) {
       
   759 				return wrapAndSplit(findFormatRoot(container), container, container, true);
       
   760 			}
       
   761 
       
   762 			function unwrap(start) {
       
   763 				var node = dom.get(start ? '_start' : '_end'),
       
   764 					out = node[start ? 'firstChild' : 'lastChild'];
       
   765 
       
   766 				// If the end is placed within the start the result will be removed
       
   767 				// So this checks if the out node is a bookmark node if it is it
       
   768 				// checks for another more suitable node
       
   769 				if (isBookmarkNode(out)) {
       
   770 					out = out[start ? 'firstChild' : 'lastChild'];
       
   771 				}
       
   772 
       
   773 				// Since dom.remove removes empty text nodes then we need to try to find a better node
       
   774 				if (out.nodeType == 3 && out.data.length === 0) {
       
   775 					out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling;
       
   776 				}
       
   777 
       
   778 				dom.remove(node, true);
       
   779 
       
   780 				return out;
       
   781 			}
       
   782 
       
   783 			function removeRngStyle(rng) {
       
   784 				var startContainer, endContainer;
       
   785 				var commonAncestorContainer = rng.commonAncestorContainer;
       
   786 
       
   787 				rng = expandRng(rng, formatList, TRUE);
       
   788 
       
   789 				if (format.split) {
       
   790 					startContainer = getContainer(rng, TRUE);
       
   791 					endContainer = getContainer(rng);
       
   792 
       
   793 					if (startContainer != endContainer) {
       
   794 						// WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN
       
   795 						// so let's see if we can use the first child instead
       
   796 						// This will happen if you triple click a table cell and use remove formatting
       
   797 						if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) {
       
   798 							if (startContainer.nodeName == "TR") {
       
   799 								startContainer = startContainer.firstChild.firstChild || startContainer;
       
   800 							} else {
       
   801 								startContainer = startContainer.firstChild || startContainer;
       
   802 							}
       
   803 						}
       
   804 
       
   805 						// Try to adjust endContainer as well if cells on the same row were selected - bug #6410
       
   806 						if (commonAncestorContainer &&
       
   807 							/^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) &&
       
   808 							isTableCell(endContainer) && endContainer.firstChild) {
       
   809 							endContainer = endContainer.firstChild || endContainer;
       
   810 						}
       
   811 
       
   812 						if (dom.isChildOf(startContainer, endContainer) && !isTableCell(startContainer) && !isTableCell(endContainer)) {
       
   813 							startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'});
       
   814 							splitToFormatRoot(startContainer);
       
   815 							startContainer = unwrap(TRUE);
       
   816 							return;
       
   817 						} else {
       
   818 							// Wrap start/end nodes in span element since these might be cloned/moved
       
   819 							startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'});
       
   820 							endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'});
       
   821 
       
   822 							// Split start/end
       
   823 							splitToFormatRoot(startContainer);
       
   824 							splitToFormatRoot(endContainer);
       
   825 
       
   826 							// Unwrap start/end to get real elements again
       
   827 							startContainer = unwrap(TRUE);
       
   828 							endContainer = unwrap();
       
   829 						}
       
   830 					} else {
       
   831 						startContainer = endContainer = splitToFormatRoot(startContainer);
       
   832 					}
       
   833 
       
   834 					// Update range positions since they might have changed after the split operations
       
   835 					rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer;
       
   836 					rng.startOffset = nodeIndex(startContainer);
       
   837 					rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer;
       
   838 					rng.endOffset = nodeIndex(endContainer) + 1;
       
   839 				}
       
   840 
       
   841 				// Remove items between start/end
       
   842 				rangeUtils.walk(rng, function(nodes) {
       
   843 					each(nodes, function(node) {
       
   844 						process(node);
       
   845 
       
   846 						// Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
       
   847 						if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' &&
       
   848 							node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
       
   849 							removeFormat({
       
   850 								'deep': false,
       
   851 								'exact': true,
       
   852 								'inline': 'span',
       
   853 								'styles': {
       
   854 									'textDecoration': 'underline'
       
   855 								}
       
   856 							}, null, node);
       
   857 						}
       
   858 					});
       
   859 				});
       
   860 			}
       
   861 
       
   862 			// Handle node
       
   863 			if (node) {
       
   864 				if (node.nodeType) {
       
   865 					rng = dom.createRng();
       
   866 					rng.setStartBefore(node);
       
   867 					rng.setEndAfter(node);
       
   868 					removeRngStyle(rng);
       
   869 				} else {
       
   870 					removeRngStyle(node);
       
   871 				}
       
   872 
       
   873 				return;
       
   874 			}
       
   875 
       
   876 			if (!selection.isCollapsed() || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) {
       
   877 				bookmark = selection.getBookmark();
       
   878 				removeRngStyle(selection.getRng(TRUE));
       
   879 				selection.moveToBookmark(bookmark);
       
   880 
       
   881 				// Check if start element still has formatting then we are at: "<b>text|</b>text"
       
   882 				// and need to move the start into the next text node
       
   883 				if (format.inline && match(name, vars, selection.getStart())) {
       
   884 					moveStart(selection.getRng(true));
       
   885 				}
       
   886 
       
   887 				ed.nodeChanged();
       
   888 			} else {
       
   889 				performCaretAction('remove', name, vars, similar);
       
   890 			}
       
   891 		}
       
   892 
       
   893 		/**
       
   894 		 * Toggles the specified format on/off.
       
   895 		 *
       
   896 		 * @method toggle
       
   897 		 * @param {String} name Name of format to apply/remove.
       
   898 		 * @param {Object} vars Optional list of variables to replace within format before applying/removing it.
       
   899 		 * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
       
   900 		 */
       
   901 		function toggle(name, vars, node) {
       
   902 			var fmt = get(name);
       
   903 
       
   904 			if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) {
       
   905 				remove(name, vars, node);
       
   906 			} else {
       
   907 				apply(name, vars, node);
       
   908 			}
       
   909 		}
       
   910 
       
   911 		/**
       
   912 		 * Return true/false if the specified node has the specified format.
       
   913 		 *
       
   914 		 * @method matchNode
       
   915 		 * @param {Node} node Node to check the format on.
       
   916 		 * @param {String} name Format name to check.
       
   917 		 * @param {Object} vars Optional list of variables to replace before checking it.
       
   918 		 * @param {Boolean} similar Match format that has similar properties.
       
   919 		 * @return {Object} Returns the format object it matches or undefined if it doesn't match.
       
   920 		 */
       
   921 		function matchNode(node, name, vars, similar) {
       
   922 			var formatList = get(name), format, i, classes;
       
   923 
       
   924 			function matchItems(node, format, item_name) {
       
   925 				var key, value, items = format[item_name], i;
       
   926 
       
   927 				// Custom match
       
   928 				if (format.onmatch) {
       
   929 					return format.onmatch(node, format, item_name);
       
   930 				}
       
   931 
       
   932 				// Check all items
       
   933 				if (items) {
       
   934 					// Non indexed object
       
   935 					if (items.length === undef) {
       
   936 						for (key in items) {
       
   937 							if (items.hasOwnProperty(key)) {
       
   938 								if (item_name === 'attributes') {
       
   939 									value = dom.getAttrib(node, key);
       
   940 								} else {
       
   941 									value = getStyle(node, key);
       
   942 								}
       
   943 
       
   944 								if (similar && !value && !format.exact) {
       
   945 									return;
       
   946 								}
       
   947 
       
   948 								if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) {
       
   949 									return;
       
   950 								}
       
   951 							}
       
   952 						}
       
   953 					} else {
       
   954 						// Only one match needed for indexed arrays
       
   955 						for (i = 0; i < items.length; i++) {
       
   956 							if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) {
       
   957 								return format;
       
   958 							}
       
   959 						}
       
   960 					}
       
   961 				}
       
   962 
       
   963 				return format;
       
   964 			}
       
   965 
       
   966 			if (formatList && node) {
       
   967 				// Check each format in list
       
   968 				for (i = 0; i < formatList.length; i++) {
       
   969 					format = formatList[i];
       
   970 
       
   971 					// Name name, attributes, styles and classes
       
   972 					if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
       
   973 						// Match classes
       
   974 						if ((classes = format.classes)) {
       
   975 							for (i = 0; i < classes.length; i++) {
       
   976 								if (!dom.hasClass(node, classes[i])) {
       
   977 									return;
       
   978 								}
       
   979 							}
       
   980 						}
       
   981 
       
   982 						return format;
       
   983 					}
       
   984 				}
       
   985 			}
       
   986 		}
       
   987 
       
   988 		/**
       
   989 		 * Matches the current selection or specified node against the specified format name.
       
   990 		 *
       
   991 		 * @method match
       
   992 		 * @param {String} name Name of format to match.
       
   993 		 * @param {Object} vars Optional list of variables to replace before checking it.
       
   994 		 * @param {Node} node Optional node to check.
       
   995 		 * @return {boolean} true/false if the specified selection/node matches the format.
       
   996 		 */
       
   997 		function match(name, vars, node) {
       
   998 			var startNode;
       
   999 
       
  1000 			function matchParents(node) {
       
  1001 				var root = dom.getRoot();
       
  1002 
       
  1003 				if (node === root) {
       
  1004 					return false;
       
  1005 				}
       
  1006 
       
  1007 				// Find first node with similar format settings
       
  1008 				node = dom.getParent(node, function(node) {
       
  1009 					return node.parentNode === root || !!matchNode(node, name, vars, true);
       
  1010 				});
       
  1011 
       
  1012 				// Do an exact check on the similar format element
       
  1013 				return matchNode(node, name, vars);
       
  1014 			}
       
  1015 
       
  1016 			// Check specified node
       
  1017 			if (node) {
       
  1018 				return matchParents(node);
       
  1019 			}
       
  1020 
       
  1021 			// Check selected node
       
  1022 			node = selection.getNode();
       
  1023 			if (matchParents(node)) {
       
  1024 				return TRUE;
       
  1025 			}
       
  1026 
       
  1027 			// Check start node if it's different
       
  1028 			startNode = selection.getStart();
       
  1029 			if (startNode != node) {
       
  1030 				if (matchParents(startNode)) {
       
  1031 					return TRUE;
       
  1032 				}
       
  1033 			}
       
  1034 
       
  1035 			return FALSE;
       
  1036 		}
       
  1037 
       
  1038 		/**
       
  1039 		 * Matches the current selection against the array of formats and returns a new array with matching formats.
       
  1040 		 *
       
  1041 		 * @method matchAll
       
  1042 		 * @param {Array} names Name of format to match.
       
  1043 		 * @param {Object} vars Optional list of variables to replace before checking it.
       
  1044 		 * @return {Array} Array with matched formats.
       
  1045 		 */
       
  1046 		function matchAll(names, vars) {
       
  1047 			var startElement, matchedFormatNames = [], checkedMap = {};
       
  1048 
       
  1049 			// Check start of selection for formats
       
  1050 			startElement = selection.getStart();
       
  1051 			dom.getParent(startElement, function(node) {
       
  1052 				var i, name;
       
  1053 
       
  1054 				for (i = 0; i < names.length; i++) {
       
  1055 					name = names[i];
       
  1056 
       
  1057 					if (!checkedMap[name] && matchNode(node, name, vars)) {
       
  1058 						checkedMap[name] = true;
       
  1059 						matchedFormatNames.push(name);
       
  1060 					}
       
  1061 				}
       
  1062 			}, dom.getRoot());
       
  1063 
       
  1064 			return matchedFormatNames;
       
  1065 		}
       
  1066 
       
  1067 		/**
       
  1068 		 * Returns true/false if the specified format can be applied to the current selection or not. It
       
  1069 		 * will currently only check the state for selector formats, it returns true on all other format types.
       
  1070 		 *
       
  1071 		 * @method canApply
       
  1072 		 * @param {String} name Name of format to check.
       
  1073 		 * @return {boolean} true/false if the specified format can be applied to the current selection/node.
       
  1074 		 */
       
  1075 		function canApply(name) {
       
  1076 			var formatList = get(name), startNode, parents, i, x, selector;
       
  1077 
       
  1078 			if (formatList) {
       
  1079 				startNode = selection.getStart();
       
  1080 				parents = getParents(startNode);
       
  1081 
       
  1082 				for (x = formatList.length - 1; x >= 0; x--) {
       
  1083 					selector = formatList[x].selector;
       
  1084 
       
  1085 					// Format is not selector based then always return TRUE
       
  1086 					// Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line
       
  1087 					if (!selector || formatList[x].defaultBlock) {
       
  1088 						return TRUE;
       
  1089 					}
       
  1090 
       
  1091 					for (i = parents.length - 1; i >= 0; i--) {
       
  1092 						if (dom.is(parents[i], selector)) {
       
  1093 							return TRUE;
       
  1094 						}
       
  1095 					}
       
  1096 				}
       
  1097 			}
       
  1098 
       
  1099 			return FALSE;
       
  1100 		}
       
  1101 
       
  1102 		/**
       
  1103 		 * Executes the specified callback when the current selection matches the formats or not.
       
  1104 		 *
       
  1105 		 * @method formatChanged
       
  1106 		 * @param {String} formats Comma separated list of formats to check for.
       
  1107 		 * @param {function} callback Callback with state and args when the format is changed/toggled on/off.
       
  1108 		 * @param {Boolean} similar True/false state if the match should handle similar or exact formats.
       
  1109 		 */
       
  1110 		function formatChanged(formats, callback, similar) {
       
  1111 			var currentFormats;
       
  1112 
       
  1113 			// Setup format node change logic
       
  1114 			if (!formatChangeData) {
       
  1115 				formatChangeData = {};
       
  1116 				currentFormats = {};
       
  1117 
       
  1118 				ed.on('NodeChange', function(e) {
       
  1119 					var parents = getParents(e.element), matchedFormats = {};
       
  1120 
       
  1121 					// Ignore bogus nodes like the <a> tag created by moveStart()
       
  1122 					parents = Tools.grep(parents, function(node) {
       
  1123 						return node.nodeType == 1 && !node.getAttribute('data-mce-bogus');
       
  1124 					});
       
  1125 
       
  1126 					// Check for new formats
       
  1127 					each(formatChangeData, function(callbacks, format) {
       
  1128 						each(parents, function(node) {
       
  1129 							if (matchNode(node, format, {}, callbacks.similar)) {
       
  1130 								if (!currentFormats[format]) {
       
  1131 									// Execute callbacks
       
  1132 									each(callbacks, function(callback) {
       
  1133 										callback(true, {node: node, format: format, parents: parents});
       
  1134 									});
       
  1135 
       
  1136 									currentFormats[format] = callbacks;
       
  1137 								}
       
  1138 
       
  1139 								matchedFormats[format] = callbacks;
       
  1140 								return false;
       
  1141 							}
       
  1142 						});
       
  1143 					});
       
  1144 
       
  1145 					// Check if current formats still match
       
  1146 					each(currentFormats, function(callbacks, format) {
       
  1147 						if (!matchedFormats[format]) {
       
  1148 							delete currentFormats[format];
       
  1149 
       
  1150 							each(callbacks, function(callback) {
       
  1151 								callback(false, {node: e.element, format: format, parents: parents});
       
  1152 							});
       
  1153 						}
       
  1154 					});
       
  1155 				});
       
  1156 			}
       
  1157 
       
  1158 			// Add format listeners
       
  1159 			each(formats.split(','), function(format) {
       
  1160 				if (!formatChangeData[format]) {
       
  1161 					formatChangeData[format] = [];
       
  1162 					formatChangeData[format].similar = similar;
       
  1163 				}
       
  1164 
       
  1165 				formatChangeData[format].push(callback);
       
  1166 			});
       
  1167 
       
  1168 			return this;
       
  1169 		}
       
  1170 
       
  1171 		/**
       
  1172 		 * Returns a preview css text for the specified format.
       
  1173 		 *
       
  1174 		 * @method getCssText
       
  1175 		 * @param {String/Object} format Format to generate preview css text for.
       
  1176 		 * @return {String} Css text for the specified format.
       
  1177 		 * @example
       
  1178 		 * var cssText1 = editor.formatter.getCssText('bold');
       
  1179 		 * var cssText2 = editor.formatter.getCssText({inline: 'b'});
       
  1180 		 */
       
  1181 		function getCssText(format) {
       
  1182 			return Preview.getCssText(ed, format);
       
  1183 		}
       
  1184 
       
  1185 		// Expose to public
       
  1186 		extend(this, {
       
  1187 			get: get,
       
  1188 			register: register,
       
  1189 			unregister: unregister,
       
  1190 			apply: apply,
       
  1191 			remove: remove,
       
  1192 			toggle: toggle,
       
  1193 			match: match,
       
  1194 			matchAll: matchAll,
       
  1195 			matchNode: matchNode,
       
  1196 			canApply: canApply,
       
  1197 			formatChanged: formatChanged,
       
  1198 			getCssText: getCssText
       
  1199 		});
       
  1200 
       
  1201 		// Initialize
       
  1202 		defaultFormats();
       
  1203 		addKeyboardShortcuts();
       
  1204 		ed.on('BeforeGetContent', function(e) {
       
  1205 			if (markCaretContainersBogus && e.format != 'raw') {
       
  1206 				markCaretContainersBogus();
       
  1207 			}
       
  1208 		});
       
  1209 		ed.on('mouseup keydown', function(e) {
       
  1210 			if (disableCaretContainer) {
       
  1211 				disableCaretContainer(e);
       
  1212 			}
       
  1213 		});
       
  1214 
       
  1215 		// Private functions
       
  1216 
       
  1217 		/**
       
  1218 		 * Checks if the specified nodes name matches the format inline/block or selector.
       
  1219 		 *
       
  1220 		 * @private
       
  1221 		 * @param {Node} node Node to match against the specified format.
       
  1222 		 * @param {Object} format Format object o match with.
       
  1223 		 * @return {boolean} true/false if the format matches.
       
  1224 		 */
       
  1225 		function matchName(node, format) {
       
  1226 			// Check for inline match
       
  1227 			if (isEq(node, format.inline)) {
       
  1228 				return TRUE;
       
  1229 			}
       
  1230 
       
  1231 			// Check for block match
       
  1232 			if (isEq(node, format.block)) {
       
  1233 				return TRUE;
       
  1234 			}
       
  1235 
       
  1236 			// Check for selector match
       
  1237 			if (format.selector) {
       
  1238 				return node.nodeType == 1 && dom.is(node, format.selector);
       
  1239 			}
       
  1240 		}
       
  1241 
       
  1242 		/**
       
  1243 		 * Compares two string/nodes regardless of their case.
       
  1244 		 *
       
  1245 		 * @private
       
  1246 		 * @param {String/Node} Node or string to compare.
       
  1247 		 * @param {String/Node} Node or string to compare.
       
  1248 		 * @return {boolean} True/false if they match.
       
  1249 		 */
       
  1250 		function isEq(str1, str2) {
       
  1251 			str1 = str1 || '';
       
  1252 			str2 = str2 || '';
       
  1253 
       
  1254 			str1 = '' + (str1.nodeName || str1);
       
  1255 			str2 = '' + (str2.nodeName || str2);
       
  1256 
       
  1257 			return str1.toLowerCase() == str2.toLowerCase();
       
  1258 		}
       
  1259 
       
  1260 		/**
       
  1261 		 * Returns the style by name on the specified node. This method modifies the style
       
  1262 		 * contents to make it more easy to match. This will resolve a few browser issues.
       
  1263 		 *
       
  1264 		 * @private
       
  1265 		 * @param {Node} node to get style from.
       
  1266 		 * @param {String} name Style name to get.
       
  1267 		 * @return {String} Style item value.
       
  1268 		 */
       
  1269 		function getStyle(node, name) {
       
  1270 			return normalizeStyleValue(dom.getStyle(node, name), name);
       
  1271 		}
       
  1272 
       
  1273 		/**
       
  1274 		 * Normalize style value by name. This method modifies the style contents
       
  1275 		 * to make it more easy to match. This will resolve a few browser issues.
       
  1276 		 *
       
  1277 		 * @private
       
  1278 		 * @param {Node} node to get style from.
       
  1279 		 * @param {String} name Style name to get.
       
  1280 		 * @return {String} Style item value.
       
  1281 		 */
       
  1282 		function normalizeStyleValue(value, name) {
       
  1283 			// Force the format to hex
       
  1284 			if (name == 'color' || name == 'backgroundColor') {
       
  1285 				value = dom.toHex(value);
       
  1286 			}
       
  1287 
       
  1288 			// Opera will return bold as 700
       
  1289 			if (name == 'fontWeight' && value == 700) {
       
  1290 				value = 'bold';
       
  1291 			}
       
  1292 
       
  1293 			// Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font"
       
  1294 			if (name == 'fontFamily') {
       
  1295 				value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ',');
       
  1296 			}
       
  1297 
       
  1298 			return '' + value;
       
  1299 		}
       
  1300 
       
  1301 		/**
       
  1302 		 * Replaces variables in the value. The variable format is %var.
       
  1303 		 *
       
  1304 		 * @private
       
  1305 		 * @param {String} value Value to replace variables in.
       
  1306 		 * @param {Object} vars Name/value array with variables to replace.
       
  1307 		 * @return {String} New value with replaced variables.
       
  1308 		 */
       
  1309 		function replaceVars(value, vars) {
       
  1310 			if (typeof value != "string") {
       
  1311 				value = value(vars);
       
  1312 			} else if (vars) {
       
  1313 				value = value.replace(/%(\w+)/g, function(str, name) {
       
  1314 					return vars[name] || str;
       
  1315 				});
       
  1316 			}
       
  1317 
       
  1318 			return value;
       
  1319 		}
       
  1320 
       
  1321 		function isWhiteSpaceNode(node) {
       
  1322 			return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
       
  1323 		}
       
  1324 
       
  1325 		function wrap(node, name, attrs) {
       
  1326 			var wrapper = dom.create(name, attrs);
       
  1327 
       
  1328 			node.parentNode.insertBefore(wrapper, node);
       
  1329 			wrapper.appendChild(node);
       
  1330 
       
  1331 			return wrapper;
       
  1332 		}
       
  1333 
       
  1334 		/**
       
  1335 		 * Expands the specified range like object to depending on format.
       
  1336 		 *
       
  1337 		 * For example on block formats it will move the start/end position
       
  1338 		 * to the beginning of the current block.
       
  1339 		 *
       
  1340 		 * @private
       
  1341 		 * @param {Object} rng Range like object.
       
  1342 		 * @param {Array} formats Array with formats to expand by.
       
  1343 		 * @return {Object} Expanded range like object.
       
  1344 		 */
       
  1345 		function expandRng(rng, format, remove) {
       
  1346 			var lastIdx, leaf, endPoint,
       
  1347 				startContainer = rng.startContainer,
       
  1348 				startOffset = rng.startOffset,
       
  1349 				endContainer = rng.endContainer,
       
  1350 				endOffset = rng.endOffset;
       
  1351 
       
  1352 			// This function walks up the tree if there is no siblings before/after the node
       
  1353 			function findParentContainer(start) {
       
  1354 				var container, parent, sibling, siblingName, root;
       
  1355 
       
  1356 				container = parent = start ? startContainer : endContainer;
       
  1357 				siblingName = start ? 'previousSibling' : 'nextSibling';
       
  1358 				root = dom.getRoot();
       
  1359 
       
  1360 				function isBogusBr(node) {
       
  1361 					return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;
       
  1362 				}
       
  1363 
       
  1364 				// If it's a text node and the offset is inside the text
       
  1365 				if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {
       
  1366 					if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
       
  1367 						return container;
       
  1368 					}
       
  1369 				}
       
  1370 
       
  1371 				/*eslint no-constant-condition:0 */
       
  1372 				while (true) {
       
  1373 					// Stop expanding on block elements
       
  1374 					if (!format[0].block_expand && isBlock(parent)) {
       
  1375 						return parent;
       
  1376 					}
       
  1377 
       
  1378 					// Walk left/right
       
  1379 					for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
       
  1380 						if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {
       
  1381 							return parent;
       
  1382 						}
       
  1383 					}
       
  1384 
       
  1385 					// Check if we can move up are we at root level or body level
       
  1386 					if (parent.parentNode == root) {
       
  1387 						container = parent;
       
  1388 						break;
       
  1389 					}
       
  1390 
       
  1391 					parent = parent.parentNode;
       
  1392 				}
       
  1393 
       
  1394 				return container;
       
  1395 			}
       
  1396 
       
  1397 			// This function walks down the tree to find the leaf at the selection.
       
  1398 			// The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
       
  1399 			function findLeaf(node, offset) {
       
  1400 				if (offset === undef) {
       
  1401 					offset = node.nodeType === 3 ? node.length : node.childNodes.length;
       
  1402 				}
       
  1403 
       
  1404 				while (node && node.hasChildNodes()) {
       
  1405 					node = node.childNodes[offset];
       
  1406 					if (node) {
       
  1407 						offset = node.nodeType === 3 ? node.length : node.childNodes.length;
       
  1408 					}
       
  1409 				}
       
  1410 				return {node: node, offset: offset};
       
  1411 			}
       
  1412 
       
  1413 			// If index based start position then resolve it
       
  1414 			if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
       
  1415 				lastIdx = startContainer.childNodes.length - 1;
       
  1416 				startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
       
  1417 
       
  1418 				if (startContainer.nodeType == 3) {
       
  1419 					startOffset = 0;
       
  1420 				}
       
  1421 			}
       
  1422 
       
  1423 			// If index based end position then resolve it
       
  1424 			if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
       
  1425 				lastIdx = endContainer.childNodes.length - 1;
       
  1426 				endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
       
  1427 
       
  1428 				if (endContainer.nodeType == 3) {
       
  1429 					endOffset = endContainer.nodeValue.length;
       
  1430 				}
       
  1431 			}
       
  1432 
       
  1433 			// Expands the node to the closes contentEditable false element if it exists
       
  1434 			function findParentContentEditable(node) {
       
  1435 				var parent = node;
       
  1436 
       
  1437 				while (parent) {
       
  1438 					if (parent.nodeType === 1 && getContentEditable(parent)) {
       
  1439 						return getContentEditable(parent) === "false" ? parent : node;
       
  1440 					}
       
  1441 
       
  1442 					parent = parent.parentNode;
       
  1443 				}
       
  1444 
       
  1445 				return node;
       
  1446 			}
       
  1447 
       
  1448 			function findWordEndPoint(container, offset, start) {
       
  1449 				var walker, node, pos, lastTextNode;
       
  1450 
       
  1451 				function findSpace(node, offset) {
       
  1452 					var pos, pos2, str = node.nodeValue;
       
  1453 
       
  1454 					if (typeof offset == "undefined") {
       
  1455 						offset = start ? str.length : 0;
       
  1456 					}
       
  1457 
       
  1458 					if (start) {
       
  1459 						pos = str.lastIndexOf(' ', offset);
       
  1460 						pos2 = str.lastIndexOf('\u00a0', offset);
       
  1461 						pos = pos > pos2 ? pos : pos2;
       
  1462 
       
  1463 						// Include the space on remove to avoid tag soup
       
  1464 						if (pos !== -1 && !remove) {
       
  1465 							pos++;
       
  1466 						}
       
  1467 					} else {
       
  1468 						pos = str.indexOf(' ', offset);
       
  1469 						pos2 = str.indexOf('\u00a0', offset);
       
  1470 						pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
       
  1471 					}
       
  1472 
       
  1473 					return pos;
       
  1474 				}
       
  1475 
       
  1476 				if (container.nodeType === 3) {
       
  1477 					pos = findSpace(container, offset);
       
  1478 
       
  1479 					if (pos !== -1) {
       
  1480 						return {container: container, offset: pos};
       
  1481 					}
       
  1482 
       
  1483 					lastTextNode = container;
       
  1484 				}
       
  1485 
       
  1486 				// Walk the nodes inside the block
       
  1487 				walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());
       
  1488 				while ((node = walker[start ? 'prev' : 'next']())) {
       
  1489 					if (node.nodeType === 3) {
       
  1490 						lastTextNode = node;
       
  1491 						pos = findSpace(node);
       
  1492 
       
  1493 						if (pos !== -1) {
       
  1494 							return {container: node, offset: pos};
       
  1495 						}
       
  1496 					} else if (isBlock(node)) {
       
  1497 						break;
       
  1498 					}
       
  1499 				}
       
  1500 
       
  1501 				if (lastTextNode) {
       
  1502 					if (start) {
       
  1503 						offset = 0;
       
  1504 					} else {
       
  1505 						offset = lastTextNode.length;
       
  1506 					}
       
  1507 
       
  1508 					return {container: lastTextNode, offset: offset};
       
  1509 				}
       
  1510 			}
       
  1511 
       
  1512 			function findSelectorEndPoint(container, sibling_name) {
       
  1513 				var parents, i, y, curFormat;
       
  1514 
       
  1515 				if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) {
       
  1516 					container = container[sibling_name];
       
  1517 				}
       
  1518 
       
  1519 				parents = getParents(container);
       
  1520 				for (i = 0; i < parents.length; i++) {
       
  1521 					for (y = 0; y < format.length; y++) {
       
  1522 						curFormat = format[y];
       
  1523 
       
  1524 						// If collapsed state is set then skip formats that doesn't match that
       
  1525 						if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) {
       
  1526 							continue;
       
  1527 						}
       
  1528 
       
  1529 						if (dom.is(parents[i], curFormat.selector)) {
       
  1530 							return parents[i];
       
  1531 						}
       
  1532 					}
       
  1533 				}
       
  1534 
       
  1535 				return container;
       
  1536 			}
       
  1537 
       
  1538 			function findBlockEndPoint(container, sibling_name) {
       
  1539 				var node, root = dom.getRoot();
       
  1540 
       
  1541 				// Expand to block of similar type
       
  1542 				if (!format[0].wrapper) {
       
  1543 					node = dom.getParent(container, format[0].block, root);
       
  1544 				}
       
  1545 
       
  1546 				// Expand to first wrappable block element or any block element
       
  1547 				if (!node) {
       
  1548 					node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) {
       
  1549 						// Fixes #6183 where it would expand to editable parent element in inline mode
       
  1550 						return node != root && isTextBlock(node);
       
  1551 					});
       
  1552 				}
       
  1553 
       
  1554 				// Exclude inner lists from wrapping
       
  1555 				if (node && format[0].wrapper) {
       
  1556 					node = getParents(node, 'ul,ol').reverse()[0] || node;
       
  1557 				}
       
  1558 
       
  1559 				// Didn't find a block element look for first/last wrappable element
       
  1560 				if (!node) {
       
  1561 					node = container;
       
  1562 
       
  1563 					while (node[sibling_name] && !isBlock(node[sibling_name])) {
       
  1564 						node = node[sibling_name];
       
  1565 
       
  1566 						// Break on BR but include it will be removed later on
       
  1567 						// we can't remove it now since we need to check if it can be wrapped
       
  1568 						if (isEq(node, 'br')) {
       
  1569 							break;
       
  1570 						}
       
  1571 					}
       
  1572 				}
       
  1573 
       
  1574 				return node || container;
       
  1575 			}
       
  1576 
       
  1577 			// Expand to closest contentEditable element
       
  1578 			startContainer = findParentContentEditable(startContainer);
       
  1579 			endContainer = findParentContentEditable(endContainer);
       
  1580 
       
  1581 			// Exclude bookmark nodes if possible
       
  1582 			if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
       
  1583 				startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
       
  1584 				startContainer = startContainer.nextSibling || startContainer;
       
  1585 
       
  1586 				if (startContainer.nodeType == 3) {
       
  1587 					startOffset = 0;
       
  1588 				}
       
  1589 			}
       
  1590 
       
  1591 			if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
       
  1592 				endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
       
  1593 				endContainer = endContainer.previousSibling || endContainer;
       
  1594 
       
  1595 				if (endContainer.nodeType == 3) {
       
  1596 					endOffset = endContainer.length;
       
  1597 				}
       
  1598 			}
       
  1599 
       
  1600 			if (format[0].inline) {
       
  1601 				if (rng.collapsed) {
       
  1602 					// Expand left to closest word boundary
       
  1603 					endPoint = findWordEndPoint(startContainer, startOffset, true);
       
  1604 					if (endPoint) {
       
  1605 						startContainer = endPoint.container;
       
  1606 						startOffset = endPoint.offset;
       
  1607 					}
       
  1608 
       
  1609 					// Expand right to closest word boundary
       
  1610 					endPoint = findWordEndPoint(endContainer, endOffset);
       
  1611 					if (endPoint) {
       
  1612 						endContainer = endPoint.container;
       
  1613 						endOffset = endPoint.offset;
       
  1614 					}
       
  1615 				}
       
  1616 
       
  1617 				// Avoid applying formatting to a trailing space.
       
  1618 				leaf = findLeaf(endContainer, endOffset);
       
  1619 				if (leaf.node) {
       
  1620 					while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) {
       
  1621 						leaf = findLeaf(leaf.node.previousSibling);
       
  1622 					}
       
  1623 
       
  1624 					if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
       
  1625 							leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
       
  1626 
       
  1627 						if (leaf.offset > 1) {
       
  1628 							endContainer = leaf.node;
       
  1629 							endContainer.splitText(leaf.offset - 1);
       
  1630 						}
       
  1631 					}
       
  1632 				}
       
  1633 			}
       
  1634 
       
  1635 			// Move start/end point up the tree if the leaves are sharp and if we are in different containers
       
  1636 			// Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
       
  1637 			// This will reduce the number of wrapper elements that needs to be created
       
  1638 			// Move start point up the tree
       
  1639 			if (format[0].inline || format[0].block_expand) {
       
  1640 				if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {
       
  1641 					startContainer = findParentContainer(true);
       
  1642 				}
       
  1643 
       
  1644 				if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {
       
  1645 					endContainer = findParentContainer();
       
  1646 				}
       
  1647 			}
       
  1648 
       
  1649 			// Expand start/end container to matching selector
       
  1650 			if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
       
  1651 				// Find new startContainer/endContainer if there is better one
       
  1652 				startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
       
  1653 				endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
       
  1654 			}
       
  1655 
       
  1656 			// Expand start/end container to matching block element or text node
       
  1657 			if (format[0].block || format[0].selector) {
       
  1658 				// Find new startContainer/endContainer if there is better one
       
  1659 				startContainer = findBlockEndPoint(startContainer, 'previousSibling');
       
  1660 				endContainer = findBlockEndPoint(endContainer, 'nextSibling');
       
  1661 
       
  1662 				// Non block element then try to expand up the leaf
       
  1663 				if (format[0].block) {
       
  1664 					if (!isBlock(startContainer)) {
       
  1665 						startContainer = findParentContainer(true);
       
  1666 					}
       
  1667 
       
  1668 					if (!isBlock(endContainer)) {
       
  1669 						endContainer = findParentContainer();
       
  1670 					}
       
  1671 				}
       
  1672 			}
       
  1673 
       
  1674 			// Setup index for startContainer
       
  1675 			if (startContainer.nodeType == 1) {
       
  1676 				startOffset = nodeIndex(startContainer);
       
  1677 				startContainer = startContainer.parentNode;
       
  1678 			}
       
  1679 
       
  1680 			// Setup index for endContainer
       
  1681 			if (endContainer.nodeType == 1) {
       
  1682 				endOffset = nodeIndex(endContainer) + 1;
       
  1683 				endContainer = endContainer.parentNode;
       
  1684 			}
       
  1685 
       
  1686 			// Return new range like object
       
  1687 			return {
       
  1688 				startContainer: startContainer,
       
  1689 				startOffset: startOffset,
       
  1690 				endContainer: endContainer,
       
  1691 				endOffset: endOffset
       
  1692 			};
       
  1693 		}
       
  1694 
       
  1695 		function isColorFormatAndAnchor(node, format) {
       
  1696 			return format.links && node.tagName == 'A';
       
  1697 		}
       
  1698 
       
  1699 		/**
       
  1700 		 * Removes the specified format for the specified node. It will also remove the node if it doesn't have
       
  1701 		 * any attributes if the format specifies it to do so.
       
  1702 		 *
       
  1703 		 * @private
       
  1704 		 * @param {Object} format Format object with items to remove from node.
       
  1705 		 * @param {Object} vars Name/value object with variables to apply to format.
       
  1706 		 * @param {Node} node Node to remove the format styles on.
       
  1707 		 * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node.
       
  1708 		 * @return {Boolean} True/false if the node was removed or not.
       
  1709 		 */
       
  1710 		function removeFormat(format, vars, node, compare_node) {
       
  1711 			var i, attrs, stylesModified;
       
  1712 
       
  1713 			// Check if node matches format
       
  1714 			if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) {
       
  1715 				return FALSE;
       
  1716 			}
       
  1717 
       
  1718 			// Should we compare with format attribs and styles
       
  1719 			if (format.remove != 'all') {
       
  1720 				// Remove styles
       
  1721 				each(format.styles, function(value, name) {
       
  1722 					value = normalizeStyleValue(replaceVars(value, vars), name);
       
  1723 
       
  1724 					// Indexed array
       
  1725 					if (typeof name === 'number') {
       
  1726 						name = value;
       
  1727 						compare_node = 0;
       
  1728 					}
       
  1729 
       
  1730 					if (format.remove_similar || (!compare_node || isEq(getStyle(compare_node, name), value))) {
       
  1731 						dom.setStyle(node, name, '');
       
  1732 					}
       
  1733 
       
  1734 					stylesModified = 1;
       
  1735 				});
       
  1736 
       
  1737 				// Remove style attribute if it's empty
       
  1738 				if (stylesModified && dom.getAttrib(node, 'style') === '') {
       
  1739 					node.removeAttribute('style');
       
  1740 					node.removeAttribute('data-mce-style');
       
  1741 				}
       
  1742 
       
  1743 				// Remove attributes
       
  1744 				each(format.attributes, function(value, name) {
       
  1745 					var valueOut;
       
  1746 
       
  1747 					value = replaceVars(value, vars);
       
  1748 
       
  1749 					// Indexed array
       
  1750 					if (typeof name === 'number') {
       
  1751 						name = value;
       
  1752 						compare_node = 0;
       
  1753 					}
       
  1754 
       
  1755 					if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
       
  1756 						// Keep internal classes
       
  1757 						if (name == 'class') {
       
  1758 							value = dom.getAttrib(node, name);
       
  1759 							if (value) {
       
  1760 								// Build new class value where everything is removed except the internal prefixed classes
       
  1761 								valueOut = '';
       
  1762 								each(value.split(/\s+/), function(cls) {
       
  1763 									if (/mce\-\w+/.test(cls)) {
       
  1764 										valueOut += (valueOut ? ' ' : '') + cls;
       
  1765 									}
       
  1766 								});
       
  1767 
       
  1768 								// We got some internal classes left
       
  1769 								if (valueOut) {
       
  1770 									dom.setAttrib(node, name, valueOut);
       
  1771 									return;
       
  1772 								}
       
  1773 							}
       
  1774 						}
       
  1775 
       
  1776 						// IE6 has a bug where the attribute doesn't get removed correctly
       
  1777 						if (name == "class") {
       
  1778 							node.removeAttribute('className');
       
  1779 						}
       
  1780 
       
  1781 						// Remove mce prefixed attributes
       
  1782 						if (MCE_ATTR_RE.test(name)) {
       
  1783 							node.removeAttribute('data-mce-' + name);
       
  1784 						}
       
  1785 
       
  1786 						node.removeAttribute(name);
       
  1787 					}
       
  1788 				});
       
  1789 
       
  1790 				// Remove classes
       
  1791 				each(format.classes, function(value) {
       
  1792 					value = replaceVars(value, vars);
       
  1793 
       
  1794 					if (!compare_node || dom.hasClass(compare_node, value)) {
       
  1795 						dom.removeClass(node, value);
       
  1796 					}
       
  1797 				});
       
  1798 
       
  1799 				// Check for non internal attributes
       
  1800 				attrs = dom.getAttribs(node);
       
  1801 				for (i = 0; i < attrs.length; i++) {
       
  1802 					if (attrs[i].nodeName.indexOf('_') !== 0) {
       
  1803 						return FALSE;
       
  1804 					}
       
  1805 				}
       
  1806 			}
       
  1807 
       
  1808 			// Remove the inline child if it's empty for example <b> or <span>
       
  1809 			if (format.remove != 'none') {
       
  1810 				removeNode(node, format);
       
  1811 				return TRUE;
       
  1812 			}
       
  1813 		}
       
  1814 
       
  1815 		/**
       
  1816 		 * Removes the node and wrap it's children in paragraphs before doing so or
       
  1817 		 * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
       
  1818 		 *
       
  1819 		 * If the div in the node below gets removed:
       
  1820 		 *  text<div>text</div>text
       
  1821 		 *
       
  1822 		 * Output becomes:
       
  1823 		 *  text<div><br />text<br /></div>text
       
  1824 		 *
       
  1825 		 * So when the div is removed the result is:
       
  1826 		 *  text<br />text<br />text
       
  1827 		 *
       
  1828 		 * @private
       
  1829 		 * @param {Node} node Node to remove + apply BR/P elements to.
       
  1830 		 * @param {Object} format Format rule.
       
  1831 		 * @return {Node} Input node.
       
  1832 		 */
       
  1833 		function removeNode(node, format) {
       
  1834 			var parentNode = node.parentNode, rootBlockElm;
       
  1835 
       
  1836 			function find(node, next, inc) {
       
  1837 				node = getNonWhiteSpaceSibling(node, next, inc);
       
  1838 
       
  1839 				return !node || (node.nodeName == 'BR' || isBlock(node));
       
  1840 			}
       
  1841 
       
  1842 			if (format.block) {
       
  1843 				if (!forcedRootBlock) {
       
  1844 					// Append BR elements if needed before we remove the block
       
  1845 					if (isBlock(node) && !isBlock(parentNode)) {
       
  1846 						if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) {
       
  1847 							node.insertBefore(dom.create('br'), node.firstChild);
       
  1848 						}
       
  1849 
       
  1850 						if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) {
       
  1851 							node.appendChild(dom.create('br'));
       
  1852 						}
       
  1853 					}
       
  1854 				} else {
       
  1855 					// Wrap the block in a forcedRootBlock if we are at the root of document
       
  1856 					if (parentNode == dom.getRoot()) {
       
  1857 						if (!format.list_block || !isEq(node, format.list_block)) {
       
  1858 							each(grep(node.childNodes), function(node) {
       
  1859 								if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
       
  1860 									if (!rootBlockElm) {
       
  1861 										rootBlockElm = wrap(node, forcedRootBlock);
       
  1862 										dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs);
       
  1863 									} else {
       
  1864 										rootBlockElm.appendChild(node);
       
  1865 									}
       
  1866 								} else {
       
  1867 									rootBlockElm = 0;
       
  1868 								}
       
  1869 							});
       
  1870 						}
       
  1871 					}
       
  1872 				}
       
  1873 			}
       
  1874 
       
  1875 			// Never remove nodes that isn't the specified inline element if a selector is specified too
       
  1876 			if (format.selector && format.inline && !isEq(format.inline, node)) {
       
  1877 				return;
       
  1878 			}
       
  1879 
       
  1880 			dom.remove(node, 1);
       
  1881 		}
       
  1882 
       
  1883 		/**
       
  1884 		 * Returns the next/previous non whitespace node.
       
  1885 		 *
       
  1886 		 * @private
       
  1887 		 * @param {Node} node Node to start at.
       
  1888 		 * @param {boolean} next (Optional) Include next or previous node defaults to previous.
       
  1889 		 * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
       
  1890 		 * @return {Node} Next or previous node or undefined if it wasn't found.
       
  1891 		 */
       
  1892 		function getNonWhiteSpaceSibling(node, next, inc) {
       
  1893 			if (node) {
       
  1894 				next = next ? 'nextSibling' : 'previousSibling';
       
  1895 
       
  1896 				for (node = inc ? node : node[next]; node; node = node[next]) {
       
  1897 					if (node.nodeType == 1 || !isWhiteSpaceNode(node)) {
       
  1898 						return node;
       
  1899 					}
       
  1900 				}
       
  1901 			}
       
  1902 		}
       
  1903 
       
  1904 		/**
       
  1905 		 * Merges the next/previous sibling element if they match.
       
  1906 		 *
       
  1907 		 * @private
       
  1908 		 * @param {Node} prev Previous node to compare/merge.
       
  1909 		 * @param {Node} next Next node to compare/merge.
       
  1910 		 * @return {Node} Next node if we didn't merge and prev node if we did.
       
  1911 		 */
       
  1912 		function mergeSiblings(prev, next) {
       
  1913 			var sibling, tmpSibling, elementUtils = new ElementUtils(dom);
       
  1914 
       
  1915 			function findElementSibling(node, sibling_name) {
       
  1916 				for (sibling = node; sibling; sibling = sibling[sibling_name]) {
       
  1917 					if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) {
       
  1918 						return node;
       
  1919 					}
       
  1920 
       
  1921 					if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) {
       
  1922 						return sibling;
       
  1923 					}
       
  1924 				}
       
  1925 
       
  1926 				return node;
       
  1927 			}
       
  1928 
       
  1929 			// Check if next/prev exists and that they are elements
       
  1930 			if (prev && next) {
       
  1931 				// If previous sibling is empty then jump over it
       
  1932 				prev = findElementSibling(prev, 'previousSibling');
       
  1933 				next = findElementSibling(next, 'nextSibling');
       
  1934 
       
  1935 				// Compare next and previous nodes
       
  1936 				if (elementUtils.compare(prev, next)) {
       
  1937 					// Append nodes between
       
  1938 					for (sibling = prev.nextSibling; sibling && sibling != next;) {
       
  1939 						tmpSibling = sibling;
       
  1940 						sibling = sibling.nextSibling;
       
  1941 						prev.appendChild(tmpSibling);
       
  1942 					}
       
  1943 
       
  1944 					// Remove next node
       
  1945 					dom.remove(next);
       
  1946 
       
  1947 					// Move children into prev node
       
  1948 					each(grep(next.childNodes), function(node) {
       
  1949 						prev.appendChild(node);
       
  1950 					});
       
  1951 
       
  1952 					return prev;
       
  1953 				}
       
  1954 			}
       
  1955 
       
  1956 			return next;
       
  1957 		}
       
  1958 
       
  1959 		function getContainer(rng, start) {
       
  1960 			var container, offset, lastIdx;
       
  1961 
       
  1962 			container = rng[start ? 'startContainer' : 'endContainer'];
       
  1963 			offset = rng[start ? 'startOffset' : 'endOffset'];
       
  1964 
       
  1965 			if (container.nodeType == 1) {
       
  1966 				lastIdx = container.childNodes.length - 1;
       
  1967 
       
  1968 				if (!start && offset) {
       
  1969 					offset--;
       
  1970 				}
       
  1971 
       
  1972 				container = container.childNodes[offset > lastIdx ? lastIdx : offset];
       
  1973 			}
       
  1974 
       
  1975 			// If start text node is excluded then walk to the next node
       
  1976 			if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
       
  1977 				container = new TreeWalker(container, ed.getBody()).next() || container;
       
  1978 			}
       
  1979 
       
  1980 			// If end text node is excluded then walk to the previous node
       
  1981 			if (container.nodeType === 3 && !start && offset === 0) {
       
  1982 				container = new TreeWalker(container, ed.getBody()).prev() || container;
       
  1983 			}
       
  1984 
       
  1985 			return container;
       
  1986 		}
       
  1987 
       
  1988 		function performCaretAction(type, name, vars, similar) {
       
  1989 			var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
       
  1990 
       
  1991 			// Creates a caret container bogus element
       
  1992 			function createCaretContainer(fill) {
       
  1993 				var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});
       
  1994 
       
  1995 				if (fill) {
       
  1996 					caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR));
       
  1997 				}
       
  1998 
       
  1999 				return caretContainer;
       
  2000 			}
       
  2001 
       
  2002 			function isCaretContainerEmpty(node, nodes) {
       
  2003 				while (node) {
       
  2004 					if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) {
       
  2005 						return false;
       
  2006 					}
       
  2007 
       
  2008 					// Collect nodes
       
  2009 					if (nodes && node.nodeType === 1) {
       
  2010 						nodes.push(node);
       
  2011 					}
       
  2012 
       
  2013 					node = node.firstChild;
       
  2014 				}
       
  2015 
       
  2016 				return true;
       
  2017 			}
       
  2018 
       
  2019 			// Returns any parent caret container element
       
  2020 			function getParentCaretContainer(node) {
       
  2021 				while (node) {
       
  2022 					if (node.id === caretContainerId) {
       
  2023 						return node;
       
  2024 					}
       
  2025 
       
  2026 					node = node.parentNode;
       
  2027 				}
       
  2028 			}
       
  2029 
       
  2030 			// Finds the first text node in the specified node
       
  2031 			function findFirstTextNode(node) {
       
  2032 				var walker;
       
  2033 
       
  2034 				if (node) {
       
  2035 					walker = new TreeWalker(node, node);
       
  2036 
       
  2037 					for (node = walker.current(); node; node = walker.next()) {
       
  2038 						if (node.nodeType === 3) {
       
  2039 							return node;
       
  2040 						}
       
  2041 					}
       
  2042 				}
       
  2043 			}
       
  2044 
       
  2045 			// Removes the caret container for the specified node or all on the current document
       
  2046 			function removeCaretContainer(node, move_caret) {
       
  2047 				var child, rng;
       
  2048 
       
  2049 				if (!node) {
       
  2050 					node = getParentCaretContainer(selection.getStart());
       
  2051 
       
  2052 					if (!node) {
       
  2053 						while ((node = dom.get(caretContainerId))) {
       
  2054 							removeCaretContainer(node, false);
       
  2055 						}
       
  2056 					}
       
  2057 				} else {
       
  2058 					rng = selection.getRng(true);
       
  2059 
       
  2060 					if (isCaretContainerEmpty(node)) {
       
  2061 						if (move_caret !== false) {
       
  2062 							rng.setStartBefore(node);
       
  2063 							rng.setEndBefore(node);
       
  2064 						}
       
  2065 
       
  2066 						dom.remove(node);
       
  2067 					} else {
       
  2068 						child = findFirstTextNode(node);
       
  2069 
       
  2070 						if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {
       
  2071 							child.deleteData(0, 1);
       
  2072 
       
  2073 							// Fix for bug #6976
       
  2074 							if (rng.startContainer == child && rng.startOffset > 0) {
       
  2075 								rng.setStart(child, rng.startOffset - 1);
       
  2076 							}
       
  2077 
       
  2078 							if (rng.endContainer == child && rng.endOffset > 0) {
       
  2079 								rng.setEnd(child, rng.endOffset - 1);
       
  2080 							}
       
  2081 						}
       
  2082 
       
  2083 						dom.remove(node, 1);
       
  2084 					}
       
  2085 
       
  2086 					selection.setRng(rng);
       
  2087 				}
       
  2088 			}
       
  2089 
       
  2090 			// Applies formatting to the caret postion
       
  2091 			function applyCaretFormat() {
       
  2092 				var rng, caretContainer, textNode, offset, bookmark, container, text;
       
  2093 
       
  2094 				rng = selection.getRng(true);
       
  2095 				offset = rng.startOffset;
       
  2096 				container = rng.startContainer;
       
  2097 				text = container.nodeValue;
       
  2098 
       
  2099 				caretContainer = getParentCaretContainer(selection.getStart());
       
  2100 				if (caretContainer) {
       
  2101 					textNode = findFirstTextNode(caretContainer);
       
  2102 				}
       
  2103 
       
  2104 				// Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character
       
  2105 				if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {
       
  2106 					// Get bookmark of caret position
       
  2107 					bookmark = selection.getBookmark();
       
  2108 
       
  2109 					// Collapse bookmark range (WebKit)
       
  2110 					rng.collapse(true);
       
  2111 
       
  2112 					// Expand the range to the closest word and split it at those points
       
  2113 					rng = expandRng(rng, get(name));
       
  2114 					rng = rangeUtils.split(rng);
       
  2115 
       
  2116 					// Apply the format to the range
       
  2117 					apply(name, vars, rng);
       
  2118 
       
  2119 					// Move selection back to caret position
       
  2120 					selection.moveToBookmark(bookmark);
       
  2121 				} else {
       
  2122 					if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) {
       
  2123 						caretContainer = createCaretContainer(true);
       
  2124 						textNode = caretContainer.firstChild;
       
  2125 
       
  2126 						rng.insertNode(caretContainer);
       
  2127 						offset = 1;
       
  2128 
       
  2129 						apply(name, vars, caretContainer);
       
  2130 					} else {
       
  2131 						apply(name, vars, caretContainer);
       
  2132 					}
       
  2133 
       
  2134 					// Move selection to text node
       
  2135 					selection.setCursorLocation(textNode, offset);
       
  2136 				}
       
  2137 			}
       
  2138 
       
  2139 			function removeCaretFormat() {
       
  2140 				var rng = selection.getRng(true), container, offset, bookmark,
       
  2141 					hasContentAfter, node, formatNode, parents = [], i, caretContainer;
       
  2142 
       
  2143 				container = rng.startContainer;
       
  2144 				offset = rng.startOffset;
       
  2145 				node = container;
       
  2146 
       
  2147 				if (container.nodeType == 3) {
       
  2148 					if (offset != container.nodeValue.length) {
       
  2149 						hasContentAfter = true;
       
  2150 					}
       
  2151 
       
  2152 					node = node.parentNode;
       
  2153 				}
       
  2154 
       
  2155 				while (node) {
       
  2156 					if (matchNode(node, name, vars, similar)) {
       
  2157 						formatNode = node;
       
  2158 						break;
       
  2159 					}
       
  2160 
       
  2161 					if (node.nextSibling) {
       
  2162 						hasContentAfter = true;
       
  2163 					}
       
  2164 
       
  2165 					parents.push(node);
       
  2166 					node = node.parentNode;
       
  2167 				}
       
  2168 
       
  2169 				// Node doesn't have the specified format
       
  2170 				if (!formatNode) {
       
  2171 					return;
       
  2172 				}
       
  2173 
       
  2174 				// Is there contents after the caret then remove the format on the element
       
  2175 				if (hasContentAfter) {
       
  2176 					// Get bookmark of caret position
       
  2177 					bookmark = selection.getBookmark();
       
  2178 
       
  2179 					// Collapse bookmark range (WebKit)
       
  2180 					rng.collapse(true);
       
  2181 
       
  2182 					// Expand the range to the closest word and split it at those points
       
  2183 					rng = expandRng(rng, get(name), true);
       
  2184 					rng = rangeUtils.split(rng);
       
  2185 
       
  2186 					// Remove the format from the range
       
  2187 					remove(name, vars, rng);
       
  2188 
       
  2189 					// Move selection back to caret position
       
  2190 					selection.moveToBookmark(bookmark);
       
  2191 				} else {
       
  2192 					caretContainer = createCaretContainer();
       
  2193 
       
  2194 					node = caretContainer;
       
  2195 					for (i = parents.length - 1; i >= 0; i--) {
       
  2196 						node.appendChild(dom.clone(parents[i], false));
       
  2197 						node = node.firstChild;
       
  2198 					}
       
  2199 
       
  2200 					// Insert invisible character into inner most format element
       
  2201 					node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR));
       
  2202 					node = node.firstChild;
       
  2203 
       
  2204 					var block = dom.getParent(formatNode, isTextBlock);
       
  2205 
       
  2206 					if (block && dom.isEmpty(block)) {
       
  2207 						// Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
       
  2208 						formatNode.parentNode.replaceChild(caretContainer, formatNode);
       
  2209 					} else {
       
  2210 						// Insert caret container after the formated node
       
  2211 						dom.insertAfter(caretContainer, formatNode);
       
  2212 					}
       
  2213 
       
  2214 					// Move selection to text node
       
  2215 					selection.setCursorLocation(node, 1);
       
  2216 
       
  2217 					// If the formatNode is empty, we can remove it safely.
       
  2218 					if (dom.isEmpty(formatNode)) {
       
  2219 						dom.remove(formatNode);
       
  2220 					}
       
  2221 				}
       
  2222 			}
       
  2223 
       
  2224 			// Checks if the parent caret container node isn't empty if that is the case it
       
  2225 			// will remove the bogus state on all children that isn't empty
       
  2226 			function unmarkBogusCaretParents() {
       
  2227 				var caretContainer;
       
  2228 
       
  2229 				caretContainer = getParentCaretContainer(selection.getStart());
       
  2230 				if (caretContainer && !dom.isEmpty(caretContainer)) {
       
  2231 					walk(caretContainer, function(node) {
       
  2232 						if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) {
       
  2233 							dom.setAttrib(node, 'data-mce-bogus', null);
       
  2234 						}
       
  2235 					}, 'childNodes');
       
  2236 				}
       
  2237 			}
       
  2238 
       
  2239 			// Only bind the caret events once
       
  2240 			if (!ed._hasCaretEvents) {
       
  2241 				// Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
       
  2242 				markCaretContainersBogus = function() {
       
  2243 					var nodes = [], i;
       
  2244 
       
  2245 					if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
       
  2246 						// Mark children
       
  2247 						i = nodes.length;
       
  2248 						while (i--) {
       
  2249 							dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
       
  2250 						}
       
  2251 					}
       
  2252 				};
       
  2253 
       
  2254 				disableCaretContainer = function(e) {
       
  2255 					var keyCode = e.keyCode;
       
  2256 
       
  2257 					removeCaretContainer();
       
  2258 
       
  2259 					// Remove caret container on keydown and it's a backspace, enter or left/right arrow keys
       
  2260 					// Backspace key needs to check if the range is collapsed due to bug #6780
       
  2261 					if ((keyCode == 8 && selection.isCollapsed()) || keyCode == 37 || keyCode == 39) {
       
  2262 						removeCaretContainer(getParentCaretContainer(selection.getStart()));
       
  2263 					}
       
  2264 
       
  2265 					unmarkBogusCaretParents();
       
  2266 				};
       
  2267 
       
  2268 				// Remove bogus state if they got filled by contents using editor.selection.setContent
       
  2269 				ed.on('SetContent', function(e) {
       
  2270 					if (e.selection) {
       
  2271 						unmarkBogusCaretParents();
       
  2272 					}
       
  2273 				});
       
  2274 				ed._hasCaretEvents = true;
       
  2275 			}
       
  2276 
       
  2277 			// Do apply or remove caret format
       
  2278 			if (type == "apply") {
       
  2279 				applyCaretFormat();
       
  2280 			} else {
       
  2281 				removeCaretFormat();
       
  2282 			}
       
  2283 		}
       
  2284 
       
  2285 		/**
       
  2286 		 * Moves the start to the first suitable text node.
       
  2287 		 */
       
  2288 		function moveStart(rng) {
       
  2289 			var container = rng.startContainer,
       
  2290 					offset = rng.startOffset, isAtEndOfText,
       
  2291 					walker, node, nodes, tmpNode;
       
  2292 
       
  2293 			// Convert text node into index if possible
       
  2294 			if (container.nodeType == 3 && offset >= container.nodeValue.length) {
       
  2295 				// Get the parent container location and walk from there
       
  2296 				offset = nodeIndex(container);
       
  2297 				container = container.parentNode;
       
  2298 				isAtEndOfText = true;
       
  2299 			}
       
  2300 
       
  2301 			// Move startContainer/startOffset in to a suitable node
       
  2302 			if (container.nodeType == 1) {
       
  2303 				nodes = container.childNodes;
       
  2304 				container = nodes[Math.min(offset, nodes.length - 1)];
       
  2305 				walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
       
  2306 
       
  2307 				// If offset is at end of the parent node walk to the next one
       
  2308 				if (offset > nodes.length - 1 || isAtEndOfText) {
       
  2309 					walker.next();
       
  2310 				}
       
  2311 
       
  2312 				for (node = walker.current(); node; node = walker.next()) {
       
  2313 					if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
       
  2314 						// IE has a "neat" feature where it moves the start node into the closest element
       
  2315 						// we can avoid this by inserting an element before it and then remove it after we set the selection
       
  2316 						tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR);
       
  2317 						node.parentNode.insertBefore(tmpNode, node);
       
  2318 
       
  2319 						// Set selection and remove tmpNode
       
  2320 						rng.setStart(node, 0);
       
  2321 						selection.setRng(rng);
       
  2322 						dom.remove(tmpNode);
       
  2323 
       
  2324 						return;
       
  2325 					}
       
  2326 				}
       
  2327 			}
       
  2328 		}
       
  2329 	};
       
  2330 });