We use cookies to make our website more effective. By using our website you agree to our privacy policy.

Source: editing.js

/** editing.js is part of Aloha Editor project http://www.alohaeditor.org
 *
 * Aloha Editor ● JavaScript Content Editing Library
 * Copyright (c) 2010-2015 Gentics Software GmbH, Vienna, Austria.
 * Contributors http://www.alohaeditor.org/docs/contributing.html
 *
 * @TODO formatStyle: in the following case the outer "font-family:
 * arial" span should be removed.  Can be done similar to how
 * findReusableAncestor() works.
 *
 * <pre>
 *	<span style="font-family: arial">
 *	   <span style="font-family: times">one</span>
 *	   <span style="font-family: helvetica">two<span>
 *	</span>
 * </pre>
 *
 * @TODO better handling of the last <br/> in a block and generally of
 *      unrendered whitespace.
 *      For example:
 *      formatting
 *      <p>{some<br/>text<br/>}</p>
 *      will result in
 *      <p>{<b>some<br/>text<br/></b>}</p>
 *      while it should probably be
 *      <p>{<b>some</br>text</b>}<br/></p>
 *
 * @namespace editing
 */
define([
	'dom',
	'mutation',
	'boundaries',
	'arrays',
	'maps',
	'strings',
	'functions',
	'html',
	'html/elements',
	'stable-range',
	'cursors',
	'content',
	'lists',
	'links',
	'overrides'
], function (
	Dom,
	Mutation,
	Boundaries,
	Arrays,
	Maps,
	Strings,
	Fn,
	Html,
	HtmlElements,
	StableRange,
	Cursors,
	Content,
	Lists,
	Links,
	Overrides
) {
	'use strict';

	/**
	 * Walks the siblings of the given child, calling before for siblings before
	 * the given child, after for siblings after the given child, and at for the
	 * given child.
	 *
	 * @private
	 */
	function walkSiblings(parent, beforeAtAfterChild, before, at, after, arg) {
		var func = before;
		Dom.walk(parent.firstChild, function (child) {
			if (child !== beforeAtAfterChild) {
				func(child, arg);
			} else {
				func = after;
				at(child, arg);
			}
		});
	}

	/**
	 * Walks the siblings of each node in the given array (see
	 * walkSiblings()).
	 *
	 * @private
	 *
	 * @param ascendNodes from lowest descendant to topmost parent. The
	 * topmost parent and its siblings will not be walked over.
	 *
	 * @param atEnd indicates that the position to ascend from is not at
	 * ascendNodes[0], but at the end of ascendNodes[0] (meaning that
	 * all of ascendNodes[0]'s children will be walked over as well).
	 *
	 * @param carryDown is invoked on each node in the given array,
	 * allowing the carrying down of a context value. May return null
	 * to return the carryDown value from above.
	 */
	function ascendWalkSiblings(ascendNodes, atEnd, carryDown, before, at, after, arg) {
		var i;
		var args = [];
		i = ascendNodes.length;
		while (i--) {
			var cd = carryDown(ascendNodes[i], arg);
			if (null != cd) {
				arg = cd;
			}
			args.push(arg);
		}
		args.reverse();
		// Because with end positions like
		// <elem>text{</elem> or <elem>text}</elem>
		// ascendecending would start at <elem> ignoring "text".
		if (ascendNodes.length && atEnd) {
			Dom.walk(ascendNodes[0].firstChild, before, args[0]);
		}
		for (i = 0; i < ascendNodes.length - 1; i++) {
			var child = ascendNodes[i];
			var parent = ascendNodes[i + 1];
			walkSiblings(parent, child, before, at, after, args[i + 1]);
		}
	}

	function makePointNodeStep(pointNode, atEnd, stepOutsideInside, stepPartial) {
		// Because the start node is inside the range, the end node is
		// outside, and all ancestors of start and end are partially
		// inside/outside (for startAtEnd/endAtEnd positions the nodes are
		// also ancestors of the position).
		return function (node, arg) {
			if (node === pointNode && !atEnd) {
				stepOutsideInside(node, arg);
			} else {
				stepPartial(node, arg);
			}
		};
	}

	/**
	 * Walks the boundary of the range.
	 *
	 * The range's boundary starts at startContainer/startOffset, goes
	 * up to to the commonAncestorContainer's child above or equal
	 * startContainer/startOffset, continues to the
	 * commonAnestorContainer's child above or equal to
	 * endContainer/endOffset, and goes down again to
	 * endContainer/endOffset.
	 *
	 * Requires range's boundary points to be between nodes
	 * (Mutation.splitTextContainers).
	 *
	 * @private
	 */
	function walkBoundaryLeftRightInbetween(liveRange,
	                                        carryDown,
	                                        stepLeftStart,
	                                        stepRightStart,
	                                        stepLeftEnd,
	                                        stepRightEnd,
	                                        stepPartial,
	                                        stepInbetween,
	                                        arg) {
		// Because range may be mutated during traversal, we must only
		// refer to it before traversal.
		var cac = liveRange.commonAncestorContainer;
		if (Dom.isTextNode(cac)) {
			cac = cac.parentNode;
		}
		var sc         = liveRange.startContainer;
		var ec         = liveRange.endContainer;
		var so         = liveRange.startOffset;
		var eo         = liveRange.endOffset;
		var collapsed  = liveRange.collapsed;
		var start      = Dom.nodeAtOffset(sc, so);
		var end        = Dom.nodeAtOffset(ec, eo);
		var startAtEnd = Boundaries.isAtEnd(Boundaries.raw(sc, so));
		var endAtEnd   = Boundaries.isAtEnd(Boundaries.raw(ec, eo));

		var ascStart    = Dom.childAndParentsUntilNode(start, cac);
		var ascEnd      = Dom.childAndParentsUntilNode(end,   cac);
		var stepAtStart = makePointNodeStep(start, startAtEnd, stepRightStart, stepPartial);
		var stepAtEnd   = makePointNodeStep(end, endAtEnd, stepRightEnd, stepPartial);
		ascendWalkSiblings(ascStart, startAtEnd, carryDown, stepLeftStart, stepAtStart, stepRightStart, arg);
		ascendWalkSiblings(ascEnd, endAtEnd, carryDown, stepLeftEnd, stepAtEnd, stepRightEnd, arg);
		var cacChildStart = Arrays.last(ascStart);
		var cacChildEnd   = Arrays.last(ascEnd);
		stepAtStart = makePointNodeStep(start, startAtEnd, stepInbetween, stepPartial);
		Dom.walkUntilNode(cac.firstChild, stepLeftStart, cacChildStart, arg);
		if (cacChildStart) {
			var next = cacChildStart.nextSibling;
			if (cacChildStart === cacChildEnd) {
				if (!collapsed) {
					stepPartial(cacChildStart, arg);
				}
			} else {
				stepAtStart(cacChildStart, arg);
				Dom.walkUntilNode(next, stepInbetween, cacChildEnd, arg);
				if (cacChildEnd) {
					next = cacChildEnd.nextSibling;
					stepAtEnd(cacChildEnd, arg);
				}
			}
			if (cacChildEnd) {
				Dom.walk(next, stepRightEnd, arg);
			}
		}
	}

	/**
	 * Simplifies walkBoundaryLeftRightInbetween from left/right/inbetween to just inside/outside.
	 *
	 * Requires range's boundary points to be between nodes
	 * (Mutation.splitTextContainers).
	 */
	function walkBoundaryInsideOutside(liveRange,
	                                   carryDown,
	                                   stepOutside,
	                                   stepPartial,
	                                   stepInside,
	                                   arg) {
		walkBoundaryLeftRightInbetween(
			liveRange,
			carryDown,
			stepOutside,
			stepInside,
			stepInside,
			stepOutside,
			stepPartial,
			stepInside,
			arg
		);
	}

	/**
	 * Pushes down an implied context above or at pushDownFrom to the
	 * given range by clearing all overrides from pushDownFrom
	 * (inclusive) to range.commonAncestorContainer, and clearing all
	 * overrides inside and along the range's boundary (see
	 * walkBoundaryInsideOutside()), invoking pushDownOverride on all
	 * siblings of the range boundary that are not contained in it.
	 *
	 * Requires range's boundary points to be between nodes
	 * (Mutation.splitTextContainers).
	 */
	function pushDownContext(liveRange,
	                         pushDownFrom,
	                         cacOverride,
	                         getOverride,
	                         clearOverride,
	                         clearOverrideRec,
	                         pushDownOverride) {
		// Because range may be mutated during traversal, we must only
		// refer to it before traversal.
		var cac = liveRange.commonAncestorContainer;
		walkBoundaryInsideOutside(
			liveRange,
			getOverride,
			pushDownOverride,
			clearOverride,
			clearOverrideRec,
			cacOverride
		);
		var fromCacToTop = Dom.childAndParentsUntilInclNode(
			cac,
			pushDownFrom
		);
		ascendWalkSiblings(
			fromCacToTop,
			false,
			getOverride,
			pushDownOverride,
			clearOverride,
			pushDownOverride,
			null
		);
		clearOverride(pushDownFrom);
	}

	function findReusableAncestor(range,
	                              hasContext,
	                              getOverride,
	                              isUpperBoundary,
	                              isReusable,
	                              isObstruction) {
		var obstruction = null;
		function beforeAfter(node) {
			obstruction = (obstruction
						   || (!Html.isUnrenderedWhitespace(node)
							   && !hasContext(node)));
		}
		walkBoundaryInsideOutside(range, Fn.noop, beforeAfter, Fn.noop, Fn.noop);
		if (obstruction) {
			return null;
		}
		var cac = range.commonAncestorContainer;
		if (Dom.isTextNode(cac)) {
			cac = cac.parentNode;
		}
		function untilIncl(node) {
			// Because we prefer a node above the cac if possible.
			return (cac !== node && isReusable(node)) || isUpperBoundary(node) || isObstruction(node);
		}
		var cacToReusable = Dom.childAndParentsUntilIncl(cac, untilIncl);
		var reusable = Arrays.last(cacToReusable);
		if (!isReusable(reusable)) {
			// Because, although we preferred a node above the cac, we
			// fall back to the cac.
			return isReusable(cac) ? cac : null;
		}
		ascendWalkSiblings(cacToReusable, false, Fn.noop, beforeAfter, Fn.noop, beforeAfter);
		if (obstruction) {
			return isReusable(cac) ? cac : null;
		}
		return reusable;
	}

	/**
	 * Walks around the boundary of range and invokes the given
	 * functions with the nodes it encounters.
	 *
	 * clearOverride    - invoked for partially contained nodes.
	 * clearOverrideRec - invoked for top-level contained nodes.
	 * pushDownOverride - invoked for left siblings of ancestors
	 *   of startContainer[startOffset], and for right siblings of
	 *   ancestors of endContainer[endOffset].
	 * setContext       - invoked for top-level contained nodes.
	 *
	 * The purpose of the walk is to either push-down or set a context
	 * on all nodes within the range, and push-down any overrides that
	 * exist along the bounderies of the range.
	 *
	 * An override is a context that overrides the context to set.
	 *
	 * Pushing-down a context means that an existing context-giving
	 * ancestor element will be reused, if available, and setContext()
	 * will not be invoked.
	 *
	 * Pushing-down an override means that ancestors of the range's
	 * start or end containers will have their overrides cleared and the
	 * subset of the ancestors' children that is not contained by the
	 * range will have the override applied via pushDownOverride().
	 *
	 * This algorithm will not by itself mutate anything, or depend on
	 * any mutations by the given functions.
	 *
	 * clearOverride, clearOverideRec, setContext, pushDownContext may
	 * mutate the given node and it's previous siblings, and may insert
	 * nextSiblings, but must not mutate the next sibling of the given
	 * node, and must return the nextSibling of the given node (the
	 * nextSibling before any mutations).
	 *
	 * When setContext is invoked with hasOverrideAncestor, it is for
	 * example when a bold element is at the same time the upper
	 * boundary (for example when the bold element itself is the editing
	 * host) and an attempt is made to set a non-bold context inside the
	 * bold element. To work around this, setContext() could force a
	 * non-bold context by wrapping the node with a <span
	 * style="font-weight: normal">. See hasOverrideAncestor below.
	 *
	 * @param liveRange range's boundary points should be between nodes
	 * (Mutation.splitTextContainers).
	 *
	 * @param formatter a map with the following properties
	 *   isUpperBoundary(node) - identifies exclusive upper
	 *   boundary element, only elements below which will be modified.
	 *
	 *   getOverride(node) - returns a node's override, or null/undefined
	 *   if the node does not provide an override. The topmost node for
	 *   which getOverride returns a non-null value is the topmost
	 *   override. If there is a topmost override, and it is below the
	 *   upper boundary element, it will be cleared and pushed down.
	 *   Should return a non-null value for any node for which
	 *   hasContext(node) returns true.
	 *
	 *   clearOverride(node) - should clear the given node of an
	 *   override. The given node may or may not have an override
	 *   set. Will be invoked shallowly for all ancestors of start and end
	 *   containers (up to isUpperBoundary or hasContext). May perform
	 *   mutations as explained above.
	 *
	 *   clearOverrideRec(node) - like clearOverride but should clear
	 *   the override recursively. If not provided, clearOverride will
	 *   be applied recursively.
	 *
	 *   pushDownOverride(node, override) - applies the given
	 *   override to node. Should check whether the given node doesn't
	 *   already provide its own override, in which case the given
	 *   override should not be applied. May perform mutations as
	 *   explained above.
	 *
	 *   hasContext(node) - returns true if the given node
	 *   already provides the context to set.
	 *
	 *   setContext(node, override, hasOverrideAncestor) - applies the context
	 *   to the given node. Should clear overrides recursively. Should
	 *   also clear context recursively to avoid unnecessarily nested
	 *   contexts. hasOverrideAncestor is true if an override is in effect
	 *   above the given node (see explanation above). May perform
	 *   mutations as explained above.
	 */
	function mutate(liveRange, formatter) {
		// Because range may be mutated during traversal, we must only
		// refer to it before traversal.
		var cac = liveRange.commonAncestorContainer;
		var isUpperBoundary = formatter.isUpperBoundary;
		var getOverride = formatter.getOverride;
		var getInheritableOverride = formatter.getInheritableOverride;
		var pushDownOverride = formatter.pushDownOverride;
		var hasContext = formatter.hasContext;
		var hasInheritableContext = formatter.hasInheritableContext;
		var setContext = formatter.setContext;
		var clearOverride = formatter.clearOverride;
		var isObstruction = formatter.isObstruction;
		var isReusable = formatter.isReusable;
		var isContextOverride = formatter.isContextOverride;
		var isClearable = formatter.isClearable;
		var clearOverrideRec = formatter.clearOverrideRec  || function (node) {
			Dom.walkRec(node, clearOverride);
		};
		var topmostOverrideNode = null;
		var cacOverride = null;
		var isNonClearableOverride = false;
		var upperBoundaryAndAbove = false;
		var fromCacToContext = Dom.childAndParentsUntilIncl(
			cac,
			function (node) {
				// Because we shouldn't expect hasContext to handle the document
				// element (which has nodeType 9).
				return (
					!node.parentNode
						|| Dom.Nodes.DOCUMENT === node.parentNode.nodeType
							|| hasInheritableContext(node)
				);
			}
		);
		fromCacToContext.forEach(function (node) {
			upperBoundaryAndAbove = upperBoundaryAndAbove || isUpperBoundary(node);
			// Because we are only interested in non-context overrides.
			var override = getInheritableOverride(node);
			if (null != override && !isContextOverride(override)) {
				topmostOverrideNode = node;
				isNonClearableOverride = isNonClearableOverride
				                      || upperBoundaryAndAbove
				                      || !isClearable(node);
				if (null == cacOverride) {
					cacOverride = override;
				}
			}
		});
		if (null == cacOverride) {
			cacOverride = getInheritableOverride(cac);
		}

		if (hasInheritableContext(Arrays.last(fromCacToContext)) && !isNonClearableOverride) {
			if (!topmostOverrideNode) {
				// Because, if there is no override in the way, we only
				// need to clear the overrides contained in the range.
				walkBoundaryInsideOutside(
					liveRange,
					getOverride,
					pushDownOverride,
					clearOverride,
					clearOverrideRec
				);
			} else {
				var pushDownFrom = topmostOverrideNode;
				pushDownContext(
					liveRange,
					pushDownFrom,
					cacOverride,
					getOverride,
					clearOverride,
					clearOverrideRec,
					pushDownOverride
				);
			}
		} else {
			var mySetContext = function (node, override) {
				setContext(node, override, isNonClearableOverride);
			};
			var reusableAncestor = findReusableAncestor(
				liveRange,
				hasContext,
				getOverride,
				isUpperBoundary,
				isReusable,
				isObstruction
			);
			if (reusableAncestor) {
				mySetContext(reusableAncestor);
			} else {
				walkBoundaryInsideOutside(
					liveRange,
					getOverride,
					pushDownOverride,
					clearOverride,
					mySetContext
				);
			}
		}
	}

	function adjustPointWrap(point, node, wrapper) {
		// Because we prefer the range to be outside the wrapper (no
		// particular reason though).
		if (point.node === node && !point.atEnd) {
			point.node = wrapper;
		}
	}

	/**
	 * This function is missing documentation.
	 * @TODO Complete documentation.
	 *
	 * @memberOf editing
	 */
	function wrap(node, wrapper, leftPoint, rightPoint) {
		if (!Content.allowsNesting(wrapper.nodeName, node.nodeName)) {
			return false;
		}
		if (wrapper.parentNode) {
			Mutation.removeShallowPreservingCursors(wrapper, [leftPoint, rightPoint]);
		}
		adjustPointWrap(leftPoint, node, wrapper);
		adjustPointWrap(rightPoint, node, wrapper);
		Dom.wrap(node, wrapper);
		return true;
	}

	// NB: depends on fixupRange to use trimClosingOpening() to move the
	// leftPoint out of an cursor.atEnd position to the first node that is to be
	// moved.
	function moveBackIntoWrapper(node, ref, atEnd, leftPoint, rightPoint) {
		// Because the points will just be moved with the node, we don't need to
		// do any special preservation.
		Dom.insert(node, ref, atEnd);
	}

	/**
	 * TODO documentation
	 *
	 * @param  {!Range}   liveRange
	 * @param  {function} mutate
	 * @param  {function} trim
	 * @return {Array.<Boundary>}
	 */
	function fixupRange(liveRange, mutate, trim) {
		// Because we are mutating the range several times and don't want the
		// caller to see the in-between updates, and because we are using
		// Ranges.trim() below to adjust the range's boundary points, which we
		// don't want the browser to re-adjust (which some browsers do).
		var range = StableRange(liveRange);

		// Because making the assumption that boundary points are between nodes
		// makes the algorithms generally a bit simpler.
		Mutation.splitTextContainers(range);

		var splitStart = Cursors.cursorFromBoundaryPoint(
			range.startContainer,
			range.startOffset
		);

		var splitEnd = Cursors.cursorFromBoundaryPoint(
			range.endContainer,
			range.endOffset
		);

		// Because we want unbolding
		// <b>one<i>two{</i>three}</b>
		// to result in
		// <b>one<i>two</i></b>three
		// and not in
		// <b>one</b><i><b>two</b></i>three
		// even though that would be cleaned up in the restacking pass
		// afterwards.
		// Also, because moveBackIntoWrapper() requires the
		// left boundary point to be next to a non-ignorable node.
		if (false !== trim) {
			trimClosingOpening(
				range,
				Html.isUnrenderedWhitespace,
				Html.isUnrenderedWhitespace
			);
		}

		// Because mutation needs to keep track and adjust boundary points so we
		// can preserve the range.
		var leftPoint = Cursors.cursorFromBoundaryPoint(
			range.startContainer,
			range.startOffset
		);

		var rightPoint = Cursors.cursorFromBoundaryPoint(
			range.endContainer,
			range.endOffset
		);

		var formatter = mutate(range, leftPoint, rightPoint);
		if (formatter) {
			formatter.postprocess();
		}

		Cursors.setToRange(range, leftPoint, rightPoint);

		// Because we want to ensure that this algorithm doesn't
		// introduce any additional splits between text nodes.
		Mutation.joinTextNodeAdjustRange(splitStart.node, range);
		Mutation.joinTextNodeAdjustRange(splitEnd.node, range);

		if (formatter) {
			formatter.postprocessTextNodes(range);
		}

		var boundaries = Boundaries.fromRange(range);
		Boundaries.setRange(liveRange, boundaries[0], boundaries[1]);
		return boundaries;
	}

	function restackRec(node, hasContext, ignoreHorizontal, ignoreVertical) {
		if (!Dom.isElementNode(node) || !ignoreVertical(node)) {
			return null;
		}
		var maybeContext = Dom.nextWhile(node.firstChild, ignoreHorizontal);
		if (!maybeContext) {
			return null;
		}
		var notIgnorable = Dom.nextWhile(maybeContext.nextSibling, ignoreHorizontal);
		if (notIgnorable) {
			return null;
		}
		if (hasContext(maybeContext)) {
			return maybeContext;
		}
		return restackRec(maybeContext, hasContext, ignoreHorizontal, ignoreVertical);
	}

	function restack(node, hasContext, ignoreHorizontal, ignoreVertical, leftPoint, rightPoint) {
		function myIgnoreHorizontal(node) {
			return !hasContext(node) && ignoreHorizontal(node);
		}
		if (hasContext(node)) {
			return node;
		}
		var context = restackRec(node, hasContext, myIgnoreHorizontal, ignoreVertical);
		if (!context) {
			return null;
		}
		if (!wrap(node, context, leftPoint, rightPoint)) {
			return null;
		}
		return context;
	}

	function ensureWrapper(node,
	                       createWrapper,
	                       isWrapper,
	                       isMergable,
	                       pruneContext,
	                       addContextValue,
	                       leftPoint,
	                       rightPoint) {
		var sibling = node.previousSibling;
		if (sibling && isMergable(sibling) && isMergable(node)) {
			moveBackIntoWrapper(node, sibling, true, leftPoint, rightPoint);
			// Because the node itself may be a wrapper.
			pruneContext(node);
		} else if (!isWrapper(node)) {
			var wrapper = createWrapper(node.ownerDocument);
			if (wrap(node, wrapper, leftPoint, rightPoint)) {
				// Because we are just making sure (probably not
				// necessary since the node isn't a wrapper).
				pruneContext(node);
			} else {
				// Because if wrapping is not successful, we try again
				// one level down.
				Dom.walk(node.firstChild, function (node) {
					ensureWrapper(
						node,
						createWrapper,
						isWrapper,
						isMergable,
						pruneContext,
						addContextValue,
						leftPoint,
						rightPoint
					);
				});
			}
		} else {
			// Because the node itself is a wrapper, but possibly not
			// with the given context value.
			addContextValue(node);
		}
	}

	function makeFormatter(contextValue, leftPoint, rightPoint, impl) {
		var hasContext = impl.hasContext;
		var isContextOverride = impl.isContextOverride;
		var hasSomeContextValue = impl.hasSomeContextValue;
		var hasContextValue = impl.hasContextValue;
		var addContextValue = impl.addContextValue;
		var removeContext = impl.removeContext;
		var createWrapper = impl.createWrapper;
		var isReusable = impl.isReusable;
		var isPrunable = impl.isPrunable;

		// Because we want to optimize reuse, we remembering any wrappers we created.
		var wrappersByContextValue = {};
		var wrappersWithContextValue = [];
		var removedNodeSiblings = [];

		function pruneContext(node) {
			if (!hasSomeContextValue(node)) {
				return;
			}
			removeContext(node);
			// TODO if the node is not prunable but overrides the
			// context (for example <b class="..."></b> may not be
			// prunable), we should descend into the node and set the
			// unformatting-context inside.
			if (!isPrunable(node)) {
				return;
			}
			if (node.previousSibling) {
				removedNodeSiblings.push(node.previousSibling);
			}
			if (node.nextSibling) {
				removedNodeSiblings.push(node.nextSibling);
			}
			Mutation.removeShallowPreservingCursors(node, [leftPoint, rightPoint]);
		}

		function createContextWrapper(value, doc) {
			var wrapper = createWrapper(value, doc);
			var key = ':' + value;
			var wrappers = wrappersByContextValue[key] = wrappersByContextValue[key] || [];
			wrappers.push(wrapper);
			wrappersWithContextValue.push([wrapper, value]);
			return wrapper;
		}

		function isClearable(node) {
			var clone = node.cloneNode(false);
			removeContext(clone);
			return isPrunable(clone);
		}

		function isMergableWrapper(value, node) {
			if (!isReusable(node)) {
				return false;
			}
			var key =  ':' + value;
			var wrappers = wrappersByContextValue[key] || [];
			if (Arrays.contains(wrappers, node)) {
				return true;
			}
			if (hasSomeContextValue(node) && !hasContextValue(node, value)) {
				return false;
			}
			// Because we assume something is mergeable if it doesn't
			// provide any context value besides the one we are
			// applying, and something doesn't provide any context value
			// at all if it is prunable.
			return isClearable(node);
		}

		function wrapContextValue(node, value) {
			ensureWrapper(
				node,
				Fn.partial(createContextWrapper, value),
				isReusable,
				Fn.partial(isMergableWrapper, value),
				pruneContext,
				Fn.partial(addContextValue, value),
				leftPoint,
				rightPoint
			);
		}

		function clearOverride(node) {
			// Because we don't want to remove any existing context if
			// not necessary (See pushDownOverride and setContext).
			if (!hasContext(node)) {
				pruneContext(node);
			}
		}

		function clearOverrideRecStep(node) {
			// Different from clearOverride because clearOverride() only
			// clears context overrides, while during a recursive
			// clearing we want to clear the override always regardless
			// of whether it is equal to context.
			pruneContext(node);
		}

		function clearOverrideRec(node) {
			Dom.walkRec(node, clearOverrideRecStep);
		}

		function pushDownOverride(node, override) {
			// Because we don't clear any context overrides, we don't
			// need to push them down either.
			if (null == override || hasSomeContextValue(node) || isContextOverride(override)) {
				return;
			}
			wrapContextValue(node, override);
		}

		function setContext(node, override, isNonClearableOverride) {
			// Because we don't clear any context overrides, we don't
			// need to set them either.
			if (isContextOverride(override)) {
				return;
			}
			Dom.walk(node.firstChild, clearOverrideRec);
			wrapContextValue(node, contextValue);
		}

		function restackMergeWrapper(wrapper, contextValue, mergeNext) {
			var sibling = mergeNext ? wrapper.nextSibling : wrapper.previousSibling;
			if (!sibling) {
				return;
			}
			function isGivenContextValue(node) {
				return hasContextValue(node, contextValue);
			}
			sibling = restack(
				sibling,
				isGivenContextValue,
				Html.isUnrenderedWhitespace,
				Html.hasInlineStyle,
				leftPoint,
				rightPoint
			);
			if (!sibling) {
				return;
			}
			var isMergable = Fn.partial(isMergableWrapper, contextValue);
			var createWrapper = Fn.partial(createContextWrapper, contextValue);
			var addValue = Fn.partial(addContextValue, contextValue);
			var mergeNode = mergeNext ? sibling : wrapper;
			ensureWrapper(
				mergeNode,
				createWrapper,
				isReusable,
				isMergable,
				pruneContext,
				addValue,
				leftPoint,
				rightPoint
			);
		}

		function mergeWrapper(wrapper, contextValue) {
			restackMergeWrapper(wrapper, contextValue, true);
			restackMergeWrapper(wrapper, contextValue, false);
		}

		function postprocess() {
			wrappersWithContextValue.forEach(function (wrapperAndContextValue) {
				mergeWrapper(wrapperAndContextValue[0], wrapperAndContextValue[1]);
			});
		}

		function postprocessTextNodes(range) {
			removedNodeSiblings.forEach(function (node) {
				Mutation.joinTextNodeAdjustRange(node, range);
			});
		}

		return {
			hasContext: hasContext,
			isReusable: isReusable,
			clearOverride: clearOverride,
			isClearable: isClearable,
			pushDownOverride: pushDownOverride,
			setContext: setContext,
			isContextOverride: isContextOverride,
			postprocess: postprocess,
			postprocessTextNodes: postprocessTextNodes,
			hasInheritableContext: impl.hasInheritableContext,
			isObstruction: impl.isObstruction,
			getOverride: impl.getOverride,
			getInheritableOverride: impl.getInheritableOverride,
			isUpperBoundary: impl.isUpperBoundary
		};
	}

	function isUpperBoundary_default(node) {
		// Because the body element is an obvious upper boundary, and
		// because, if we are inside a block element, we shouldn't touch
		// it as that causes changes in the layout, and because, when we
		// are inside an editable, we shouldn't make modifications
		// outside of it (if we are not inside an editable, we don't
		// care).
		return 'BODY' === node.nodeName || Html.hasBlockStyle(node) || Dom.isEditingHost(node);
	}

	function isStyleEqual_default(styleValueA, styleValueB) {
		return styleValueA === styleValueB;
	}

	var inlineWrapperProperties = {
		underline: {
			name: 'U',
			nodes: ['U'],
			style: 'text-decoration',
			value: 'underline',
			normal: 'none',
			normalize: {}
		},
		bold: {
			name: 'B',
			nodes: ['B', 'STRONG'],
			style: 'font-weight',
			value: 'bold',
			normal: 'normal',
			normalize: {
				/* ie7/ie8 only */
				'700': 'bold',
				'400': 'normal'
			}
		},
		italic: {
			name: 'I',
			nodes: ['I', 'EM'],
			style: 'font-style',
			value: 'italic',
			normal: 'normal',
			normalize: {}
		}
	};
	inlineWrapperProperties['emphasis']  = Maps.merge(inlineWrapperProperties.italic, {name: 'EM'});
	inlineWrapperProperties['strong']    = Maps.merge(inlineWrapperProperties.bold, {name: 'STRONG'});
	inlineWrapperProperties['bold']      = inlineWrapperProperties.bold;
	inlineWrapperProperties['italic']    = inlineWrapperProperties.italic;
	inlineWrapperProperties['underline'] = inlineWrapperProperties.underline;

	function getStyleSafely(node, name) {
		return (Dom.isElementNode(node)
		        ? Dom.getStyle(node, name)
		        : null);
	}

	function makeStyleFormatter(styleName, styleValue, leftPoint, rightPoint, opts) {
		var isStyleEqual = opts.isStyleEqual || isStyleEqual_default;
		var nodeNames = [];
		var unformat = false;
		var wrapperProps = inlineWrapperProperties[styleName];
		if (wrapperProps) {
			nodeNames = wrapperProps.nodes;
			styleName = wrapperProps.style;
			unformat = !styleValue;
			styleValue = unformat ? wrapperProps.normal : wrapperProps.value;
		}
		function normalizeStyleValue(value) {
			if (wrapperProps && wrapperProps.normalize[value]) {
				value = wrapperProps.normalize[value];
			}
			return value;
		}
		function getOverride(node) {
			if (Arrays.contains(nodeNames, node.nodeName)) {
				return wrapperProps.value;
			}
			var override = getStyleSafely(node, styleName);
			return !Strings.isEmpty(override) ? override : null;
		}
		function getInheritableOverride(node) {
			if (Arrays.contains(nodeNames, node.nodeName)) {
				return wrapperProps.value;
			}
			var override = Dom.getComputedStyle(node, styleName);
			return !Strings.isEmpty(override) ? override : null;
		}
		function isContextStyle(value) {
			return isStyleEqual(normalizeStyleValue(value), styleValue);
		}
		function isContextOverride(value) {
			return isContextStyle(value);
		}
		function hasSomeContextValue(node) {
			if (Arrays.contains(nodeNames, node.nodeName)) {
				return true;
			}
			return !Strings.isEmpty(getStyleSafely(node, styleName));
		}
		function hasContextValue(node, value) {
			value = normalizeStyleValue(value);
			if (Arrays.contains(nodeNames, node.nodeName) && isStyleEqual(wrapperProps.value, value)) {
				return true;
			}
			return isStyleEqual(getStyleSafely(node, styleName), value);
		}
		function hasContext(node) {
			if (!unformat && Arrays.contains(nodeNames, node.nodeName)) {
				return true;
			}
			return isContextStyle(getStyleSafely(node, styleName));
		}
		function hasInheritableContext(node) {
			if (!unformat && Arrays.contains(nodeNames, node.nodeName)) {
				return true;
			}
			if (unformat && 'BODY' === node.nodeName) {
				return true;
			}
			// Because default values of not-inherited styles don't
			// provide any context.
			// TODO This causes any classes that set a non-inherited
			// style to the default value, for example
			// "text-decoration: none" to be ignored.
			if (unformat && Html.isStyleInherited(styleName)) {
				return isContextStyle(getStyleSafely(node, styleName));
			}
			return isContextStyle(Dom.getComputedStyle(node, styleName));
		}
		function addContextValue(value, node) {
			value = normalizeStyleValue(value);
			if (Arrays.contains(nodeNames, node.nodeName) && isStyleEqual(wrapperProps.value, value)) {
				return;
			}
			// Because we don't want to add an explicit style if for
			// example the element already has a class set on it. For
			// example: <span class="bold"></span>.
			if (isStyleEqual(normalizeStyleValue(Dom.getComputedStyle(node, styleName)), value)) {
				return;
			}
			Dom.setStyle(node, styleName, value);
		}
		function removeContext(node) {
			Dom.removeStyle(node, styleName);
		}
		function isReusable(node) {
			if (Arrays.contains(nodeNames, node.nodeName)) {
				return true;
			}
			return 'SPAN' === node.nodeName;
		}
		function isPrunable(node) {
			return isReusable(node) && !Dom.hasAttrs(node);
		}
		function createWrapper(value, doc) {
			value = normalizeStyleValue(value);
			if (wrapperProps && isStyleEqual(wrapperProps.value, value)) {
				return doc.createElement(wrapperProps.name);
			}
			var wrapper = doc.createElement('SPAN');
			Dom.setStyle(wrapper, styleName, value);
			return wrapper;
		}
		var impl = Maps.merge({
			getOverride: getOverride,
			getInheritableOverride: getInheritableOverride,
			hasContext: hasContext,
			hasInheritableContext: hasInheritableContext,
			isContextOverride: isContextOverride,
			hasSomeContextValue: hasSomeContextValue,
			hasContextValue: hasContextValue,
			addContextValue: addContextValue,
			removeContext: removeContext,
			isPrunable: isPrunable,
			isStyleEqual: isStyleEqual,
			createWrapper: createWrapper,
			isReusable: isReusable,
			isObstruction: Fn.complement(Html.hasInlineStyle),
			isUpperBoundary: isUpperBoundary_default
		}, opts);
		return makeFormatter(styleValue, leftPoint, rightPoint, impl);
	}

	function makeElemFormatter(nodeName, unformat, leftPoint, rightPoint, opts) {
		// Because we assume nodeNames are always uppercase, but don't
		// want the user to remember this detail.
		nodeName = nodeName.toUpperCase();
		function createWrapper(wrapper, doc) {
			return doc.createElement(nodeName);
		}
		function getOverride(node) {
			return nodeName === node.nodeName || null;
		}
		function hasContext(node) {
			if (unformat) {
				// Because unformatting has no context value.
				return false;
			}
			return nodeName === node.nodeName;
		}
		function hasInheritableContext(node) {
			// Because there can be no nodes above the body element that
			// can provide a context.
			if (unformat && 'BODY' === node.nodeName) {
				return true;
			}
			return hasContext(node);
		}
		function isContextOverride(value) {
			if (unformat) {
				// Because unformatting has no context value.
				return false;
			}
			return null != value;
		}
		function isReusable(node) {
			// Because we don't want to merge with a context node that
			// does more than just provide a context (for example a <b>
			// node may have a class which shouldn't also being wrapped
			// around the merged-with node).
			return node.nodeName === nodeName && !Dom.hasAttrs(node);
		}
		function isPrunable(node) {
			return isReusable(node);
		}
		function hasSomeContextValue(node) {
			return node.nodeName === nodeName;
		}
		var impl = Maps.merge({
			getOverride: getOverride,
			// Because inheritable overrides are only useful for
			// formatters that consider the CSS style.
			getInheritableOverride: getOverride,
			hasContext: hasContext,
			hasInheritableContext: hasInheritableContext,
			isContextOverride: isContextOverride,
			hasSomeContextValue: hasSomeContextValue,
			// Because hasContextValue and hasSomeContextValue makes no
			// difference for an element formatter, since there is only one
			// context value.
			hasContextValue: hasSomeContextValue,
			addContextValue: Fn.noop,
			removeContext: Fn.noop,
			createWrapper: createWrapper,
			isReusable: isReusable,
			isPrunable: isPrunable,
			isObstruction: Fn.complement(Html.hasInlineStyle),
			isUpperBoundary: isUpperBoundary_default
		}, opts);
		return makeFormatter(nodeName, leftPoint, rightPoint, impl);
	}

	/**
	 * Ensures the given range is wrapped by elements with a given nodeName.
	 *
	 * @param {string}    nodeName
	 * @param {!Boundary} start
	 * @param {!Boundary} end
	 * @param {boolean} remove Optional flag, which when set to false will cause
	 *                         the given markup to be removed (unwrapped) rather
	 *                         then set.
	 * @param {?Object} opts A map of options (all optional):
	 *        createWrapper - a function that returns a new empty
	 *        wrapper node to use.
	 *
	 *        isReusable - a function that returns true if a given node,
	 *        already in the DOM at the correct place, can be reused
	 *        instead of creating a new wrapper node. May be merged with
	 *        other reusable or newly created wrapper nodes.
	 * @return {Array.<Boundary>} updated boundaries
	 */
	function wrapElem(nodeName, start, end, remove, opts) {
		opts = opts || {};

		var liveRange = Boundaries.range(start, end);

		// Because we should avoid splitTextContainers() if this call is a noop.
		if (liveRange.collapsed) {
			return [start, end];
		}

		return fixupRange(liveRange, function (range, leftPoint, rightPoint) {
			var formatter = makeElemFormatter(nodeName, remove, leftPoint, rightPoint, opts);
			mutate(range, formatter);
			return formatter;
		});
	}

	/**
	 * Ensures the contents between start and end are wrapped by elements 
	 * that have a given CSS style set. Returns the updated boundaries.
	 *
	 * @param styleName a CSS style name
	 *        Please note that not-inherited styles currently may (or
	 *        may not) cause undesirable results.  See also
	 *        Html.isStyleInherited().
	 *
	 *        The underline style can't be unformatted inside a
	 *        non-clearable ancestor ("text-decoration: none" doesn't do
	 *        anything as the underline will be drawn by the ancestor).
	 *
	 * @param opts all options supported by wrapElem() as well as the following:
	 *        createWrapper - a function that takes a style value and
	 *        returns a new empty wrapper node that has the style value
	 *        applied.
	 *
	 *        isPrunable - a function that returns true if a given node,
	 *        after some style was removed from it, can be removed
	 *        entirely. That's usually the case if the given node is
	 *        equivalent to an empty wrapper.
	 *
	 *        isStyleEqual - a function that returns true if two given
	 *        style values are equal.
	 *        TODO currently we just use strict equals by default, but
	 *             we should implement for each supported style it's own
	 *             equals function.
	 * @return {Array.<Boundary>}
	 * @memberOf editing
	 */
	function style(start, end, name, value, opts) {
		var range = Boundaries.range(start, end);
		// Because we should avoid splitTextContainers() if this call is a noop.
		if (range.collapsed) {
			return [start, end];
		}
		return fixupRange(range, function (range, leftPoint, rightPoint) {
			var formatter = makeStyleFormatter(name, value, leftPoint, rightPoint, opts || {});
			mutate(range, formatter);
			return formatter;
		});
	}

	/**
	 * Applies inline formatting to contents enclosed by start and end boundary.
	 * Will return updated array of boundaries after the operation.
	 *
	 * @private
	 * @param  {string}    node
	 * @param  {!Boundary} start
	 * @param  {!Boundary} end
	 * @param  {boolean}   isWrapping
	 * @return {Array.<Boundary>}
	 */
	function formatInline(node, start, end, isWrapping) {
		var styleName = resolveStyleName(node);
		var boundaries = (styleName === false)
		               ? [start, end]
		               : style(start, end, styleName, isWrapping);
		if (Boundaries.equals(boundaries[0], boundaries[1])) {
			return boundaries;
		}
		var next = Boundaries.nodeAfter(boundaries[0]);
		var prev = Boundaries.nodeBefore(boundaries[1]);
		start = next
		      ? Boundaries.normalize(Boundaries.fromStartOfNode(next))
		      : boundaries[0];
		end = prev
		    ? Boundaries.normalize(Boundaries.fromEndOfNode(prev))
		    : boundaries[1];
		return [start, end];
	}

	/**
	 * Resolves the according CSS style name for an uppercase (!) node name
	 * passed in styleNode. Will return the CSS name of the style (eg. 'bold') 
	 * or false.
	 * So 'B' will eg. be resolved to 'bold'
	 *
	 * @param  {string} styleNode
	 * @return {string|false}
	 */
	function resolveStyleName(styleNode) {
		for (var styleName in inlineWrapperProperties) {
			if (inlineWrapperProperties[styleName].nodes.indexOf(styleNode) !== -1) {
				return styleName;
			}
		}
		return false;
	}

	/**
	 * Ensures that the given start point Cursor is not at a "start position"
	 * and the given end point Cursor is not at an "end position" by moving the
	 * points to the left and right respectively.  This is effectively the
	 * opposite of trimBoundaries().
	 *
	 * @param {Cusor} start
	 * @param {Cusor} end
	 * @param {function():boolean} until
	 *        Optional predicate.  May be used to stop the trimming process from
	 *        moving the Cursor from within an element outside of it.
	 * @param {function():boolean} ignore
	 *        Optional predicate.  May be used to ignore (skip)
	 *        following/preceding siblings which otherwise would stop the
	 *        trimming process, like for example underendered whitespace.
	 */
	function expandBoundaries(start, end, until, ignore) {
		until = until || Fn.returnFalse;
		ignore = ignore || Fn.returnFalse;
		start.prevWhile(function (start) {
			var prevSibling = start.prevSibling();
			return prevSibling ? ignore(prevSibling) : !until(start.parent());
		});
		end.nextWhile(function (end) {
			return !end.atEnd ? ignore(end.node) : !until(end.parent());
		});
	}

	/**
	 * Ensures that the given start point Cursor is not at an "start position"
	 * and the given end point Cursor is not at an "end position" by moving the
	 * points to the left and right respectively.  This is effectively the
	 * opposite of expandBoundaries().
	 *
	 * If the boundaries are equal (collapsed), or become equal during this
	 * operation, or if until() returns true for either point, they may remain
	 * in start and end position respectively.
	 *
	 * @param {Cusor} start
	 * @param {Cusor} end
	 * @param {function():boolean} until
	 *        Optional predicate.  May be used to stop the trimming process from
	 *        moving the Cursor from within an element outside of it.
	 * @param {function():boolean} ignore
	 *        Optional predicate.  May be used to ignore (skip)
	 *        following/preceding siblings which otherwise would stop the
	 *        trimming process, like for example underendered whitespace.
	 */
	function trimBoundaries(start, end, until, ignore) {
		until = until || Fn.returnFalse;
		ignore = ignore || Fn.returnFalse;
		start.nextWhile(function (start) {
			return (
				!start.equals(end)
					&& (
						!start.atEnd
							? ignore(start.node)
							: !until(start.parent())
					)
			);
		});
		end.prevWhile(function (end) {
			var prevSibling = end.prevSibling();
			return (
				!start.equals(end)
					&& (
						prevSibling
							? ignore(prevSibling)
							: !until(end.parent())
					)
			);
		});
	}

	/**
	 * Ensures that the given boundaries are neither in start nor end positions.
	 * In other words, after this operation, both will have preceding and
	 * following siblings.
	 *
	 * Expansion/trimming can be controlled via expandUntil and trimUntil, but
	 * may cause one or both of the boundaries to remain in start or end
	 * position.
	 */
	function trimExpandBoundaries(startPoint, endPoint, trimUntil, expandUntil, ignore) {
		var collapsed = startPoint.equals(endPoint);
		trimBoundaries(startPoint, endPoint, trimUntil, ignore);
		expandBoundaries(startPoint, endPoint, expandUntil, ignore);
		if (collapsed) {
			endPoint.setFrom(startPoint);
		}
	}

	/**
	 * Seekts a boundary point.
	 *
	 * @private
	 */
	function seekBoundaryPoint(range, container, offset, oppositeContainer,
	                           oppositeOffset, setFn, ignore, backwards) {
		var cursor = Cursors.cursorFromBoundaryPoint(container, offset);

		// Because when seeking backwards, if the boundary point is inside a
		// text node, trimming starts after it. When seeking forwards, the
		// cursor starts before the node, which is what
		// cursorFromBoundaryPoint() does automatically.
		if (backwards
				&& Dom.isTextNode(container)
					&& offset > 0
						&& offset < container.length) {
			if (cursor.next()) {
				if (!ignore(cursor)) {
					return range;
				}
				// Bacause the text node can be ignored, we go back to the
				// initial position.
				cursor.prev();
			}
		}
		var opposite = Cursors.cursorFromBoundaryPoint(
			oppositeContainer,
			oppositeOffset
		);
		var changed = false;
		while (!cursor.equals(opposite)
		           && ignore(cursor)
		           && (backwards ? cursor.prev() : cursor.next())) {
			changed = true;
		}
		if (changed) {
			setFn(range, cursor);
		}
		return range;
	}


	/**
	 * Starting with the given range's start and end boundary points, seek
	 * inward using a cursor, passing the cursor to ignoreLeft and ignoreRight,
	 * stopping when either of these returns true, adjusting the given range to
	 * the end positions of both cursors.
	 *
	 * The dom cursor passed to ignoreLeft and ignoreRight does not traverse
	 * positions inside text nodes. The exact rules for when text node
	 * containers are passed are as follows: If the left boundary point is
	 * inside a text node, trimming will start before it. If the right boundary
	 * point is inside a text node, trimming will start after it.
	 * ignoreLeft/ignoreRight() are invoked with the cursor before/after the
	 * text node that contains the boundary point.
	 *
	 * @todo: Implement in terms of boundaries
	 *
	 * @param  {Range}     range
	 * @param  {function=} ignoreLeft
	 * @param  {function=} ignoreRight
	 * @return {Range}
	 */
	function trim(range, ignoreLeft, ignoreRight) {
		ignoreLeft = ignoreLeft || Fn.returnFalse;
		ignoreRight = ignoreRight || Fn.returnFalse;
		if (range.collapsed) {
			return range;
		}
		// Because range may be mutated, we must store its properties before
		// doing anything else.
		var sc = range.startContainer;
		var so = range.startOffset;
		var ec = range.endContainer;
		var eo = range.endOffset;
		seekBoundaryPoint(
			range,
			sc,
			so,
			ec,
			eo,
			Cursors.setRangeStart,
			ignoreLeft,
			false
		);
		sc = range.startContainer;
		so = range.startOffset;
		seekBoundaryPoint(
			range,
			ec,
			eo,
			sc,
			so,
			Cursors.setRangeEnd,
			ignoreRight,
			true
		);
		return range;
	}

	/**
	 * Like trim() but ignores closing (to the left) and opening positions (to
	 * the right).
	 *
	 * @param  {Range}     range
	 * @param  {function=} ignoreLeft
	 * @param  {function=} ignoreRight
	 * @return {Range}
	 */
	function trimClosingOpening(range, ignoreLeft, ignoreRight) {
		ignoreLeft = ignoreLeft || Fn.returnFalse;
		ignoreRight = ignoreRight || Fn.returnFalse;
		trim(range, function (cursor) {
			return cursor.atEnd || ignoreLeft(cursor.node);
		}, function (cursor) {
			return !cursor.prevSibling() || ignoreRight(cursor.prevSibling());
		});
		return range;
	}

	function splitBoundaryPoint(node, atEnd, leftPoint, rightPoint, removeEmpty, opts) {
		var wrapper = null;

		function carryDown(elem, stop) {
			return stop || opts.until(elem);
		}

		function intoWrapper(node, stop) {
			if (stop) {
				return;
			}
			var parent = node.parentNode;
			if (!wrapper || parent.previousSibling !== wrapper) {
				wrapper = opts.clone(parent);
				removeEmpty.push(parent);
				Dom.insert(wrapper, parent, false);
				if (leftPoint.node === parent && !leftPoint.atEnd) {
					leftPoint.node = wrapper;
				}
				if (rightPoint.node === parent) {
					rightPoint.node = wrapper;
				}
			}
			moveBackIntoWrapper(node, wrapper, true, leftPoint, rightPoint);
		}

		var ascend = Dom.childAndParentsUntilIncl(node, opts.below);
		var unsplitParent = ascend.pop();
		if (unsplitParent && opts.below(unsplitParent)) {
			ascendWalkSiblings(ascend, atEnd, carryDown, intoWrapper, Fn.noop, Fn.noop);
		}

		return unsplitParent;
	}

	/**
	 * Tries to move the given boundary to the start of line, skipping over any
	 * unrendered nodes, or if that fails to the end of line (after a br
	 * element if present), and for the last line in a block, to the very end
	 * of the block.
	 *
	 * If the selection is inside a block with only a single empty line (empty
	 * except for unrendered nodes), and both boundary points are normalized,
	 * the selection will be collapsed to the start of the block.
	 *
	 * For some operations it's useful to think of a block as a number of
	 * lines, each including its respective br and any preceding unrendered
	 * whitespace and in case of the last line, also any following unrendered
	 * whitespace.
	 *
	 * @param  {!Cursor} point
	 * @return {boolean} True if the cursor is moved.
	 */
	function normalizeBoundary(point) {
		if (HtmlElements.skipUnrenderedToStartOfLine(point)) {
			return true;
		}
		if (!HtmlElements.skipUnrenderedToEndOfLine(point)) {
			return false;
		}
		if ('BR' === point.node.nodeName) {
			point.skipNext();
			// Because, if this is the last line in a block, any unrendered
			// whitespace after the last br will not constitute an independent
			// line, and as such we must include it in the last line.
			var endOfBlock = point.clone();
			if (HtmlElements.skipUnrenderedToEndOfLine(endOfBlock) && endOfBlock.atEnd) {
				point.setFrom(endOfBlock);
			}
		}
		return true;
	}

	function splitRangeAtBoundaries(range, left, right, opts) {
		var normalizeLeft = opts.normalizeRange ? left : left.clone();
		var normalizeRight = opts.normalizeRange ? right : right.clone();
		normalizeBoundary(normalizeLeft);
		normalizeBoundary(normalizeRight);

		Cursors.setToRange(range, normalizeLeft, normalizeRight);

		var removeEmpty = [];
		var start = Dom.nodeAtOffset(range.startContainer, range.startOffset);
		var end = Dom.nodeAtOffset(range.endContainer, range.endOffset);
		var startAtEnd = Boundaries.isAtEnd(Boundaries.raw(range.startContainer, range.startOffset));
		var endAtEnd = Boundaries.isAtEnd(Boundaries.raw(range.endContainer, range.endOffset));
		var unsplitParentStart = splitBoundaryPoint(start, startAtEnd, left, right, removeEmpty, opts);
		var unsplitParentEnd = splitBoundaryPoint(end, endAtEnd, left, right, removeEmpty, opts);

		removeEmpty.forEach(function (elem) {
			// Because we may end up cloning the same node twice (by splitting
			// both start and end points)
			if (!elem.firstChild && elem.parentNode) {
				Mutation.removeShallowPreservingCursors(elem, [left, right]);
			}
		});

		if (opts.normalizeRange) {
			trimExpandBoundaries(left, right, null, function (node) {
				return node === unsplitParentStart || node === unsplitParentEnd;
			});
		}
	}

	/**
	 * Splits the ancestors above the given range's start and end points.
	 *
	 * @param opts a map of options (all optional):
	 *
	 *        clone - a function that clones a given element node
	 *        shallowly and returns the cloned node.
	 *
	 *        until - a function that returns true if splitting
	 *        should stop at a given node (exclusive) below the topmost
	 *        node for which below() returns true. By default all
	 *        nodes are split.
	 *
	 *        below - a function that returns true if descendants
	 *        of a given node can be split. Used to determine the
	 *        topmost node at which to end the splitting process. If
	 *        false is returned for all ancestors of the start and end
	 *        points of the range, nothing will be split. By default,
	 *        returns true for an editing host.
	 *
	 *        normalizeRange - a boolean, defaults to true.
	 *        After splitting the selection may still be inside the split
	 *        nodes, for example after splitting the DOM may look like
	 *
	 *        <b>1</b><b>\{2</b><i>3</i><i>\}4</i>
	 *
	 *	      If normalizeRange is true, the selection is trimmed to
	 *	      correct <i>\}4</i> and expanded to correct <b>\{2</b>, such
	 *        that it will look like
	 *
	 *	      <b>1</b>\{<b>2</b><i>3</i>\}<i>4</i>
	 *
	 *	      This should make both start and end points children of the
	 *        same cac which is going to be the topmost unsplit node. This
	 *        is usually what one expects the range to look like after a
	 *        split.
	 *        NB: if splitUntil() returns true, start and end points
	 *        may not become children of the topmost unsplit node. Also,
	 *        if splitUntil() returns true, the selection may be moved
	 *        out of an unsplit node which may be unexpected.
	 * @return {Array.<Boundary>}
	 */
	function split(liveRange, opts) {
		opts = opts || {};

		opts = Maps.merge({
			clone: Dom.cloneShallow,
			until: Fn.returnFalse,
			below: Dom.isEditingHost,
			normalizeRange: true
		}, opts);

		return fixupRange(liveRange, function (range, left, right) {
			splitRangeAtBoundaries(range, left, right, opts);
			return null;
		});
	}

	/**
	 * Removes the content inside the given range.
	 *
	 * “If you delete a paragraph-boundary, the result seems to be consistent:
	 * The leftmost block 'wins', and the content of the rightmost block is
	 * included in the leftmost:
	 *
	 * <h1>Overskrift</h1><p>[]Text</p>
	 *
	 * “If delete is pressed, this is the result:
	 *
	 * <h1>Overskrift[]Text</h1>”
	 * -- http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-1
	 *
	 * TODO:
	 * put &nbsp; at beginning and end position in order to preserve spaces at
	 * these locations when deleting.
	 *
	 * @see https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#deleting-the-selection
	 *
	 * @param  {!Boundary} start
	 * @param  {!Boundary} end
	 * @return {Array.<Boundary>}
	 * @memberOf editing
	 */
	function remove(start, end) {
		var range = Boundaries.range(start, end);
		return fixupRange(range, function (range, left, right) {
			var remove = function (node) {
				Mutation.removePreservingRange(node, range);
			};
			walkBoundaryLeftRightInbetween(
				range,
				//carryDown
				Fn.noop,
				// stepLeftStart
				Fn.noop,
				// remove
				//   |
				//   v
				// {<b>...
				remove,
				//   remove
				//     |
				//     v
				// ...<b>}
				remove,
				// stepRightEnd
				Fn.noop,
				// stepPartial
				Fn.noop,
				//      remove
				//        |
				//        v
				// {...<b></b>...}
				remove,
				null
			);
			return {
				postprocessTextNodes: Fn.noop,
				postprocess: function () {
					var split = Html.removeBreak(
						Boundaries.fromRangeStart(range),
						Boundaries.fromRangeEnd(range)
					)[0];
					var cursor = Cursors.createFromBoundary(
						Boundaries.container(split),
						Boundaries.offset(split)
					);
					left.setFrom(cursor);
					right.setFrom(cursor);
				}
			};
		}, false);
	}

	/**
	 * Creates a visual line break at the given boundary position.
	 *
	 * @see
	 * https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#splitting-a-node-list's-parent
	 * http://lists.whatwg.org/htdig.cgi/whatwg-whatwg.org/2011-May/031700.html
	 *
	 * @param  {!Boundary} boundary
	 * @param  {string}    breaker
	 * @return {Array.<Boundary>}
	 * @memberOf editing
	 */
	function breakline(boundary, breaker) {
		var op = 'BR' === breaker ? Html.insertLineBreak : Html.insertBreak;
		boundary = op(boundary, breaker);
		return [boundary, boundary];
	}

	/**
	 * This function is missing documentation.
	 * @TODO Complete documentation.
	 *
	 * @memberOf editing
	 */
	function insert(start, end, insertion) {
		var range = Boundaries.range(start, end);
		split(range, {
			below: function (node) {
				return Content.allowsNesting(node.nodeName, insertion.nodeName);
			}
		});
		var boundary = Mutation.insertNodeAtBoundary(
			insertion,
			Boundaries.fromRangeStart(range)
		);
		Boundaries.setRange(range, boundary, Boundaries.create(
			Boundaries.container(boundary),
			Boundaries.offset(boundary) + 1
		));
		return Boundaries.fromRangeStart(range);
	}

	/**
	 * This function is missing documentation.
	 * @TODO Complete documentation.
	 *
	 * @memberOf editing
	 */
	function className(start, end, name, value, boundaries) {
		throw 'Not implemented';
	}

	/**
	 * This function is missing documentation.
	 * @TODO Complete documentation.
	 *
	 * @memberOf editing
	 */
	function attribute(start, end, name, value, boundaries) {
		throw 'Not implemented';
	}

	/**
	 * This function is not yet implemented.
	 * @TODO to be implemented
	 * @memberOf editing
	 */
	function cut(start, end, boundaries) {
		throw 'Not implemented';
	}

	/**
	 * This function is not yet implemented.
	 * @TODO to be implemented
	 * @memberOf editing
	 */
	function copy(start, end, boundaries) {
		throw 'Not implemented';
	}

	/**
	 * Starting with the given, returns the first node that matches the given
	 * predicate.
	 *
	 * @private
	 * @param  {!Node}                  node
	 * @param  {function(Node):boolean} pred
	 * @return {Node}
	 */
	function nearest(node, pred) {
		return Dom.upWhile(node, function (node) {
			return !pred(node)
			    && !(node.parentNode && Dom.isEditingHost(node.parentNode));
		});
	}

	/**
	 * Expands the given start and end boundaires until the nearst containers
	 * that match the given predicate.
	 *
	 * @private
	 * @param  {Boundary}               start
	 * @param  {Boundary}               end
	 * @param  {function(Node):boolean} pred
	 * @return {Array.<Boundary>}
	 */
	function expandUntil(start, end, pred) {
		var node, startNode, endNode;
		if (Html.isBoundariesEqual(start, end)) {
			//       node ----------.
			//        |             |
			//        v             v
			// </p>{}<u> or </b>{}</p>
			node = Boundaries.nextNode(end);
			if (Dom.isEditingHost(node)) {
				node = Boundaries.prevNode(start);
			}
			if (Dom.isEditingHost(node)) {
				return [start, end];
			}
			startNode = endNode = pred(node) ? node : nearest(node, pred);
		} else {
			startNode = nearest(Boundaries.nextNode(start), pred);
			endNode = nearest(Boundaries.prevNode(end), pred);
		}
		return [
			Boundaries.fromFrontOfNode(startNode),
			Boundaries.fromBehindOfNode(endNode)
		];
	}

	/**
	 * Given a list of sibling nodes and a formatting, will apply the formatting
	 * across the list of nodes.
	 *
	 * @private
	 * @param  {string}       formatting
	 * @param  {Array.<Node>} siblings
	 */
	function formatSiblings(formatting, siblings) {
		var wrapper = null;
		siblings.forEach(function (node) {
			if (Html.isUnrendered(node) && !wrapper) {
				return;
			}
			if (Content.allowsNesting(formatting, node.nodeName)) {
				if (!wrapper) {
					wrapper = node.ownerDocument.createElement(formatting);
					Dom.insert(wrapper, node);
				}
				return Dom.move([node], wrapper);
			}
			wrapper = null;
			if (Html.isVoidType(node)) {
				return;
			}
			var children = Dom.children(node);
			var childNames = children.map(function (child) { return child.nodeName; });
			var canWrapChildren = childNames.length === childNames.filter(
				Fn.partial(Content.allowsNesting, formatting)
			).length;
			var allowedInParent = Content.allowsNesting(
				node.parentNode.nodeName,
				formatting
			);
			if (
				canWrapChildren              &&
				allowedInParent              &&
				!Html.isGroupContainer(node) &&
				!Html.isGroupedElement(node)
			) {
				return Dom.replaceShallow(
					node,
					node.ownerDocument.createElement(formatting)
				);
			}
			var i = Arrays.someIndex(children, Html.isRendered);
			if (i > -1) {
				formatSiblings(formatting, children.slice(i));
			}
		});
	}

	/**
	 * Applies block formatting to contents enclosed by start and end boundary.
	 * Will return updated array of boundaries after the operation.
	 *
	 * @private
	 * @param  {!string}   formatting
	 * @param  {!Boundary} start
	 * @param  {!Boundary} end
	 * @return {Array.<Boundary>}
	 */
	function formatBlock(formatting, start, end, preserve) {
		var boundaries = expandUntil(start, end, Html.hasLinebreakingStyle);
		boundaries = Html.walkBetween(
			boundaries[0],
			boundaries[1],
			Fn.partial(formatSiblings, formatting)
		);
		start = Boundaries.fromStartOfNode(Boundaries.nextNode(boundaries[0]));
		end = Boundaries.fromEndOfNode(Boundaries.prevNode(boundaries[1]));
		return [Html.expandForward(start), Html.expandBackward(end)];
	}

	/**
	 * Applies block and inline formattings (eg. 'B', 'I', 'H2' - be sure to use
	 * UPPERCASE node names here) to contents enclosed by start and end
	 * boundary.
	 *
	 * Will return updated array of boundaries after the operation.
	 *
	 * @param  {!Boundary} start
	 * @param  {!Boundary} end
	 * @param  {!string}   nodeName
	 * @param  {Array.<Boundary>}
	 * @return {Array.<Boundary>}
	 * @memberOf editing
	 */
	function format(start, end, nodeName, boundaries) {
		var range;
		var node = {nodeName: nodeName};
		if (nodeName.toLowerCase() === 'a') {
			range = Links.create('', start, end);
		} else if (Html.isTextLevelSemanticNode(node)) {
			range = formatInline(nodeName, start, end, true);
		} else if (Html.isListContainer(node)) {
			range = Lists.toggle(nodeName, start, end);
		} else if (Html.isBlockNode(node)) {
			range = formatBlock(nodeName, start, end);
		}
		return range;
	}

	/**
	 * This function is missing documentation.
	 * @TODO Complete documentation.
	 * 
	 * @memberOf editing
	 */
	function unformat(start, end, nodeName, boundaries) {
	   return formatInline(nodeName, start, end, false);
	}

	/**
	 * Toggles inline style round the given selection.
	 *
	 * @private
	 * @param  {string}    nodeName
	 * @param  {!Boundary} start
	 * @param  {!Boundary} end
	 * @return {Array.<Boundary>}
	 */
	function toggleInline(nodeName, start, end) {
		var override = Overrides.nodeToState[nodeName];
		if (!override) {
			return [start, end];
		}
		var next = Boundaries.nextNode(Html.expandForward(start));
		var prev = Boundaries.prevNode(Html.expandBackward(end));
		var overrides = Overrides.harvest(next).concat(Overrides.harvest(prev));
		var hasStyle = -1 < Overrides.indexOf(overrides, override);
		var op = hasStyle ? unformat : format;
		return op(start, end, nodeName);
	}

	/**
	 * Toggles formatting round the given selection.
	 *
	 * @todo   Support block formatting
	 * @param  {!Boundary} start
	 * @param  {!Boundary} end
	 * @param  {string}    nodeName
	 * @param  {Array.<Boundary>}
	 * @return {Array.<Boundary>}
	 */
	function toggle(start, end, nodeName, boundaries) {
		var node = {nodeName: nodeName};
		if (Html.isTextLevelSemanticNode(node)) {
			return toggleInline(nodeName, start, end);
		}
		return [start, end];
	}

	return {
		format     : format,
		unformat   : unformat,
		toggle     : toggle,
		style      : style,
		className  : className,
		attribute  : attribute,
		cut        : cut,
		copy       : copy,
		breakline  : breakline,
		insert     : insert,
		wrap      : wrapElem,

		// obsolete
		split     : split,
		remove    : remove,
		trimClosingOpening: trimClosingOpening
	};
});
comments powered by Disqus