1 /**
  2  * Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API.
  3  *
  4  * RichDom provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API
  5  */
  6 xq.RichDom = xq.Class({
  7 	/**
  8 	 * Initialize RichDom. Target window and root element should be set after initialization. See setWin and setRoot.
  9 	 *
 10      * @constructor
 11 	 */
 12 	initialize: function() {
 13 		xq.addToFinalizeQueue(this);
 14 
 15 		/**
 16 		 * {xq.DomTree} instance of DomTree
 17 		 */
 18 		this.tree = new xq.DomTree();
 19 		
 20 		this._lastMarkerId = 0;
 21 	},
 22 	
 23 	
 24 	
 25 	/**
 26 	 * @param {Window} win Browser's window object
 27 	 */
 28 	setWin: function(win) {
 29 		if(!win) throw "[win] is null";
 30 		this.win = win;
 31 	},
 32 	
 33 	/**
 34 	 * @param {Element} root Root element
 35 	 */
 36 	setRoot: function(root) {
 37 		if(!root) throw "[root] is null";
 38 		if(this.win && (root.ownerDocument != this.win.document)) throw "root.ownerDocument != this.win.document";
 39 		this.root = root;
 40 		this.doc = this.root.ownerDocument;
 41 	},
 42 	
 43 	/**
 44 	 * @returns Browser's window object.
 45 	 */
 46 	getWin: function() {return this.win},
 47 	
 48 	/**
 49 	 * @returns Document object of root element.
 50 	 */
 51 	getDoc: function() {return this.doc},
 52 	
 53 	/**
 54 	 * @returns Root element.
 55 	 */
 56 	getRoot: function() {return this.root},
 57 	
 58 	
 59 	
 60 	/////////////////////////////////////////////
 61 	// CRUDs
 62 	
 63 	clearRoot: function() {
 64 		this.root.innerHTML = "";
 65 		this.root.appendChild(this.makeEmptyParagraph());
 66 	},
 67 	
 68 	/**
 69 	 * Removes place holders and empty text nodes of given element.
 70 	 *
 71 	 * @param {Element} element target element
 72 	 */
 73 	removePlaceHoldersAndEmptyNodes: function(element) {
 74 		var children = element.childNodes;
 75 		if(!children) return;
 76 		var stopAt = this.getBottommostLastChild(element);
 77 		if(!stopAt) return;
 78 		stopAt = this.tree.walkForward(stopAt);
 79 		
 80 		while(true) {
 81 			if(!element || element == stopAt) break;
 82 			
 83 			if(
 84 				this.isPlaceHolder(element) ||
 85 				(element.nodeType == 3 && element.nodeValue == "") ||
 86 				(!this.getNextSibling(element) && element.nodeType == 3 && element.nodeValue.strip() == "")
 87 			) {
 88 				var deleteTarget = element;
 89 				element = this.tree.walkForward(element);
 90 				
 91 				this.deleteNode(deleteTarget);
 92 			} else {
 93 				element = this.tree.walkForward(element);
 94 			}
 95 		}
 96 	},
 97 	
 98 	/**
 99 	 * Sets multiple attributes into element at once
100 	 *
101 	 * @param {Element} element target element
102 	 * @param {Object} map key-value pairs
103 	 */
104 	setAttributes: function(element, map) {
105 		for(var key in map) element.setAttribute(key, map[key]);
106 	},
107 
108 	/**
109 	 * Creates textnode by given node value.
110 	 *
111 	 * @param {String} value value of textnode
112 	 * @returns {Node} Created text node
113 	 */	
114 	createTextNode: function(value) {return this.doc.createTextNode(value);},
115 
116 	/**
117 	 * Creates empty element by given tag name.
118 	 *
119 	 * @param {String} tagName name of tag
120 	 * @returns {Element} Created element
121 	 */	
122 	createElement: function(tagName) {return this.doc.createElement(tagName);},
123 
124 	/**
125 	 * Creates element from HTML string
126 	 * 
127 	 * @param {String} html HTML string
128 	 * @returns {Element} Created element
129 	 */
130 	createElementFromHtml: function(html) {
131 		var node = this.createElement("div");
132 		node.innerHTML = html;
133 		if(node.childNodes.length != 1) {
134 			throw "Illegal HTML fragment";
135 		}
136 		return this.getFirstChild(node);
137 	},
138 	
139 	/**
140 	 * Deletes node from DOM tree.
141 	 *
142 	 * @param {Node} node Target node which should be deleted
143 	 * @param {boolean} deleteEmptyParentsRecursively Recursively delete empty parent elements
144 	 * @param {boolean} correctEmptyParent Call #correctEmptyElement on empty parent element after deletion
145 	 */	
146 	deleteNode: function(node, deleteEmptyParentsRecursively, correctEmptyParent) {
147 		if(!node || !node.parentNode) return;
148 		
149 		var parent = node.parentNode;
150 		parent.removeChild(node);
151 		
152 		if(deleteEmptyParentsRecursively) {
153 			while(!parent.hasChildNodes()) {
154 				node = parent;
155 				parent = node.parentNode;
156 				if(!parent || this.getRoot() == node) break;
157 				parent.removeChild(node);
158 			}
159 		}
160 		
161 		if(correctEmptyParent && this.isEmptyBlock(parent)) {
162 			parent.innerHTML = "";
163 			this.correctEmptyElement(parent);
164 		}
165 	},
166 
167 	/**
168 	 * Inserts given node into current caret position
169 	 *
170 	 * @param {Node} node Target node
171 	 * @returns {Node} Inserted node. It could be different with given node.
172 	 */
173 	insertNode: function(node) {throw "Not implemented"},
174 
175 	/**
176 	 * Inserts given html into current caret position
177 	 *
178 	 * @param {String} html HTML string
179 	 * @returns {Node} Inserted node. It could be different with given node.
180 	 */
181 	insertHtml: function(html) {
182 		return this.insertNode(this.createElementFromHtml(html));
183 	},
184 	
185 	/**
186 	 * Creates textnode from given text and inserts it into current caret position
187 	 *
188 	 * @param {String} text Value of textnode
189 	 * @returns {Node} Inserted node
190 	 */
191 	insertText: function(text) {
192 		this.insertNode(this.createTextNode(text));
193 	},
194 	
195 	/**
196 	 * Places given node nearby target.
197 	 *
198 	 * @param {Node} node Node to be inserted.
199 	 * @param {Node} target Target node.
200 	 * @param {String} where Possible values: "before", "start", "end", "after"
201 	 * @param {boolean} performValidation Validate node if needed. For example when P placed into UL, its tag name automatically replaced with LI
202 	 *
203 	 * @returns {Node} Inserted node. It could be different with given node.
204 	 */
205 	insertNodeAt: function(node, target, where, performValidation) {
206 		if(
207 			["HTML", "HEAD"].indexOf(target.nodeName) != -1 ||
208 			"BODY" == target.nodeName && ["before", "after"].indexOf(where) != -1
209 		) throw "Illegal argument. Cannot move node[" + node.nodeName + "] to '" + where + "' of target[" + target.nodeName + "]"
210 		
211 		var object;
212 		var message;
213 		var secondParam;
214 		
215 		switch(where.toLowerCase()) {
216 			case "before":
217 				object = target.parentNode;
218 				message = 'insertBefore';
219 				secondParam = target;
220 				break
221 			case "start":
222 				if(target.firstChild) {
223 					object = target;
224 					message = 'insertBefore';
225 					secondParam = target.firstChild;
226 				} else {
227 					object = target;
228 					message = 'appendChild';
229 				}
230 				break
231 			case "end":
232 				object = target;
233 				message = 'appendChild';
234 				break
235 			case "after":
236 				if(target.nextSibling) {
237 					object = target.parentNode;
238 					message = 'insertBefore';
239 					secondParam = target.nextSibling;
240 				} else {
241 					object = target.parentNode;
242 					message = 'appendChild';
243 				}
244 				break
245 		}
246 
247 		if(performValidation && this.tree.isListContainer(object) && node.nodeName != "LI") {
248 			var li = this.createElement("LI");
249 			li.appendChild(node);
250 			node = li;
251 			object[message](node, secondParam);		
252 		} else if(performValidation && !this.tree.isListContainer(object) && node.nodeName == "LI") {
253 			this.wrapAllInlineOrTextNodesAs("P", node, true);
254 			var div = this.createElement("DIV");
255 			this.moveChildNodes(node, div);
256 			this.deleteNode(node);
257 			object[message](div, secondParam);
258 			node = this.unwrapElement(div, true);
259 		} else {
260 			object[message](node, secondParam);
261 		}
262 		
263 		return node;
264 	},
265 
266 	/**
267 	 * Creates textnode from given text and places given node nearby target.
268 	 *
269 	 * @param {String} text Text to be inserted.
270 	 * @param {Node} target Target node.
271 	 * @param {String} where Possible values: "before", "start", "end", "after"
272 	 *
273 	 * @returns {Node} Inserted node.
274 	 */
275 	insertTextAt: function(text, target, where) {
276 		return this.insertNodeAt(this.createTextNode(text), target, where);
277 	},
278 
279 	/**
280 	 * Creates element from given HTML string and places given it nearby target.
281 	 *
282 	 * @param {String} html HTML to be inserted.
283 	 * @param {Node} target Target node.
284 	 * @param {String} where Possible values: "before", "start", "end", "after"
285 	 *
286 	 * @returns {Node} Inserted node.
287 	 */
288 	insertHtmlAt: function(html, target, where) {
289 		return this.insertNodeAt(this.createElementFromHtml(html), target, where);
290 	},
291 
292 	/**
293 	 * Replaces element's tag by removing current element and creating new element by given tag name.
294 	 *
295 	 * @param {String} tag New tag name
296 	 * @param {Element} element Target element
297 	 *
298 	 * @returns {Element} Replaced element
299 	 */	
300 	replaceTag: function(tag, element) {
301 		if(element.nodeName == tag) return null;
302 		if(this.tree.isTableCell(element)) return null;
303 		
304 		var newElement = this.createElement(tag);
305 		this.moveChildNodes(element, newElement);
306 		this.copyAttributes(element, newElement, true);
307 		element.parentNode.replaceChild(newElement, element);
308 		
309 		if(!newElement.hasChildNodes()) this.correctEmptyElement(newElement);
310 		
311 		return newElement;
312 	},
313 
314 	/**
315 	 * Unwraps unnecessary paragraph.
316 	 *
317 	 * Unnecessary paragraph is P which is the only child of given container element.
318 	 * For example, P which is contained by LI and is the only child is the unnecessary paragraph.
319 	 * But if given container element is a block-only-container(BLOCKQUOTE, BODY), this method does nothing.
320 	 *
321 	 * @param {Element} element Container element
322 	 * @returns {boolean} True if unwrap performed.
323 	 */
324 	unwrapUnnecessaryParagraph: function(element) {
325 		if(!element) return false;
326 		
327 		if(!this.tree.isBlockOnlyContainer(element) && element.childNodes.length == 1 && element.firstChild.nodeName == "P" && !this.hasImportantAttributes(element.firstChild)) {
328 			var p = element.firstChild;
329 			this.moveChildNodes(p, element);
330 			this.deleteNode(p);
331 			return true;
332 		}
333 		return false;
334 	},
335 	
336 	/**
337 	 * Unwraps element by extracting all children out and removing the element.
338 	 *
339 	 * @param {Element} element Target element
340 	 * @param {boolean} wrapInlineAndTextNodes Wrap all inline and text nodes with P before unwrap
341 	 * @returns {Node} First child of unwrapped element
342 	 */
343 	unwrapElement: function(element, wrapInlineAndTextNodes) {
344 		if(wrapInlineAndTextNodes) this.wrapAllInlineOrTextNodesAs("P", element);
345 		
346 		var nodeToReturn = element.firstChild;
347 		
348 		while(element.firstChild) this.insertNodeAt(element.firstChild, element, "before");
349 		this.deleteNode(element);
350 		
351 		return nodeToReturn;
352 	},
353 	
354 	/**
355 	 * Wraps element by given tag
356 	 *
357 	 * @param {String} tag tag name
358 	 * @param {Element} element target element to wrap
359 	 * @returns {Element} wrapper
360 	 */
361 	wrapElement: function(tag, element) {
362 		var wrapper = this.insertNodeAt(this.createElement(tag), element, "before");
363 		wrapper.appendChild(element);
364 		return wrapper;
365 	},
366 	
367 	/**
368 	 * Tests #smartWrap with given criteria but doesn't change anything
369 	 */
370 	testSmartWrap: function(endElement, criteria) {
371 		return this.smartWrap(endElement, null, criteria, true);
372 	},
373 	
374 	/**
375 	 * Create inline element with given tag name and wraps nodes nearby endElement by given criteria
376 	 *
377 	 * @param {Element} endElement Boundary(end point, exclusive) of wrapper.
378 	 * @param {String} tag Tag name of wrapper.
379 	 * @param {Object} function which returns text index of start boundary.
380 	 * @param {boolean} testOnly just test boundary and do not perform actual wrapping.
381 	 *
382 	 * @returns {Element} wrapper
383 	 */
384 	smartWrap: function(endElement, tag, criteria, testOnly) {
385 		var block = this.getParentBlockElementOf(endElement);
386 
387 		tag = tag || "SPAN";
388 		criteria = criteria || function(text) {return -1};
389 		
390 		// check for empty wrapper
391 		if(!testOnly && (!endElement.previousSibling || this.isEmptyBlock(block))) {
392 			var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before");
393 			return wrapper;
394 		}
395 		
396 		// collect all textnodes
397 		var textNodes = this.tree.collectForward(block, function(node) {return node == endElement}, function(node) {return node.nodeType == 3});
398 		
399 		// find textnode and break-point
400 		var nodeIndex = 0;
401 		var nodeValues = [];
402 		for(var i = 0; i < textNodes.length; i++) {
403 			nodeValues.push(textNodes[i].nodeValue);
404 		}
405 		var textToWrap = nodeValues.join("");
406 		var textIndex = criteria(textToWrap)
407 		var breakPoint = textIndex;
408 		
409 		if(breakPoint == -1) {
410 			breakPoint = 0;
411 		} else {
412 			textToWrap = textToWrap.substring(breakPoint);
413 		}
414 		
415 		for(var i = 0; i < textNodes.length; i++) {
416 			if(breakPoint > nodeValues[i].length) {
417 				breakPoint -= nodeValues[i].length;
418 			} else {
419 				nodeIndex = i;
420 				break;
421 			}
422 		}
423 		
424 		if(testOnly) return {text:textToWrap, textIndex:textIndex, nodeIndex:nodeIndex, breakPoint:breakPoint};
425 		
426 		// break textnode if necessary 
427 		if(breakPoint != 0) {
428 			var splitted = textNodes[nodeIndex].splitText(breakPoint);
429 			nodeIndex++;
430 			textNodes.splice(nodeIndex, 0, splitted);
431 		}
432 		var startElement = textNodes[nodeIndex] || block.firstChild;
433 		
434 		// split inline elements up to parent block if necessary
435 		var family = this.tree.findCommonAncestorAndImmediateChildrenOf(startElement, endElement);
436 		var ca = family.parent;
437 		if(ca) {
438 			if(startElement.parentNode != ca) startElement = this.splitElementUpto(startElement, ca, true);
439 			if(endElement.parentNode != ca) endElement = this.splitElementUpto(endElement, ca, true);
440 			
441 			var prevStart = startElement.previousSibling;
442 			var nextEnd = endElement.nextSibling;
443 			
444 			// remove empty inline elements
445 			if(prevStart && prevStart.nodeType == 1 && this.isEmptyBlock(prevStart)) this.deleteNode(prevStart);
446 			if(nextEnd && nextEnd.nodeType == 1 && this.isEmptyBlock(nextEnd)) this.deleteNode(nextEnd);
447 			
448 			// wrap
449 			var wrapper = this.insertNodeAt(this.createElement(tag), startElement, "before");
450 			while(wrapper.nextSibling != endElement) wrapper.appendChild(wrapper.nextSibling);
451 			return wrapper;
452 		} else {
453 			// wrap
454 			var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before");
455 			return wrapper;
456 		}
457 	},
458 	
459 	/**
460 	 * Wraps all adjust inline elements and text nodes into block element.
461 	 *
462 	 * TODO: empty element should return empty array when it is not forced and (at least) single item array when forced
463 	 *
464 	 * @param {String} tag Tag name of wrapper
465 	 * @param {Element} element Target element
466 	 * @param {boolean} force Force wrapping. If it is set to false, this method do not makes unnecessary wrapper.
467 	 *
468 	 * @returns {Array} Array of wrappers. If nothing performed it returns empty array
469 	 */
470 	wrapAllInlineOrTextNodesAs: function(tag, element, force) {
471 		var wrappers = [];
472 		
473 		if(!force && !this.tree.hasMixedContents(element)) return wrappers;
474 		
475 		var node = element.firstChild;
476 		while(node) {
477 			if(this.tree.isTextOrInlineNode(node)) {
478 				var wrapper = this.wrapInlineOrTextNodesAs(tag, node);
479 				wrappers.push(wrapper);
480 				node = wrapper.nextSibling;
481 			} else {
482 				node = node.nextSibling;
483 			}
484 		}
485 
486 		return wrappers;
487 	},
488 
489 	/**
490 	 * Wraps node and its adjust next siblings into an element
491 	 */
492 	wrapInlineOrTextNodesAs: function(tag, node) {
493 		var wrapper = this.createElement(tag);
494 		var from = node;
495 
496 		from.parentNode.replaceChild(wrapper, from);
497 		wrapper.appendChild(from);
498 
499 		// move nodes into wrapper
500 		while(wrapper.nextSibling && this.tree.isTextOrInlineNode(wrapper.nextSibling)) wrapper.appendChild(wrapper.nextSibling);
501 
502 		return wrapper;
503 	},
504 	
505 	/**
506 	 * Turns block element into list item
507 	 *
508 	 * @param {Element} element Target element
509 	 * @param {String} type One of "UL", "OL", "CODE". "CODE" is same with "OL" but it gives "OL" a class name "code"
510 	 *
511 	 * @return {Element} LI element
512 	 */
513 	turnElementIntoListItem: function(element, type) {
514 		type = type.toUpperCase();
515 		
516 		var container = this.createElement(type == "UL" ? "UL" : "OL");
517 		if(type == "CODE") container.className = "code";
518 		
519 		if(this.tree.isTableCell(element)) {
520 			var p = this.wrapAllInlineOrTextNodesAs("P", element, true)[0];
521 			container = this.insertNodeAt(container, element, "start");
522 			var li = this.insertNodeAt(this.createElement("LI"), container, "start");
523 			li.appendChild(p);
524 		} else {
525 			container = this.insertNodeAt(container, element, "after");
526 			var li = this.insertNodeAt(this.createElement("LI"), container, "start");
527 			li.appendChild(element);
528 		}
529 		
530 		this.unwrapUnnecessaryParagraph(li);
531 		this.mergeAdjustLists(container);
532 		
533 		return li;
534 	},
535 	
536 	/**
537 	 * Extracts given element out from its parent element.
538 	 * 
539 	 * @param {Element} element Target element
540 	 */
541 	extractOutElementFromParent: function(element) {
542 		if(element == this.root || this.root == element.parentNode || !element.offsetParent) return null;
543 		
544 		if(element.nodeName == "LI") {
545 			this.wrapAllInlineOrTextNodesAs("P", element, true);
546 			element = element.firstChild;
547 		}
548 
549 		var container = element.parentNode;
550 		var nodeToReturn = null;
551 		
552 		if(container.nodeName == "LI" && container.parentNode.parentNode.nodeName == "LI") {
553 			// nested list item
554 			if(element.previousSibling) {
555 				this.splitContainerOf(element, true);
556 				this.correctEmptyElement(element);
557 			}
558 			
559 			this.outdentListItem(element);
560 			nodeToReturn = element;
561 		} else if(container.nodeName == "LI") {
562 			// not-nested list item
563 			
564 			if(this.tree.isListContainer(element.nextSibling)) {
565 				// 1. split listContainer
566 				var listContainer = container.parentNode;
567 				this.splitContainerOf(container, true);
568 				this.correctEmptyElement(element);
569 				
570 				// 2. extract out LI's children
571 				nodeToReturn = container.firstChild;
572 				while(container.firstChild) {
573 					this.insertNodeAt(container.firstChild, listContainer, "before");
574 				}
575 				
576 				// 3. remove listContainer and merge adjust lists
577 				var prevContainer = listContainer.previousSibling;
578 				this.deleteNode(listContainer);
579 				if(prevContainer && this.tree.isListContainer(prevContainer)) this.mergeAdjustLists(prevContainer);
580 			} else {
581 				// 1. split LI
582 				this.splitContainerOf(element, true);
583 				this.correctEmptyElement(element);
584 				
585 				// 2. split list container
586 				var listContainer = this.splitContainerOf(container);
587 				
588 				// 3. extract out
589 				this.insertNodeAt(element, listContainer.parentNode, "before");
590 				this.deleteNode(listContainer.parentNode);
591 				
592 				nodeToReturn = element;
593 			}
594 		} else if(this.tree.isTableCell(container) || this.tree.isTableCell(element)) {
595 			// do nothing
596 		} else {
597 			// normal block
598 			this.splitContainerOf(element, true);
599 			this.correctEmptyElement(element);
600 			nodeToReturn = this.insertNodeAt(element, container, "before");
601 			
602 			this.deleteNode(container);
603 		}
604 		
605 		return nodeToReturn;
606 	},
607 	
608 	/**
609 	 * Insert new block above or below given element.
610 	 *
611 	 * @param {Element} block Target block
612 	 * @param {boolean} before Insert new block above(before) target block
613 	 * @param {String} forceTag New block's tag name. If omitted, target block's tag name will be used.
614 	 *
615 	 * @returns {Element} Inserted block
616 	 */
617 	insertNewBlockAround: function(block, before, forceTag) {
618 		var isListItem = block.nodeName == "LI" || block.parentNode.nodeName == "LI";
619 		
620 		this.removeTrailingWhitespace(block);
621 		if(this.isFirstLiWithNestedList(block) && !forceTag && before) {
622 			var li = this.getParentElementOf(block, ["LI"]);
623 			var newBlock = this._insertNewBlockAround(li, before);
624 			return newBlock;
625 		} else if(isListItem && !forceTag) {
626 			var li = this.getParentElementOf(block, ["LI"]);
627 			var newBlock = this._insertNewBlockAround(block, before);
628 			if(li != block) newBlock = this.splitContainerOf(newBlock, false, "prev");
629 			return newBlock;
630 		} else if(this.tree.isBlockContainer(block)) {
631 			this.wrapAllInlineOrTextNodesAs("P", block, true);
632 			return this._insertNewBlockAround(block.firstChild, before, forceTag);
633 		} else {
634 			return this._insertNewBlockAround(block, before, this.tree.isHeading(block) ? "P" : forceTag);
635 		}
636 	},
637 	
638 	/**
639 	 * @private
640 	 *
641 	 * TODO: Rename
642 	 */
643 	_insertNewBlockAround: function(element, before, tagName) {
644 		var newElement = this.createElement(tagName || element.nodeName);
645 		this.copyAttributes(element, newElement, false);
646 		this.correctEmptyElement(newElement);
647 		newElement = this.insertNodeAt(newElement, element, before ? "before" : "after");
648 		return newElement;
649 	},
650 	
651 	/**
652 	 * Wrap or replace element with given tag name.
653 	 *
654 	 * @param {String} tag Tag name
655 	 * @param {Element} element Target element
656 	 *
657 	 * @return {Element} wrapper element or replaced element.
658 	 */
659 	applyTagIntoElement: function(tag, element) {
660 		if(this.tree.isBlockOnlyContainer(tag)) {
661 			return this.wrapBlock(tag, element);
662 		} else if(this.tree.isBlockContainer(element)) {
663 			var wrapper = this.createElement(tag);
664 			this.moveChildNodes(element, wrapper);
665 			return this.insertNodeAt(wrapper, element, "start");
666 		} else {
667 			if(this.tree.isBlockContainer(tag) && this.hasImportantAttributes(element)) {
668 				return this.wrapBlock(tag, element);
669 			} else {
670 				return this.replaceTag(tag, element);
671 			}
672 		}
673 		
674 		throw "IllegalArgumentException - [" + tag + ", " + element + "]";
675 	},
676 	
677 	/**
678 	 * Wrap or replace elements with given tag name.
679 	 *
680 	 * @param {String} tag Tag name
681 	 * @param {Element} from Start boundary (inclusive)
682 	 * @param {Element} to End boundary (inclusive)
683 	 *
684 	 * @returns {Array} Array of wrappers or replaced elements
685 	 */
686 	applyTagIntoElements: function(tagName, from, to) {
687 		var applied = [];
688 		
689 		if(this.tree.isBlockContainer(tagName)) {
690 			var family = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
691 			var node = family.left;
692 			var wrapper = this.insertNodeAt(this.createElement(tagName), node, "before");
693 			
694 			var coveringWholeList =
695 				family.parent.nodeName == "LI" &&
696 				family.parent.parentNode.childNodes.length == 1 &&
697 				!family.left.previousSilbing &&
698 				!family.right.nextSibling;
699 				
700 			if(coveringWholeList) {
701 				var ul = node.parentNode.parentNode;
702 				this.insertNodeAt(wrapper, ul, "before");
703 				wrapper.appendChild(ul);
704 			} else {
705 				while(node != family.right) {
706 					next = node.nextSibling;
707 					wrapper.appendChild(node);
708 					node = next;
709 				}
710 				wrapper.appendChild(family.right);
711 			}
712 			applied.push(wrapper);
713 		} else {
714 			// is normal tagName
715 			var elements = this.getBlockElementsBetween(from, to);
716 			for(var i = 0; i < elements.length; i++) {
717 				if(this.tree.isBlockContainer(elements[i])) {
718 					var wrappers = this.wrapAllInlineOrTextNodesAs(tagName, elements[i], true);
719 					for(var j = 0; j < wrappers.length; j++) {
720 						applied.push(wrappers[j]);
721 					}
722 				} else {
723 					applied.push(this.replaceTag(tagName, elements[i]));
724 				}
725 			}
726 		}
727 		return applied;
728 	},
729 	
730 	/**
731 	 * Moves block up or down
732 	 *
733 	 * @param {Element} block Target block
734 	 * @param {boolean} up Move up if true
735 	 * 
736 	 * @returns {Element} Moved block. It could be different with given block.
737 	 */
738 	moveBlock: function(block, up) {
739 		// if block is table cell or contained by table cell, select its row as mover
740 		block = this.getParentElementOf(block, ["TR"]) || block;
741 		
742 		// if block is only child, select its parent as mover
743 		while(block.nodeName != "TR" && block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
744 			block = block.parentNode;
745 		}
746 		
747 		// find target and where
748 		var target, where;
749 		if (up) {
750 			target = block.previousSibling;
751 			
752 			if(target) {
753 				var singleNodeLi = target.nodeName == 'LI' && ((target.childNodes.length == 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target));
754 				var table = ['TABLE', 'TR'].indexOf(target.nodeName) != -1;
755 
756 				where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "end" : "before";
757 			} else if(block.parentNode != this.getRoot()) {
758 				target = block.parentNode;
759 				where = "before";
760 			}
761 		} else {
762 			target = block.nextSibling;
763 			
764 			if(target) {
765 				var singleNodeLi = target.nodeName == 'LI' && ((target.childNodes.length == 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target));
766 				var table = ['TABLE', 'TR'].indexOf(target.nodeName) != -1;
767 				
768 				where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "start" : "after";
769 			} else if(block.parentNode != this.getRoot()) {
770 				target = block.parentNode;
771 				where = "after";
772 			}
773 		}
774 		
775 		
776 		// no way to go?
777 		if(!target) return null;
778 		if(["TBODY", "THEAD"].indexOf(target.nodeName) != -1) return null;
779 		
780 		// normalize
781 		this.wrapAllInlineOrTextNodesAs("P", target, true);
782 		
783 		// make placeholder if needed
784 		if(this.isFirstLiWithNestedList(block)) {
785 			this.insertNewBlockAround(block, false, "P");
786 		}
787 		
788 		// perform move
789 		var parent = block.parentNode;
790 		var moved = this.insertNodeAt(block, target, where, true);
791 		
792 		// cleanup
793 		if(!parent.hasChildNodes()) this.deleteNode(parent, true);
794 		this.unwrapUnnecessaryParagraph(moved);
795 		this.unwrapUnnecessaryParagraph(target);
796 
797 		// remove placeholder
798 		if(up) {
799 			if(moved.previousSibling && this.isEmptyBlock(moved.previousSibling) && !moved.previousSibling.previousSibling && moved.parentNode.nodeName == "LI" && this.tree.isListContainer(moved.nextSibling)) {
800 				this.deleteNode(moved.previousSibling);
801 			}
802 		} else {
803 			if(moved.nextSibling && this.isEmptyBlock(moved.nextSibling) && !moved.previousSibling && moved.parentNode.nodeName == "LI" && this.tree.isListContainer(moved.nextSibling.nextSibling)) {
804 				this.deleteNode(moved.nextSibling);
805 			}
806 		}
807 		
808 		this.correctEmptyElement(moved);
809 		
810 		return moved;
811 	},
812 	
813 	/**
814 	 * Remove given block
815 	 *
816 	 * @param {Element} block Target block
817 	 * @returns {Element} Nearest block of remove element
818 	 */
819 	removeBlock: function(block) {
820 		var blockToMove;
821 
822 		// if block is only child, select its parent as mover
823 		while(block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
824 			block = block.parentNode;
825 		}
826 		
827 		var finder = function(node) {return this.tree.isBlock(node) && !this.tree.isAtomic(node) && !this.tree.isDescendantOf(block, node) && !this.tree.hasBlocks(node);}.bind(this);
828 		var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this);
829 		
830 		if(this.isFirstLiWithNestedList(block)) {
831 			blockToMove = this.outdentListItem(block.nextSibling.firstChild);
832 			this.deleteNode(blockToMove.previousSibling, true);
833 		} else if(this.tree.isTableCell(block)) {
834 			var rtable = new xq.RichTable(this, this.getParentElementOf(block, ["TABLE"]));
835 			blockToMove = rtable.getBelowCellOf(block);
836 			
837 			// should not delete row when there's thead and the row is the only child of tbody
838 			if(
839 				block.parentNode.parentNode.nodeName == "TBODY" &&
840 				rtable.hasHeadingAtTop() &&
841 				rtable.getDom().tBodies[0].rows.length == 1) return blockToMove;
842 			
843 			blockToMove = blockToMove ||
844 				this.tree.findForward(block, finder, exitCondition) ||
845 				this.tree.findBackward(block, finder, exitCondition);
846 			
847 			this.deleteNode(block.parentNode, true);
848 		} else {
849 			blockToMove = blockToMove ||
850 				this.tree.findForward(block, finder, exitCondition) ||
851 				this.tree.findBackward(block, finder, exitCondition);
852 			
853 			if(!blockToMove) blockToMove = this.insertNodeAt(this.makeEmptyParagraph(), block, "after");
854 			
855 			this.deleteNode(block, true);
856 		}
857 		if(!this.getRoot().hasChildNodes()) {
858 			blockToMove = this.createElement("P");
859 			this.getRoot().appendChild(blockToMove);
860 			this.correctEmptyElement(blockToMove);
861 		}
862 		
863 		return blockToMove;
864 	},
865 	
866 	/**
867 	 * Removes trailing whitespaces of given block
868 	 *
869 	 * @param {Element} block Target block
870 	 */
871 	removeTrailingWhitespace: function(block) {throw "Not implemented"},
872 	
873 	/**
874 	 * Extract given list item out and change its container's tag
875 	 *
876 	 * @param {Element} element LI or P which is a child of LI
877 	 * @param {String} type "OL", "UL", or "CODE"
878 	 *
879 	 * @returns {Element} changed element
880 	 */
881 	changeListTypeTo: function(element, type) {
882 		type = type.toUpperCase();
883 		
884 		var li = this.getParentElementOf(element, ["LI"]);
885 		if(!li) throw "IllegalArgumentException";
886 		
887 		var container = li.parentNode;
888 
889 		this.splitContainerOf(li);
890 		
891 		var newContainer = this.insertNodeAt(this.createElement(type == "UL" ? "UL" : "OL"), container, "before");
892 		if(type == "CODE") newContainer.className = "code";
893 		
894 		this.insertNodeAt(li, newContainer, "start");
895 		this.deleteNode(container);
896 		
897 		this.mergeAdjustLists(newContainer);
898 		
899 		return element;
900 	},
901 	
902 	/**
903 	 * Split container of element into (maxium) three pieces.
904 	 */
905 	splitContainerOf: function(element, preserveElementItself, dir) {
906 		if([element, element.parentNode].indexOf(this.getRoot()) != -1) return element;
907 
908 		var container = element.parentNode;
909 		if(element.previousSibling && (!dir || dir.toLowerCase() == "prev")) {
910 			var prev = this.createElement(container.nodeName);
911 			this.copyAttributes(container, prev);
912 			while(container.firstChild != element) {
913 				prev.appendChild(container.firstChild);
914 			}
915 			this.insertNodeAt(prev, container, "before");
916 			this.unwrapUnnecessaryParagraph(prev);
917 		}
918 		
919 		if(element.nextSibling && (!dir || dir.toLowerCase() == "next")) {
920 			var next = this.createElement(container.nodeName);
921 			this.copyAttributes(container, next);
922 			while(container.lastChild != element) {
923 				this.insertNodeAt(container.lastChild, next, "start");
924 			}
925 			this.insertNodeAt(next, container, "after");
926 			this.unwrapUnnecessaryParagraph(next);
927 		}
928 		
929 		if(!preserveElementItself) element = this.unwrapUnnecessaryParagraph(container) ? container : element;
930 		return element;
931 	},
932 
933 	/**
934 	 * TODO: Add specs
935 	 */
936 	splitParentElement: function(seperator) {
937 		var parent = seperator.parentNode;
938 		if(["HTML", "HEAD", "BODY"].indexOf(parent.nodeName) != -1) throw "Illegal argument. Cannot seperate element[" + parent.nodeName + "]";
939 
940 		var previousSibling = seperator.previousSibling;
941 		var nextSibling = seperator.nextSibling;
942 		
943 		var newElement = this.insertNodeAt(this.createElement(parent.nodeName), parent, "after");
944 		
945 		var next;
946 		while(next = seperator.nextSibling) newElement.appendChild(next);
947 		
948 		this.insertNodeAt(seperator, newElement, "start");
949 		this.copyAttributes(parent, newElement);
950 		
951 		return newElement;
952 	},
953 	
954 	/**
955 	 * TODO: Add specs
956 	 */
957 	splitElementUpto: function(seperator, element, excludeElement) {
958 		while(seperator.previousSibling != element) {
959 			if(excludeElement && seperator.parentNode == element) break;
960 			seperator = this.splitParentElement(seperator);
961 		}
962 		return seperator;
963 	},
964 	
965 	/**
966 	 * Merges two adjust elements
967 	 *
968 	 * @param {Element} element base element
969 	 * @param {boolean} withNext merge base element with next sibling
970 	 * @param {boolean} skip skip merge steps
971 	 */
972 	mergeElement: function(element, withNext, skip) {
973 		this.wrapAllInlineOrTextNodesAs("P", element.parentNode, true);
974 		
975 		// find two block
976 		if(withNext) {
977 			var prev = element;
978 			var next = this.tree.findForward(
979 				element,
980 				function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node != element.parentNode}.bind(this)
981 			);
982 		} else {
983 			var next = element;
984 			var prev = this.tree.findBackward(
985 				element,
986 				function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node != element.parentNode}.bind(this)
987 			);
988 		}
989 		
990 		// normalize next block
991 		if(next && this.tree.isDescendantOf(this.getRoot(), next)) {
992 			var nextContainer = next.parentNode;
993 			if(this.tree.isBlockContainer(next)) {
994 				nextContainer = next;
995 				this.wrapAllInlineOrTextNodesAs("P", nextContainer, true);
996 				next = nextContainer.firstChild;
997 			}
998 		} else {
999 			next = null;
1000 		}
1001 		
1002 		// normalize prev block
1003 		if(prev && this.tree.isDescendantOf(this.getRoot(), prev)) {
1004 			var prevContainer = prev.parentNode;
1005 			if(this.tree.isBlockContainer(prev)) {
1006 				prevContainer = prev;
1007 				this.wrapAllInlineOrTextNodesAs("P", prevContainer, true);
1008 				prev = prevContainer.lastChild;
1009 			}
1010 		} else {
1011 			prev = null;
1012 		}
1013 		
1014 		try {
1015 			var containersAreTableCell =
1016 				prevContainer && (this.tree.isTableCell(prevContainer) || ['TR', 'THEAD', 'TBODY'].indexOf(prevContainer.nodeName) != -1) &&
1017 				nextContainer && (this.tree.isTableCell(nextContainer) || ['TR', 'THEAD', 'TBODY'].indexOf(nextContainer.nodeName) != -1);
1018 			
1019 			if(containersAreTableCell && prevContainer != nextContainer) return null;
1020 			
1021 			// if next has margin, perform outdent
1022 			if((!skip || !prev) && next && this.outdentElement(next)) return element;
1023 
1024 			// nextContainer is first li and next of it is list container
1025 			if(nextContainer && nextContainer.nodeName == 'LI' && this.tree.isListContainer(next.nextSibling)) {
1026 				this.extractOutElementFromParent(nextContainer);
1027 				return prev;
1028 			}
1029 			
1030 			// merge two list containers
1031 			if(nextContainer && nextContainer.nodeName == 'LI' && this.tree.isListContainer(nextContainer.parentNode.previousSibling)) {
1032 				this.mergeAdjustLists(nextContainer.parentNode.previousSibling, true, "next");
1033 				return prev;
1034 			}
1035 
1036 			if(next && !containersAreTableCell && prevContainer && prevContainer.nodeName == 'LI' && nextContainer && nextContainer.nodeName == 'LI' && prevContainer.parentNode.nextSibling == nextContainer.parentNode) {
1037 				var nextContainerContainer = nextContainer.parentNode;
1038 				this.moveChildNodes(nextContainer.parentNode, prevContainer.parentNode);
1039 				this.deleteNode(nextContainerContainer);
1040 				return prev;
1041 			}
1042 			
1043 			// merge two containers
1044 			if(next && !containersAreTableCell && prevContainer && prevContainer.nextSibling == nextContainer && ((skip && prevContainer.nodeName != "LI") || (!skip && prevContainer.nodeName == "LI"))) {
1045 				this.moveChildNodes(nextContainer, prevContainer);
1046 				return prev;
1047 			}
1048 
1049 			// unwrap container
1050 			if(nextContainer && nextContainer.nodeName != "LI" && !this.getParentElementOf(nextContainer, ["TABLE"]) && !this.tree.isListContainer(nextContainer) && nextContainer != this.getRoot() && !next.previousSibling) {
1051 				return this.unwrapElement(nextContainer, true);
1052 			}
1053 			
1054 			// delete table
1055 			if(withNext && nextContainer && nextContainer.nodeName == "TABLE") {
1056 				this.deleteNode(nextContainer, true);
1057 				return prev;
1058 			} else if(!withNext && prevContainer && this.tree.isTableCell(prevContainer) && !this.tree.isTableCell(nextContainer)) {
1059 				this.deleteNode(this.getParentElementOf(prevContainer, ["TABLE"]), true);
1060 				return next;
1061 			}
1062 			
1063 			// if prev is same with next, do nothing
1064 			if(prev == next) return null;
1065 
1066 			// if there is a null block, do nothing
1067 			if(!prev || !next || !prevContainer || !nextContainer) return null;
1068 			
1069 			// if two blocks are not in the same table cell, do nothing
1070 			if(this.getParentElementOf(prev, ["TD", "TH"]) != this.getParentElementOf(next, ["TD", "TH"])) return null;
1071 			
1072 			var prevIsEmpty = false;
1073 			
1074 			// cleanup empty block before merge
1075 
1076 			// 1. cleanup prev node which ends with marker +  
1077 			if(
1078 				xq.Browser.isTrident &&
1079 				prev.childNodes.length >= 2 &&
1080 				this.isMarker(prev.lastChild.previousSibling) &&
1081 				prev.lastChild.nodeType == 3 &&
1082 				prev.lastChild.nodeValue.length == 1 &&
1083 				prev.lastChild.nodeValue.charCodeAt(0) == 160
1084 			) {
1085 				this.deleteNode(prev.lastChild);
1086 			}
1087 
1088 			// 2. cleanup prev node (if prev is empty, then replace prev's tag with next's)
1089 			this.removePlaceHoldersAndEmptyNodes(prev);
1090 			if(this.isEmptyBlock(prev)) {
1091 				// replace atomic block with normal block so that following code don't need to care about atomic block
1092 				if(this.tree.isAtomic(prev)) prev = this.replaceTag("P", prev);
1093 				
1094 				prev = this.replaceTag(next.nodeName, prev) || prev;
1095 				prev.innerHTML = "";
1096 			} else if(prev.firstChild == prev.lastChild && this.isMarker(prev.firstChild)) {
1097 				prev = this.replaceTag(next.nodeName, prev) || prev;
1098 			}
1099 			
1100 			// 3. cleanup next node
1101 			if(this.isEmptyBlock(next)) {
1102 				// replace atomic block with normal block so that following code don't need to care about atomic block
1103 				if(this.tree.isAtomic(next)) next = this.replaceTag("P", next);
1104 				
1105 				next.innerHTML = "";
1106 			}
1107 			
1108 			// perform merge
1109 			this.moveChildNodes(next, prev);
1110 			this.deleteNode(next);
1111 			return prev;
1112 		} finally {
1113 			// cleanup
1114 			if(prevContainer && this.isEmptyBlock(prevContainer)) this.deleteNode(prevContainer, true);
1115 			if(nextContainer && this.isEmptyBlock(nextContainer)) this.deleteNode(nextContainer, true);
1116 			
1117 			if(prevContainer) this.unwrapUnnecessaryParagraph(prevContainer);
1118 			if(nextContainer) this.unwrapUnnecessaryParagraph(nextContainer);
1119 		}
1120 	},
1121 	
1122 	/**
1123 	 * Merges adjust list containers which has same tag name
1124 	 *
1125 	 * @param {Element} container target list container
1126 	 * @param {boolean} force force adjust list container even if they have different list type
1127 	 * @param {String} dir Specify merge direction: PREV or NEXT. If not supplied it will be merged with both direction.
1128 	 */
1129 	mergeAdjustLists: function(container, force, dir) {
1130 		var prev = container.previousSibling;
1131 		var isPrevSame = prev && (prev.nodeName == container.nodeName && prev.className == container.className);
1132 		if((!dir || dir.toLowerCase() == 'prev') && (isPrevSame || (force && this.tree.isListContainer(prev)))) {
1133 			while(prev.lastChild) {
1134 				this.insertNodeAt(prev.lastChild, container, "start");
1135 			}
1136 			this.deleteNode(prev);
1137 		}
1138 		
1139 		var next = container.nextSibling;
1140 		var isNextSame = next && (next.nodeName == container.nodeName && next.className == container.className);
1141 		if((!dir || dir.toLowerCase() == 'next') && (isNextSame || (force && this.tree.isListContainer(next)))) {
1142 			while(next.firstChild) {
1143 				this.insertNodeAt(next.firstChild, container, "end");
1144 			}
1145 			this.deleteNode(next);
1146 		}
1147 	},
1148 	
1149 	/**
1150 	 * Moves child nodes from one element into another.
1151 	 *
1152 	 * @param {Elemet} from source element
1153 	 * @param {Elemet} to target element
1154 	 */
1155 	moveChildNodes: function(from, to) {
1156 		if(this.tree.isDescendantOf(from, to) || ["HTML", "HEAD"].indexOf(to.nodeName) != -1)
1157 			throw "Illegal argument. Cannot move children of element[" + from.nodeName + "] to element[" + to.nodeName + "]";
1158 		
1159 		if(from == to) return;
1160 		
1161 		while(from.firstChild) to.appendChild(from.firstChild);
1162 	},
1163 	
1164 	/**
1165 	 * Copies attributes from one element into another.
1166 	 *
1167 	 * @param {Element} from source element
1168 	 * @param {Element} to target element
1169 	 * @param {boolean} copyId copy ID attribute of source element
1170 	 */
1171 	copyAttributes: function(from, to, copyId) {
1172 		// IE overrides this
1173 		
1174 		var attrs = from.attributes;
1175 		if(!attrs) return;
1176 		
1177 		for(var i = 0; i < attrs.length; i++) {
1178 			if(attrs[i].nodeName == "class" && attrs[i].nodeValue) {
1179 				to.className = attrs[i].nodeValue;
1180 			} else if((copyId || "id" != attrs[i].nodeName) && attrs[i].nodeValue) {
1181 				to.setAttribute(attrs[i].nodeName, attrs[i].nodeValue);
1182 			}
1183 		}
1184 	},
1185 
1186 	_indentElements: function(node, blocks, affect) {
1187 		for (var i=0; i < affect.length; i++) {
1188 			if (affect[i] == node || this.tree.isDescendantOf(affect[i], node))
1189 				return;
1190 		}
1191 		leaves = this.tree.getLeavesAtEdge(node);
1192 		
1193 		if (blocks.include(leaves[0])) {
1194 			var affected = this.indentElement(node, true);
1195 			if (affected) {
1196 				affect.push(affected);
1197 				return;
1198 			}
1199 		}
1200 		
1201 		if (blocks.include(node)) {
1202 			var affected = this.indentElement(node, true);
1203 			if (affected) {
1204 				affect.push(affected);
1205 				return;
1206 			}
1207 		}
1208 
1209 		var children=xq.$A(node.childNodes);
1210 		for (var i=0; i < children.length; i++)
1211 			this._indentElements(children[i], blocks, affect);
1212 		return;
1213 	},
1214 
1215 	indentElements: function(from, to) {
1216 		var blocks = this.getBlockElementsBetween(from, to);
1217 		var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
1218 		
1219 		var affect = [];
1220 		
1221 		leaves = this.tree.getLeavesAtEdge(top.parent);
1222 		if (blocks.include(leaves[0])) {
1223 			var affected = this.indentElement(top.parent);
1224 			if (affected)
1225 				return [affected];
1226 		}
1227 		
1228 		var children = xq.$A(top.parent.childNodes);
1229 		for (var i=0; i < children.length; i++) {
1230 			this._indentElements(children[i], blocks, affect);
1231 		}
1232 		
1233 		affect = affect.flatten()
1234 		return affect.length > 0 ? affect : blocks;
1235 	},
1236 	
1237 	outdentElementsCode: function(node) {
1238 		if (node.tagName == 'LI')
1239 			node = node.parentNode;
1240 		if (node.tagName == 'OL' && node.className == 'code')
1241 			return true;
1242 		return false;
1243 	},
1244 	
1245 	_outdentElements: function(node, blocks, affect) {
1246 		for (var i=0; i < affect.length; i++) {
1247 			if (affect[i] == node || this.tree.isDescendantOf(affect[i], node))
1248 				return;
1249 		}
1250 		leaves = this.tree.getLeavesAtEdge(node);
1251 		
1252 		if (blocks.include(leaves[0]) && !this.outdentElementsCode(leaves[0])) {
1253 			var affected = this.outdentElement(node, true);
1254 			if (affected) {
1255 				affect.push(affected);
1256 				return;
1257 			}
1258 		}
1259 		
1260 		if (blocks.include(node)) {
1261 			var children = xq.$A(node.parentNode.childNodes);
1262 			var isCode = this.outdentElementsCode(node);
1263 			var affected = this.outdentElement(node, true, isCode);
1264 			if (affected) {
1265 				if (children.include(affected) && this.tree.isListContainer(node.parentNode) && !isCode) {
1266 					for (var i=0; i < children.length; i++) {
1267 						if (blocks.include(children[i]) && !affect.include(children[i]))
1268 							affect.push(children[i]);
1269 					}
1270 				}else
1271 					affect.push(affected);
1272 				return;
1273 			}
1274 		}
1275 
1276 		var children=xq.$A(node.childNodes);
1277 		for (var i=0; i < children.length; i++)
1278 			this._outdentElements(children[i], blocks, affect);
1279 		return;
1280 	},
1281 
1282 	outdentElements: function(from, to) {
1283 		var start, end;
1284 		
1285 		if (from.parentNode.tagName == 'LI') start=from.parentNode;
1286 		if (to.parentNode.tagName == 'LI') end=to.parentNode;
1287 		
1288 		var blocks = this.getBlockElementsBetween(from, to);
1289 		var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
1290 		
1291 		var affect = [];
1292 		
1293 		leaves = this.tree.getLeavesAtEdge(top.parent);
1294 		if (blocks.include(leaves[0]) && !this.outdentElementsCode(top.parent)) {
1295 			var affected = this.outdentElement(top.parent);
1296 			if (affected)
1297 				return [affected];
1298 		}
1299 		
1300 		var children = xq.$A(top.parent.childNodes);
1301 		for (var i=0; i < children.length; i++) {
1302 			this._outdentElements(children[i], blocks, affect);
1303 		}
1304 
1305 		if (from.offsetParent && to.offsetParent) {
1306 			start = from;
1307 			end = to;
1308 		}else if (blocks.first().offsetParent && blocks.last().offsetParent) {
1309 			start = blocks.first();
1310 			end = blocks.last();
1311 		}
1312 		
1313 		affect = affect.flatten()
1314 		if (!start || !start.offsetParent)
1315 			start = affect.first();
1316 		if (!end || !end.offsetParent)
1317 			end = affect.last();
1318 		
1319 		return this.getBlockElementsBetween(start, end);
1320 	},
1321 	
1322 	/**
1323 	 * Performs indent by increasing element's margin-left
1324 	 */	
1325 	indentElement: function(element, noParent, forceMargin) {
1326 		if(
1327 			!forceMargin &&
1328 			(element.nodeName == "LI" || (!this.tree.isListContainer(element) && !element.previousSibling && element.parentNode.nodeName == "LI"))
1329 		) return this.indentListItem(element, noParent);
1330 		
1331 		var root = this.getRoot();
1332 		if(!element || element == root) return null;
1333 		
1334 		if (element.parentNode != root && !element.previousSibling && !noParent) element=element.parentNode;
1335 		
1336 		var margin = element.style.marginLeft;
1337 		var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"};
1338 		
1339 		cssValue.value += 2;
1340 		element.style.marginLeft = cssValue.value + cssValue.unit;
1341 		
1342 		return element;
1343 	},
1344 	
1345 	/**
1346 	 * Performs outdent by decreasing element's margin-left
1347 	 */	
1348 	outdentElement: function(element, noParent, forceMargin) {
1349 		if(!forceMargin && element.nodeName == "LI") return this.outdentListItem(element, noParent);
1350 		
1351 		var root = this.getRoot();
1352 		if(!element || element == root) return null;
1353 		
1354 		var margin = element.style.marginLeft;
1355 		
1356 		var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"};
1357 		if(cssValue.value == 0) {
1358 			return element.previousSibling || forceMargin ?
1359 				null :
1360 				this.outdentElement(element.parentNode, noParent);
1361 		}
1362 		
1363 		cssValue.value -= 2;
1364 		element.style.marginLeft = cssValue.value <= 0 ? "" : cssValue.value + cssValue.unit;
1365 		if(element.style.cssText == "") element.removeAttribute("style");
1366 		
1367 		return element;
1368 	},
1369 	
1370 	/**
1371 	 * Performs indent for list item
1372 	 */
1373 	indentListItem: function(element, treatListAsNormalBlock) {
1374 		var li = this.getParentElementOf(element, ["LI"]);
1375 		var container = li.parentNode;
1376 		var prev = li.previousSibling;
1377 		if(!li.previousSibling) return this.indentElement(container);
1378 		
1379 		if(li.parentNode.nodeName == "OL" && li.parentNode.className == "code") return this.indentElement(li, treatListAsNormalBlock, true);
1380 		
1381 		if(!prev.lastChild) prev.appendChild(this.makePlaceHolder());
1382 		
1383 		var targetContainer = 
1384 			this.tree.isListContainer(prev.lastChild) ?
1385 			// if there's existing list container, select it as target container
1386 			prev.lastChild :
1387 			// if there's nothing, create new one
1388 			this.insertNodeAt(this.createElement(container.nodeName), prev, "end");
1389 		
1390 		this.wrapAllInlineOrTextNodesAs("P", prev, true);
1391 		
1392 		// perform move
1393 		targetContainer.appendChild(li);
1394 		
1395 		// flatten nested list
1396 		if(!treatListAsNormalBlock && li.lastChild && this.tree.isListContainer(li.lastChild)) {
1397 			var childrenContainer = li.lastChild;
1398 			var child;
1399 			while(child = childrenContainer.lastChild) {
1400 				this.insertNodeAt(child, li, "after");
1401 			}
1402 			this.deleteNode(childrenContainer);
1403 		}
1404 		
1405 		this.unwrapUnnecessaryParagraph(li);
1406 		
1407 		return li;
1408 	},
1409 	
1410 	/**
1411 	 * Performs outdent for list item
1412 	 *
1413 	 * @return {Element} outdented list item or null if no outdent performed
1414 	 */
1415 	outdentListItem: function(element, treatListAsNormalBlock) {
1416 		var li = this.getParentElementOf(element, ["LI"]);
1417 		var container = li.parentNode;
1418 
1419 		if(!li.previousSibling) {
1420 			var performed = this.outdentElement(container);
1421 			if(performed) return performed;
1422 		}
1423 
1424 		if(li.parentNode.nodeName == "OL" && li.parentNode.className == "code") return this.outdentElement(li, treatListAsNormalBlock, true);
1425 		
1426 		var parentLi = container.parentNode;
1427 		if(parentLi.nodeName != "LI") return null;
1428 		
1429 		if(treatListAsNormalBlock) {
1430 			while(container.lastChild != li) {
1431 				this.insertNodeAt(container.lastChild, parentLi, "after");
1432 			}
1433 		} else {
1434 			// make next siblings as children
1435 			if(li.nextSibling) {
1436 				var targetContainer =
1437 					li.lastChild && this.tree.isListContainer(li.lastChild) ?
1438 						// if there's existing list container, select it as target container
1439 						li.lastChild :
1440 						// if there's nothing, create new one
1441 						this.insertNodeAt(this.createElement(container.nodeName), li, "end");
1442 				
1443 				this.copyAttributes(container, targetContainer);
1444 				
1445 				var sibling;
1446 				while(sibling = li.nextSibling) {
1447 					targetContainer.appendChild(sibling);
1448 				}
1449 			}
1450 		}
1451 		
1452 		// move current LI into parent LI's next sibling
1453 		li = this.insertNodeAt(li, parentLi, "after");
1454 		
1455 		// remove empty container
1456 		if(container.childNodes.length == 0) this.deleteNode(container);
1457 		
1458 		if(li.firstChild && this.tree.isListContainer(li.firstChild)) {
1459 			this.insertNodeAt(this.makePlaceHolder(), li, "start");
1460 		}
1461 		
1462 		this.wrapAllInlineOrTextNodesAs("P", li);
1463 		this.unwrapUnnecessaryParagraph(parentLi);
1464 		
1465 		return li;
1466 	},
1467 	
1468 	/**
1469 	 * Performs justification
1470 	 *
1471 	 * @param {Element} block target element
1472 	 * @param {String} dir one of "LEFT", "CENTER", "RIGHT", "BOTH"
1473 	 */
1474 	justifyBlock: function(block, dir) {
1475 		// if block is only child, select its parent as mover
1476 		while(block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
1477 			block = block.parentNode;
1478 		}
1479 		
1480 		var styleValue = dir.toLowerCase() == "both" ? "justify" : dir;
1481 		if(styleValue == "left") {
1482 			block.style.textAlign = "";
1483 			if(block.style.cssText == "") block.removeAttribute("style");
1484 		} else {
1485 			block.style.textAlign = styleValue;
1486 		}
1487 		return block;
1488 	},
1489 	
1490 	justifyBlocks: function(blocks, dir) {
1491 		for(var i = 0; i < blocks.length; i++) {
1492 			this.justifyBlock(blocks[i], dir);
1493 		}
1494 		return blocks;
1495 	},
1496 	
1497 	/**
1498      * Turn given element into list. If the element is a list already, it will be reversed into normal element.
1499 	 *
1500 	 * @param {Element} element target element
1501 	 * @param {String} type one of "UL", "OL"
1502 	 * @returns {Element} affected element
1503 	 */
1504 	applyList: function(element, type) {
1505 		type = type.toUpperCase();
1506 		var containerTag = type == "UL" ? "UL" : "OL";
1507 		
1508 		if(element.nodeName == "LI" || (element.parentNode.nodeName == "LI" && !element.previousSibling)) {
1509 			var element = this.getParentElementOf(element, ["LI"]);
1510 			var container = element.parentNode;
1511 			if(container.nodeName == containerTag) {
1512 				return this.extractOutElementFromParent(element);
1513 			} else {
1514 				return this.changeListTypeTo(element, type);
1515 			}
1516 		} else {
1517 			return this.turnElementIntoListItem(element, type);
1518 		}
1519 	},
1520 	
1521 	applyLists: function(from, to, type) {
1522 		type = type.toUpperCase();
1523 		var containerTag = type == "UL" ? "UL" : "OL";
1524 		var blocks = this.getBlockElementsBetween(from, to);
1525 		
1526 		// LIs or Non-containing blocks
1527 		var whole = blocks.findAll(function(e) {
1528 			return e.nodeName == "LI" || !this.tree.isBlockContainer(e);
1529 		}.bind(this));
1530 		
1531 		// LIs
1532 		var listItems = whole.findAll(function(e) {return e.nodeName == "LI"}.bind(this));
1533 		
1534 		// Non-containing blocks which is not a descendant of any LIs selected above(listItems).
1535 		var normalBlocks = whole.findAll(function(e) {
1536 			return e.nodeName != "LI" &&
1537 				!(e.parentNode.nodeName == "LI" && !e.previousSibling && !e.nextSibling) &&
1538 				!this.tree.isDescendantOf(listItems, e)
1539 		}.bind(this));
1540 		
1541 		var diffListItems = listItems.findAll(function(e) {
1542 			return e.parentNode.nodeName != containerTag;
1543 		}.bind(this));
1544 		
1545 		// Conditions needed to determine mode
1546 		var hasNormalBlocks = normalBlocks.length > 0;
1547 		var hasDifferentListStyle = diffListItems.length > 0;
1548 		
1549 		var blockToHandle = null;
1550 		
1551 		if(hasNormalBlocks) {
1552 			blockToHandle = normalBlocks;
1553 		} else if(hasDifferentListStyle) {
1554 			blockToHandle = diffListItems;
1555 		} else {
1556 			blockToHandle = listItems;
1557 		}
1558 		
1559 		// perform operation
1560 		for(var i = 0; i < blockToHandle.length; i++) {
1561 			var block = blockToHandle[i];
1562 			
1563 			// preserve original index to restore selection
1564 			var originalIndex = blocks.indexOf(block);
1565 			blocks[originalIndex] = this.applyList(block, type);
1566 		}
1567 		
1568 		return blocks;
1569 	},
1570 
1571 	/**
1572 	 * Insert place-holder for given empty element. Empty element does not displayed and causes many editing problems.
1573 	 *
1574 	 * @param {Element} element empty element
1575 	 */
1576 	correctEmptyElement: function(element) {throw "Not implemented"},
1577 
1578 	/**
1579 	 * Corrects current block-only-container to do not take any non-block element or node.
1580 	 */
1581 	correctParagraph: function() {throw "Not implemented"},
1582 	
1583 	/**
1584 	 * Makes place-holder for empty element.
1585 	 *
1586 	 * @returns {Node} Platform specific place holder
1587 	 */
1588 	makePlaceHolder: function() {throw "Not implemented"},
1589 	
1590 	/**
1591 	 * Makes place-holder string.
1592 	 *
1593 	 * @returns {String} Platform specific place holder string
1594 	 */
1595 	makePlaceHolderString: function() {throw "Not implemented"},
1596 	
1597 	/**
1598 	 * Makes empty paragraph which contains only one place-holder
1599 	 */
1600 	makeEmptyParagraph: function() {throw "Not implemented"},
1601 
1602 	/**
1603 	 * Applies background color to selected area
1604 	 *
1605 	 * @param {Object} color valid CSS color value
1606 	 */
1607 	applyBackgroundColor: function(color) {throw "Not implemented";},
1608 
1609 	/**
1610 	 * Applies foreground color to selected area
1611 	 *
1612 	 * @param {Object} color valid CSS color value
1613 	 */
1614 	applyForegroundColor: function(color) {
1615 		this.execCommand("forecolor", color);
1616 	},
1617 	
1618 	execCommand: function(commandId, param) {throw "Not implemented";},
1619 	
1620 	applyRemoveFormat: function() {throw "Not implemented";},
1621 	applyEmphasis: function() {throw "Not implemented";},
1622 	applyStrongEmphasis: function() {throw "Not implemented";},
1623 	applyStrike: function() {throw "Not implemented";},
1624 	applyUnderline: function() {throw "Not implemented";},
1625 	applySuperscription: function() {
1626 		this.execCommand("superscript");
1627 	},
1628 	applySubscription: function() {
1629 		this.execCommand("subscript");
1630 	},
1631 	indentBlock: function(element, treatListAsNormalBlock) {
1632 		return (!element.previousSibling && element.parentNode.nodeName == "LI") ?
1633 			this.indentListItem(element, treatListAsNormalBlock) :
1634 			this.indentElement(element);
1635 	},
1636 	outdentBlock: function(element, treatListAsNormalBlock) {
1637 		while(true) {
1638 			if(!element.previousSibling && element.parentNode.nodeName == "LI") {
1639 				element = this.outdentListItem(element, treatListAsNormalBlock);
1640 				return element;
1641 			} else {
1642 				var performed = this.outdentElement(element);
1643 				if(performed) return performed;
1644 				
1645 				// first-child can outdent container
1646 				if(!element.previousSibling) {
1647 					element = element.parentNode;
1648 				} else {
1649 					break;
1650 				}
1651 			}
1652 		}
1653 		
1654 		return null;
1655 	},
1656 	wrapBlock: function(tag, start, end) {
1657 		if(this.tree._blockTags.indexOf(tag) == -1) throw "Unsuppored block container: [" + tag + "]";
1658 		if(!start) start = this.getCurrentBlockElement();
1659 		if(!end) end = start;
1660 		
1661 		// Check if the selection captures valid fragement
1662 		var validFragment = false;
1663 		
1664 		if(start == end) {
1665 			// are they same block?
1666 			validFragment = true;
1667 		} else if(start.parentNode == end.parentNode && !start.previousSibling && !end.nextSibling) {
1668 			// are they covering whole parent?
1669 			validFragment = true;
1670 			start = end = start.parentNode;
1671 		} else {
1672 			// are they siblings of non-LI blocks?
1673 			validFragment =
1674 				(start.parentNode == end.parentNode) &&
1675 				(start.nodeName != "LI");
1676 		}
1677 		
1678 		if(!validFragment) return null;
1679 		
1680 		var wrapper = this.createElement(tag);
1681 		
1682 		if(start == end) {
1683 			// They are same.
1684 			if(this.tree.isBlockContainer(start) && !this.tree.isListContainer(start)) {
1685 				// It's a block container. Wrap its contents.
1686 				if(this.tree.isBlockOnlyContainer(wrapper)) {
1687 					this.correctEmptyElement(start);
1688 					this.wrapAllInlineOrTextNodesAs("P", start, true);
1689 				}
1690 				this.moveChildNodes(start, wrapper);
1691 				start.appendChild(wrapper);
1692 			} else {
1693 				// It's not a block container. Wrap itself.
1694 				wrapper = this.insertNodeAt(wrapper, start, "after");
1695 				wrapper.appendChild(start);
1696 			}
1697 			
1698 			this.correctEmptyElement(wrapper);
1699 		} else {
1700 			// They are siblings. Wrap'em all.
1701 			wrapper = this.insertNodeAt(wrapper, start, "before");
1702 			var node = start;
1703 			
1704 			while(node != end) {
1705 				next = node.nextSibling;
1706 				wrapper.appendChild(node);
1707 				node = next;
1708 			}
1709 			wrapper.appendChild(node);
1710 		}
1711 		
1712 		return wrapper;
1713 	},
1714 
1715 
1716 	
1717 	/////////////////////////////////////////////
1718 	// Focus/Caret/Selection
1719 	
1720 	/**
1721 	 * Gives focus to root element's window
1722 	 */
1723 	focus: function() {throw "Not implemented";},
1724 
1725 	/**
1726 	 * Returns selection object
1727 	 */
1728 	sel: function() {throw "Not implemented";},
1729 	
1730 	/**
1731 	 * Returns range object
1732 	 */
1733 	rng: function() {throw "Not implemented";},
1734 	
1735 	/**
1736 	 * Returns true if DOM has selection
1737 	 */
1738 	hasSelection: function() {throw "Not implemented";},
1739 
1740 	/**
1741 	 * Returns true if root element's window has selection
1742 	 */
1743 	hasFocus: function() {
1744 		var cur = this.getCurrentElement();
1745 		return (cur && cur.ownerDocument == this.getDoc());
1746 	},
1747 	
1748 	/**
1749 	 * Adjust scrollbar to make the element visible in current viewport.
1750 	 *
1751 	 * @param {Element} element Target element
1752 	 * @param {boolean} toTop Align element to top of the viewport
1753 	 * @param {boolean} moveCaret Move caret to the element
1754 	 */
1755 	scrollIntoView: function(element, toTop, moveCaret) {
1756 		element.scrollIntoView(toTop);
1757 		if(moveCaret) this.placeCaretAtStartOf(element);
1758 	},
1759 	
1760 	/**
1761 	 * Select all document
1762 	 */
1763 	selectAll: function() {
1764 		return this.execCommand('selectall');
1765 	},
1766 	
1767 	/**
1768 	 * Select specified element.
1769 	 *
1770 	 * @param {Element} element element to select
1771 	 * @param {boolean} entireElement true to select entire element, false to select inner content of element 
1772 	 */
1773 	selectElement: function(node, entireElement) {throw "Not implemented"},
1774 	
1775 	/**
1776 	 * Select all elements between two blocks(inclusive).
1777 	 *
1778 	 * @param {Element} start start of selection
1779 	 * @param {Element} end end of selection
1780 	 */
1781 	selectBlocksBetween: function(start, end) {throw "Not implemented"},
1782 	
1783 	/**
1784 	 * Delete selected area
1785 	 */
1786 	deleteSelection: function() {throw "Not implemented"},
1787 	
1788 	/**
1789 	 * Collapses current selection.
1790 	 *
1791 	 * @param {boolean} toStart true to move caret to start of selected area.
1792 	 */
1793 	collapseSelection: function(toStart) {throw "Not implemented"},
1794 	
1795 	/**
1796 	 * Returns selected area as HTML string
1797 	 */
1798 	getSelectionAsHtml: function() {throw "Not implemented"},
1799 	
1800 	/**
1801 	 * Returns selected area as text string
1802 	 */
1803 	getSelectionAsText: function() {throw "Not implemented"},
1804 	
1805 	/**
1806 	 * Places caret at start of the element
1807 	 *
1808 	 * @param {Element} element Target element
1809 	 */
1810 	placeCaretAtStartOf: function(element) {throw "Not implemented"},
1811 	
1812 	/**
1813 	 * Checks if the node is empty-text-node or not
1814 	 */
1815 	isEmptyTextNode: function(node) {
1816 		return node.nodeType == 3 && node.nodeValue.length == 0;
1817 	},
1818 	
1819 	/**
1820 	 * Checks if the caret is place in empty block element
1821 	 */
1822 	isCaretAtEmptyBlock: function() {
1823 		return this.isEmptyBlock(this.getCurrentBlockElement());
1824 	},
1825 	
1826 	/**
1827 	 * Checks if the caret is place at start of the block
1828 	 */
1829 	isCaretAtBlockStart: function() {throw "Not implemented"},
1830 
1831 	/**
1832 	 * Checks if the caret is place at end of the block
1833 	 */
1834 	isCaretAtBlockEnd: function() {throw "Not implemented"},
1835 	
1836 	/**
1837 	 * Saves current selection info
1838 	 *
1839 	 * @returns {Object} Bookmark for selection
1840 	 */
1841 	saveSelection: function() {throw "Not implemented"},
1842 	
1843 	/**
1844 	 * Restores current selection info
1845 	 *
1846 	 * @param {Object} bookmark Bookmark
1847 	 */
1848 	restoreSelection: function(bookmark) {throw "Not implemented"},
1849 	
1850 	/**
1851 	 * Create marker
1852 	 */
1853 	createMarker: function() {
1854 		var marker = this.createElement("SPAN");
1855 		marker.id = "xquared_marker_" + (this._lastMarkerId++);
1856 		marker.className = "xquared_marker";
1857 		return marker;
1858 	},
1859 
1860 	/**
1861 	 * Create and insert marker into current caret position.
1862 	 * Marker is an inline element which has no child nodes. It can be used with many purposes.
1863 	 * For example, You can push marker to mark current caret position.
1864 	 *
1865 	 * @returns {Element} marker
1866 	 */
1867 	pushMarker: function() {
1868 		var marker = this.createMarker();
1869 		return this.insertNode(marker);
1870 	},
1871 	
1872 	/**
1873 	 * Removes last marker
1874 	 *
1875 	 * @params {boolean} moveCaret move caret into marker before delete.
1876 	 */
1877 	popMarker: function(moveCaret) {
1878 		var id = "xquared_marker_" + (--this._lastMarkerId);
1879 		var marker = this.$(id);
1880 		if(!marker) return;
1881 		
1882 		if(moveCaret) {
1883 			this.selectElement(marker, true);
1884 			this.collapseSelection(false);
1885 		}
1886 		
1887 		this.deleteNode(marker);
1888 	},
1889 	
1890 	
1891 	
1892 	/////////////////////////////////////////////
1893 	// Query methods
1894 	
1895 	isMarker: function(node) {
1896 		return (node.nodeType == 1 && node.nodeName == "SPAN" && node.className == "xquared_marker");
1897 	},
1898 	
1899 	isFirstBlockOfBody: function(block) {
1900 		var root = this.getRoot();
1901 		var found = this.tree.findBackward(
1902 			block,
1903 			function(node) {return (node == root) || node.previousSibling;}.bind(this)
1904 		);
1905 		
1906 		return found == root;
1907 	},
1908 	
1909 	/**
1910 	 * Returns outer HTML of given element
1911 	 */
1912 	getOuterHTML: function(element) {throw "Not implemented"},
1913 	
1914 	/**
1915 	 * Returns inner text of given element
1916 	 * 
1917 	 * @param {Element} element Target element
1918 	 * @returns {String} Text string
1919 	 */
1920 	getInnerText: function(element) {
1921 		return element.innerHTML.stripTags();
1922 	},
1923 
1924 	/**
1925 	 * Checks if given node is place holder or not.
1926 	 * 
1927 	 * @param {Node} node DOM node
1928 	 */
1929 	isPlaceHolder: function(node) {throw "Not implemented"},
1930 	
1931 	/**
1932 	 * Checks if given block is the first LI whose next sibling is a nested list.
1933 	 *
1934 	 * @param {Element} block Target block
1935 	 */
1936 	isFirstLiWithNestedList: function(block) {
1937 		return !block.previousSibling &&
1938 			block.parentNode.nodeName == "LI" &&
1939 			this.tree.isListContainer(block.nextSibling);
1940 	},
1941 	
1942 	/**
1943 	 * Search all links within given element
1944 	 *
1945 	 * @param {Element} [element] Container element. If not given, the root element will be used.
1946 	 * @param {Array} [found] if passed, links will be appended into this array.
1947 	 * @returns {Array} Array of anchors. It returns empty array if there's no links.
1948 	 */
1949 	searchAnchors: function(element, found) {
1950 		if(!element) element = this.getRoot();
1951 		if(!found) found = [];
1952 
1953 		var anchors = element.getElementsByTagName("A");
1954 		for(var i = 0; i < anchors.length; i++) {
1955 			found.push(anchors[i]);
1956 		}
1957 
1958 		return found;
1959 	},
1960 	
1961 	/**
1962 	 * Search all headings within given element
1963 	 *
1964 	 * @param {Element} [element] Container element. If not given, the root element will be used.
1965 	 * @param {Array} [found] if passed, headings will be appended into this array.
1966 	 * @returns {Array} Array of headings. It returns empty array if there's no headings.
1967 	 */
1968 	searchHeadings: function(element, found) {
1969 		if(!element) element = this.getRoot();
1970 		if(!found) found = [];
1971 
1972 		var regexp = /^h[1-6]/ig;
1973 		var nodes = element.childNodes;
1974 		if (!nodes) return [];
1975 		
1976 		for(var i = 0; i < nodes.length; i++) {
1977 			var isContainer = nodes[i] && this.tree._blockContainerTags.indexOf(nodes[i].nodeName) != -1;
1978 			var isHeading = nodes[i] && nodes[i].nodeName.match(regexp);
1979 
1980 			if (isContainer) {
1981 				this.searchHeadings(nodes[i], found);
1982 			} else if (isHeading) {
1983 				found.push(nodes[i]);
1984 			}
1985 		}
1986 
1987 		return found;
1988 	},
1989 	
1990 	/**
1991 	 * Collect structure and style informations of given element.
1992 	 *
1993 	 * @param {Element} element target element
1994 	 * @returns {Object} object that contains information: {em: true, strong: false, block: "p", list: "ol", ...}
1995 	 */
1996 	collectStructureAndStyle: function(element) {
1997 		if(!element || element.nodeName == "#document") return {};
1998 
1999 		var block = this.getParentBlockElementOf(element);
2000 		
2001 		// IE���� ��Ȥ DOM�� �� ��: element�� ���ڷ� �Ѿ�4���찡��
2002 		if(block == null) return {};
2003 		
2004 		var parents = this.tree.collectParentsOf(element, true, function(node) {return block.parentNode == node});
2005 		var blockName = block.nodeName;
2006 
2007 		var info = {};
2008 		
2009 		var doc = this.getDoc();
2010 		var em = doc.queryCommandState("Italic");
2011 		var strong = doc.queryCommandState("Bold");
2012 		var strike = doc.queryCommandState("Strikethrough");
2013 		var underline = doc.queryCommandState("Underline") && !this.getParentElementOf(element, ["A"]);
2014 		var superscription = doc.queryCommandState("superscript");
2015 		var subscription = doc.queryCommandState("subscript");
2016 		
2017 		// if block is only child, select its parent
2018 		while(block.parentNode && block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
2019 			block = block.parentNode;
2020 		}
2021 
2022 		var list = false;
2023 		if(block.nodeName == "LI") {
2024 			var parent = block.parentNode;
2025 			var isCode = parent.nodeName == "OL" && parent.className == "code";
2026 			list = isCode ? "CODE" : parent.nodeName;
2027 		}
2028 		
2029 		var justification = block.style.textAlign || "left";
2030 		
2031 		return {
2032 			block:blockName,
2033 			em: em,
2034 			strong: strong,
2035 			strike: strike,
2036 			underline: underline,
2037 			superscription: superscription,
2038 			subscription: subscription,
2039 			list: list,
2040 			justification: justification
2041 		};
2042 	},
2043 	
2044 	/**
2045 	 * Checks if the element has one or more important attributes: id, class, style
2046 	 *
2047 	 * @param {Element} element Target element
2048 	 */
2049 	hasImportantAttributes: function(element) {throw "Not implemented"},
2050 	
2051 	/**
2052 	 * Checks if the element is empty or not. Place-holder is not counted as a child.
2053 	 *
2054 	 * @param {Element} element Target element
2055 	 */
2056 	isEmptyBlock: function(element) {throw "Not implemented"},
2057 	
2058 	/**
2059 	 * Returns element that contains caret.
2060 	 */
2061 	getCurrentElement: function() {throw "Not implemented"},
2062 	
2063 	/**
2064 	 * Returns block element that contains caret.
2065 	 */
2066 	getCurrentBlockElement: function() {
2067 		var cur = this.getCurrentElement();
2068 		if(!cur) return null;
2069 		
2070 		var block = this.getParentBlockElementOf(cur);
2071 		if(!block) return null;
2072 		
2073 		return (block.nodeName == "BODY") ? null : block;
2074 	},
2075 	
2076 	/**
2077 	 * Returns parent block element of parameter.
2078 	 * If the parameter itself is a block, it will be returned.
2079 	 *
2080 	 * @param {Element} element Target element
2081 	 *
2082 	 * @returns {Element} Element or null
2083 	 */
2084 	getParentBlockElementOf: function(element) {
2085 		while(element) {
2086 			if(this.tree._blockTags.indexOf(element.nodeName) != -1) return element;
2087 			element = element.parentNode;
2088 		}
2089 		return null;
2090 	},
2091 	
2092 	/**
2093 	 * Returns parent element of parameter which has one of given tag name.
2094 	 * If the parameter itself has the same tag name, it will be returned.
2095 	 *
2096 	 * @param {Element} element Target element
2097 	 * @param {Array} tagNames Array of string which contains tag names
2098 	 *
2099 	 * @returns {Element} Element or null
2100 	 */
2101 	getParentElementOf: function(element, tagNames) {
2102 		while(element) {
2103 			if(tagNames.indexOf(element.nodeName) != -1) return element;
2104 			element = element.parentNode;
2105 		}
2106 		return null;
2107 	},
2108 	
2109 	/**
2110 	 * Collects all block elements between two elements
2111 	 *
2112 	 * @param {Element} from Start element(inclusive)
2113 	 * @param {Element} to End element(inclusive)
2114 	 */
2115 	getBlockElementsBetween: function(from, to) {
2116 		return this.tree.collectNodesBetween(from, to, function(node) {
2117 			return node.nodeType == 1 && this.tree.isBlock(node);
2118 		}.bind(this));
2119 	},
2120 	
2121 	/**
2122 	 * Returns block element that contains selection start.
2123 	 *
2124 	 * This method will return exactly same result with getCurrentBlockElement method
2125 	 * when there's no selection.
2126 	 */
2127 	getBlockElementAtSelectionStart: function() {throw "Not implemented"},
2128 	
2129 	/**
2130 	 * Returns block element that contains selection end.
2131 	 *
2132 	 * This method will return exactly same result with getCurrentBlockElement method
2133 	 * when there's no selection.
2134 	 */
2135 	getBlockElementAtSelectionEnd: function() {throw "Not implemented"},
2136 	
2137 	/**
2138 	 * Returns blocks at each edge of selection(start and end).
2139 	 *
2140 	 * TODO: implement ignoreEmptyEdges for FF
2141 	 *
2142 	 * @param {boolean} naturalOrder Mak the start element always comes before the end element
2143 	 * @param {boolean} ignoreEmptyEdges Prevent some browser(Gecko) from selecting one more block than expected
2144 	 */
2145 	getBlockElementsAtSelectionEdge: function(naturalOrder, ignoreEmptyEdges) {throw "Not implemented"},
2146 	
2147 	/**
2148 	 * Returns array of selected block elements
2149 	 */
2150 	getSelectedBlockElements: function() {
2151 		var selectionEdges = this.getBlockElementsAtSelectionEdge(true, true);
2152 		var start = selectionEdges[0];
2153 		var end = selectionEdges[1];
2154 		
2155 		return this.tree.collectNodesBetween(start, end, function(node) {
2156 			return node.nodeType == 1 && this.tree.isBlock(node);
2157 		}.bind(this));
2158 	},
2159 	
2160 	/**
2161 	 * Get element by ID
2162 	 *
2163 	 * @param {String} id Element's ID
2164 	 * @returns {Element} element or null
2165 	 */
2166 	getElementById: function(id) {return this.doc.getElementById(id)},
2167 	
2168 	/**
2169 	 * Shortcut for #getElementById
2170 	 */
2171 	$: function(id) {return this.getElementById(id)},
2172 	
2173 	/**
2174 	  * Returns first "valid" child of given element. It ignores empty textnodes.
2175 	  *
2176 	  * @param {Element} element Target element
2177 	  * @returns {Node} first child node or null
2178 	  */
2179 	getFirstChild: function(element) {
2180 		if(!element) return null;
2181 		
2182 		var nodes = xq.$A(element.childNodes);
2183 		return nodes.find(function(node) {return !this.isEmptyTextNode(node)}.bind(this));
2184 	},
2185 	
2186 	/**
2187 	  * Returns last "valid" child of given element. It ignores empty textnodes and place-holders.
2188 	  *
2189 	  * @param {Element} element Target element
2190 	  * @returns {Node} last child node or null
2191 	  */
2192 	getLastChild: function(element) {throw "Not implemented"},
2193 
2194 	getNextSibling: function(node) {
2195 		while(node = node.nextSibling) {
2196 			if(node.nodeType != 3 || node.nodeValue.strip() != "") break;
2197 		}
2198 		return node;
2199 	},
2200 
2201 	getBottommostFirstChild: function(node) {
2202 		while(node.firstChild && node.nodeType == 1) node = node.firstChild;
2203 		return node;
2204 	},
2205 	
2206 	getBottommostLastChild: function(node) {
2207 		while(node.lastChild && node.nodeType == 1) node = node.lastChild;
2208 		return node;
2209 	},
2210 
2211 	/** @private */
2212 	_getCssValue: function(str, defaultUnit) {
2213 		if(!str || str.length == 0) return {value:0, unit:defaultUnit};
2214 		
2215 		var tokens = str.match(/(\d+)(.*)/);
2216 		return {
2217 			value:parseInt(tokens[1]),
2218 			unit:tokens[2] || defaultUnit
2219 		};
2220 	}
2221 });
2222 
2223 /**
2224  * Creates and returns instance of browser specific implementation.
2225  */
2226 xq.RichDom.createInstance = function() {
2227 	if(xq.Browser.isTrident) {
2228 		return new xq.RichDomTrident();
2229 	} else if(xq.Browser.isWebkit) {
2230 		return new xq.RichDomWebkit();
2231 	} else {
2232 		return new xq.RichDomGecko();
2233 	}
2234 }
2235