vendor/assets/javascripts/aloha/lib/util/range-context.js in locomotive-aloha-rails-0.23.2.1 vs vendor/assets/javascripts/aloha/lib/util/range-context.js in locomotive-aloha-rails-0.23.2.2

- old
+ new

@@ -22,20 +22,35 @@ * non-source (e.g., minimized or compacted) forms of the Aloha-Editor * source code without the copy of the GNU GPL normally required, * provided you include this license notice and a URL through which * recipients can access the Corresponding Source. */ +/** + * TODO improve restacking and joining algorithm + * TODO what do do about insignificant whitespace when pushing down or setting a context? + * TODO check contained-in rules when when pushing down or setting a context + * TODO formatStyle: in the following case the outer "font-family: arial" span should be removed. + * Can be done similar to how findReusableAncestor() works. + * <span style="font-family: arial"> + * <span style="font-family: times">one</span> + * <span style="font-family: helvetica">two<span> + * </span> + */ define([ + 'jquery', 'util/dom2', 'util/arrays', 'util/trees', + 'util/strings', 'util/functions', 'util/html' ], function ( + $, Dom, Arrays, Trees, + Strings, Fn, Html ) { 'use strict'; @@ -46,14 +61,14 @@ */ function walkSiblings(parent, beforeAfterChild, before, at, after, arg) { var fn = before; Dom.walk(parent.firstChild, function (child) { if (child !== beforeAfterChild) { - return fn(child, arg); + fn(child, arg); } else { fn = after; - return at(child, arg); + at(child, arg); } }); } /** @@ -91,10 +106,24 @@ 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 startEnd/endEnd 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 @@ -116,38 +145,27 @@ var eo = liveRange.endOffset; var start = Dom.nodeAtOffset(sc, so); var end = Dom.nodeAtOffset(ec, eo); var startEnd = Dom.isAtEnd(sc, so); var endEnd = Dom.isAtEnd(ec, eo); - var uptoCacChildStart = Dom.childAndParentsUntilNode(start, cac); - var uptoCacChildEnd = Dom.childAndParentsUntilNode(end, cac); - var cacChildStart = uptoCacChildStart.length ? uptoCacChildStart[uptoCacChildStart.length - 1] : null; - var cacChildEnd = uptoCacChildEnd.length ? uptoCacChildEnd[uptoCacChildEnd.length - 1] : null; - arg = carryDown(cac, arg) || arg; - // 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 startEnd/endEnd positions the nodes are - // also ancestors of the position). - function stepAtStart(node, arg) { - return node === start && !startEnd - ? stepInside(node, arg) - : stepPartial(node, arg); - } - function stepAtEnd(node, arg) { - return node === end && !endEnd - ? stepOutside(node, arg) - : stepPartial(node, arg); - } - ascendWalkSiblings(uptoCacChildStart, startEnd, carryDown, stepOutside, stepAtStart, stepInside, arg); - ascendWalkSiblings(uptoCacChildEnd, endEnd, carryDown, stepInside, stepAtEnd, stepOutside, arg); + var ascStart = Dom.childAndParentsUntilNode(start, cac); + var ascEnd = Dom.childAndParentsUntilNode(end, cac); + var stepAtStart = makePointNodeStep(start, startEnd, stepInside, stepPartial); + var stepAtEnd = makePointNodeStep(end, endEnd, stepOutside, stepPartial); + ascendWalkSiblings(ascStart, startEnd, carryDown, stepOutside, stepAtStart, stepInside, arg); + ascendWalkSiblings(ascEnd, endEnd, carryDown, stepInside, stepAtEnd, stepOutside, arg); + var cacChildStart = Arrays.last(ascStart); + var cacChildEnd = Arrays.last(ascEnd); if (cacChildStart && cacChildStart !== cacChildEnd) { var next; Dom.walkUntilNode(cac.firstChild, stepOutside, cacChildStart, arg); - next = stepAtStart(cacChildStart, arg); + next = cacChildStart.nextSibling; + stepAtStart(cacChildStart, arg); Dom.walkUntilNode(next, stepInside, cacChildEnd, arg); if (cacChildEnd) { - next = stepAtEnd(cacChildEnd, arg); + next = cacChildEnd.nextSibling; + stepAtEnd(cacChildEnd, arg); Dom.walk(next, stepOutside, arg); } } } @@ -215,76 +233,143 @@ * style="font-weight: normal">. See hasOverrideAncestor below. * * @param liveRange range's boundary points should be between nodes * (Dom.splitTextContainers). * - * @param isUpperBoundary args (node). Identifies exclusive upper - * boundary element, only elements below which will be modified. + * @param formatter a map with the following properties + * isUpperBoundary(node) - identifies exclusive upper + * boundary element, only elements below which will be modified. * - * @param getOverride(node). Returns a node's override, or - * null 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. + * 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. + * A node with an override must not also provide the context: + * !(null != getOverride(node) && hasContext(node)) * - * @param 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. + * 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. * - * @parma clearOverrideRec(node). Like clearOverride but - * should clear the override recursively. + * clearOverrideRec(node) - like clearOverride but should clear + * the override recursively. If not provided, clearOverride will + * be applied recursively. * - * @param 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. + * 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. * - * @param hasContext(node). Returns true if the given node - * already provides the context to set. + * hasContext(node) - returns true if the given node + * already provides the context to set. * - * @param setContext(node, 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. + * setContext(node, 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, isUpperBoundary, getOverride, clearOverride, clearOverrideRec, pushDownOverride, hasContext, setContext, rootHasContext) { + function mutate(liveRange, formatter, rootHasImpliedContext) { if (liveRange.collapsed) { return; } // Because range may be mutated during traversal, we must only // refer to it before traversal. var cac = liveRange.commonAncestorContainer; var topmostOverrideNode = null; var bottommostOverrideNode = null; var isNonClearableOverride = false; var upperBoundaryAndBeyond = false; - var fromCacToContext = Dom.childAndParentsUntilIncl(cac, hasContext); + 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 || 9 === node.parentNode.nodeType || formatter.hasContext(node); + }); Arrays.forEach(fromCacToContext, function (node) { - upperBoundaryAndBeyond = upperBoundaryAndBeyond || isUpperBoundary(node); - if (getOverride(node)) { + upperBoundaryAndBeyond = upperBoundaryAndBeyond || formatter.isUpperBoundary(node); + if (null != formatter.getOverride(node)) { topmostOverrideNode = node; isNonClearableOverride = upperBoundaryAndBeyond; bottommostOverrideNode = bottommostOverrideNode || node; } }); - if ((rootHasContext || hasContext(fromCacToContext[fromCacToContext.length - 1])) + if ((rootHasImpliedContext || formatter.hasContext(Arrays.last(fromCacToContext))) && !isNonClearableOverride) { var pushDownFrom = topmostOverrideNode || cac; - var cacOverride = getOverride(bottommostOverrideNode || cac); - pushDownContext(liveRange, pushDownFrom, cacOverride, getOverride, clearOverride, clearOverrideRec, pushDownOverride); + var cacOverride = formatter.getOverride(bottommostOverrideNode || cac); + var clearOverrideRec = formatter.clearOverrideRec || function (node) { + Dom.walkRec(node, formatter.clearOverride); + }; + pushDownContext( + liveRange, + pushDownFrom, + cacOverride, + formatter.getOverride, + formatter.clearOverride, + clearOverrideRec, + formatter.pushDownOverride + ); } else { - walkBoundary(liveRange, getOverride, pushDownOverride, clearOverride, function (node) { - return setContext(node, isNonClearableOverride); - }); + var setContext = function (node) { + formatter.setContext(node, isNonClearableOverride); + }; + walkBoundary( + liveRange, + formatter.getOverride, + formatter.pushDownOverride, + formatter.clearOverride, + setContext + ); } } + function fixupRange(liveRange, mutate) { + // 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 trimRange() below to adjust the range's boundary + // points, which we don't want the browser to re-adjust (which + // some browsers do). + var range = Dom.stableRange(liveRange); + + // Because we should avoid splitTextContainers() if this call is a noop. + if (range.collapsed) { + return; + } + + // Because trimRangeClosingOpening(), mutate() and + // adjustPointMoveBackWithinRange() require boundary points to + // be between nodes. + Dom.splitTextContainers(range); + + // 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 + // and because adjustPointMoveBackWithinRange() requires the + // left boundary point to be next to a non-ignorable node. + Dom.trimRangeClosingOpening(range, Html.isIgnorableWhitespace); + + // Because mutation needs to keep track and adjust boundary + // points. + var leftPoint = Dom.cursorFromBoundaryPoint(range.startContainer, range.startOffset); + var rightPoint = Dom.cursorFromBoundaryPoint(range.endContainer, range.endOffset); + + mutate(range, leftPoint, rightPoint); + + // Because we must reflect the adjusted boundary points in the + // given range. + Dom.setRangeStartFromCursor(liveRange, leftPoint); + Dom.setRangeEndFromCursor(liveRange, rightPoint); + } + function adjustPointShallowRemove(point, left, node) { if (point.node === node) { point.next(); } } @@ -312,19 +397,19 @@ if (point.node === node && !point.atEnd) { point.node = wrapper; } } - function shallowRemoveAdjust(node, leftPoint, rightPoint) { + function removeShallowAdjust(node, leftPoint, rightPoint) { adjustPointShallowRemove(leftPoint, true, node); adjustPointShallowRemove(rightPoint, false, node); - Dom.shallowRemove(node); + Dom.removeShallow(node); } function wrapAdjust(node, wrapper, leftPoint, rightPoint) { if (wrapper.parentNode) { - shallowRemoveAdjust(wrapper, leftPoint, rightPoint); + removeShallowAdjust(wrapper, leftPoint, rightPoint); } adjustPointWrap(leftPoint, true, node, wrapper); adjustPointWrap(rightPoint, false, node, wrapper); Dom.wrap(node, wrapper); } @@ -333,25 +418,19 @@ adjustPointMoveBackWithinRange(leftPoint, true, node, ref, atEnd); adjustPointMoveBackWithinRange(rightPoint, false, node, ref, atEnd); Dom.insert(node, ref, atEnd); } - function nextSibling(node) { - return node.nextSibling; - } - - // TODO when restacking the <b> that wraps "z" in - // <u><b>x</b><s><b>z</b></s></u>, join with the <b> that wraps "x". function restackRec(node, hasContext, notIgnoreHorizontal, notIgnoreVertical) { if (1 !== node.nodeType || notIgnoreVertical(node)) { return null; } - var maybeContext = Dom.walkUntil(node.firstChild, nextSibling, notIgnoreHorizontal); + var maybeContext = Dom.next(node.firstChild, notIgnoreHorizontal); if (!maybeContext) { return null; } - var notIgnorable = Dom.walkUntil(maybeContext.nextSibling, nextSibling, notIgnoreHorizontal); + var notIgnorable = Dom.next(maybeContext.nextSibling, notIgnoreHorizontal); if (notIgnorable) { return null; } if (hasContext(maybeContext)) { return maybeContext; @@ -373,156 +452,325 @@ } wrapAdjust(node, context, leftPoint, rightPoint); return true; } - function format(liveRange, nodeName, unformat) { - var leftPoint; - var rightPoint; + function ensureWrapper(node, nodeName, hasWrapper, leftPoint, rightPoint) { + if (node.previousSibling && !hasWrapper(node.previousSibling)) { + // Because restacking here solves two problems: one the + // case where the context was unnecessarily pushed down + // on the left of the range, and two to join with a + // context node that already exists to the left of the + // range. + restack(node.previousSibling, + hasWrapper, + Html.isIgnorableWhitespace, + Html.isInlineFormattable, + leftPoint, + rightPoint); + } + if (node.previousSibling && hasWrapper(node.previousSibling)) { + insertAdjust(node, node.previousSibling, true, leftPoint, rightPoint); + return true; + } + if (!hasWrapper(node)) { + var wrapper = document.createElement(nodeName); + wrapAdjust(node, wrapper, leftPoint, rightPoint); + return true; + } + return false; + } + function isUpperBoundary_default(node) { + // Because the body element is an obvious upper boundary, and + // because, when we are inside an editable, we shouldn't make + // modifications outside the editable (if we are not inside + // an editable, we don't care). + return 'BODY' === node.nodeName || Html.isEditingHost(node); + } + + function makeNodeFormatter(nodeName, leftPoint, rightPoint) { function hasContext(node) { - if (unformat) { - // Because we pass rootHasContext=true to mutate. - return false; - } return nodeName === node.nodeName; } - function getOverride(node) { - if (!unformat) { - return false; + function clearContext(node) { + if (nodeName === node.nodeName) { + removeShallowAdjust(node, leftPoint, rightPoint); } - return nodeName === node.nodeName; } - function hasOverride(node) { - return !!getOverride(node); + function clearContextRec(node) { + Dom.walkRec(node, clearContext); } - function clearOverride(node) { - var next = node.nextSibling; - if (unformat && nodeName === node.nodeName) { - shallowRemoveAdjust(node, leftPoint, rightPoint); + function setContext(node) { + if (ensureWrapper(node, nodeName, hasContext, leftPoint, rightPoint)) { + // Because the node was wrapped with a context, and if + // the node itself has the context, it should be cleared + // to avoid nested contexts. + clearContextRec(node); + } else { + // Because the node itself has the context and was not + // wrapped, we must only clear its children. + Dom.walk(node.firstChild, clearContextRec); } - return next; } - function clearOverrideRec(node) { - return Dom.walkRec(node, clearOverride); + return { + hasContext: hasContext, + getOverride: Fn.noop, + clearOverride: Fn.noop, + pushDownOverride: Fn.noop, + setContext: setContext, + isUpperBoundary: isUpperBoundary_default + }; + } + + function makeNodeUnformatter(nodeName, leftPoint, rightPoint) { + + function getOverride(node) { + return nodeName === node.nodeName ? true : null; } - function clearContext(node) { - var next = node.nextSibling; - if (!unformat && nodeName === node.nodeName) { - shallowRemoveAdjust(node, leftPoint, rightPoint); + function clearOverride(node) { + if (nodeName === node.nodeName) { + removeShallowAdjust(node, leftPoint, rightPoint); } - return next; } - function clearContextRec(node) { - return Dom.walkRec(node, clearContext); + function pushDownOverride(node, override) { + if (!override) { + return; + } + ensureWrapper(node, nodeName, getOverride, leftPoint, rightPoint); } - function ensureWrapper(node, hasWrapper) { - if (node.previousSibling && !hasWrapper(node.previousSibling)) { - // Because restacking here solves two problems: one the - // case where the context was unnecessarily pushed down - // on the left of the range, and two to join with a - // context node that already exists to the left of the - // range. - restack(node.previousSibling, - hasWrapper, - Html.isIgnorableWhitespace, - Html.isInlineFormattable, - leftPoint, rightPoint); + return { + hasContext: Fn.returnFalse, + setContext: Fn.noop, + getOverride: getOverride, + clearOverride: clearOverride, + pushDownOverride: pushDownOverride, + isUpperBoundary: isUpperBoundary_default + }; + } + + function createStyleWrapper_default() { + return document.createElement('SPAN'); + } + + function isStyleEq_default(styleValueA, styleValueB) { + return styleValueA === styleValueB; + } + + function isStyleWrapperReusable_default(node) { + return 'SPAN' === node.nodeName; + } + + function isStyleWrapperPrunable_default(node) { + return ('SPAN' === node.nodeName + && Arrays.every(Arrays.map(Dom.attrs(node), Arrays.second), + Strings.empty)); + } + + function makeStyleFormatter(styleName, styleValue, createWrapper, isStyleEq, isReusable, isPrunable, leftPoint, rightPoint) { + + function removeStyle(node, styleName) { + if (Strings.empty(Dom.getStyle(node, styleName))) { + return; } - if (node.previousSibling && hasWrapper(node.previousSibling)) { - insertAdjust(node, node.previousSibling, true, leftPoint, rightPoint); - return true; - } else if (!hasWrapper(node)) { - var wrapper = document.createElement(nodeName); - wrapAdjust(node, wrapper, leftPoint, rightPoint); - return true; + Dom.setStyle(node, styleName, null); + if (isPrunable(node)) { + removeShallowAdjust(node, leftPoint, rightPoint); } - return false; } - function pushDownOverride(node, override) { - if (!override) { - return node.nextSibling; + function setStyle(node, styleName, styleValue, prevWrapper) { + if (prevWrapper && prevWrapper === node.previousSibling) { + insertAdjust(node, prevWrapper, true, leftPoint, rightPoint); + removeStyle(node, styleName); + return prevWrapper; } - if (!unformat) { - throw "not implemented"; + if (isReusable(node)) { + Dom.setStyle(node, styleName, styleValue); + return prevWrapper; } - var next = node.nextSibling; - ensureWrapper(node, hasOverride); - return next; + var wrapper = createWrapper(); + Dom.setStyle(wrapper, styleName, styleValue); + wrapAdjust(node, wrapper, leftPoint, rightPoint); + removeStyle(node, styleName); + return wrapper; } - function setContext(node) { - if (unformat) { - throw "not implemented"; + function hasContext(node) { + return isStyleEq(Dom.getStyle(node, styleName), styleValue); + } + + function getOverride(node) { + var override = Dom.getStyle(node, styleName); + return (Strings.empty(override) || isStyleEq(override, styleValue) + ? null + : override); + } + + function clearOverride(node) { + removeStyle(node, styleName); + } + + function clearOverrideRec(node) { + Dom.walkRec(node, clearOverride); + } + + var overrideWrapper = null; + function pushDownOverride(node, override) { + if (Strings.empty(override) || !Strings.empty(Dom.getStyle(node, styleName))) { + return; } - var next = node.nextSibling; - if (ensureWrapper(node, hasContext)) { - // Because the node was wrapped with a context, and if - // the node itself has the context, it should be cleared - // to avoid nested contexts. - clearContextRec(node); - } else { - // Because the node itself has the context and was not - // wrapped, we must only clear its children. - Dom.walk(node.firstChild, clearContextRec); - } - clearOverrideRec(node); - return next; + overrideWrapper = setStyle(node, styleName, override, overrideWrapper); } - function isUpperBoundary(node) { - return 'BODY' === node.nodeName; + var contextWrapper = null; + function setContext(node) { + Dom.walk(node.firstChild, clearOverrideRec); + contextWrapper = setStyle(node, styleName, styleValue, contextWrapper); } - // 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 trimRange() below to adjust the range's boundary - // points, which we don't want the browser to re-adjust (which - // some browsers do). - var range = Dom.stableRange(liveRange); + return { + hasContext: hasContext, + getOverride: getOverride, + clearOverride: clearOverride, + pushDownOverride: pushDownOverride, + setContext: setContext, + isUpperBoundary: isUpperBoundary_default + }; + } - // Because we should avoid splitTextContainers() if this call is a noop. - if (range.collapsed) { - return; + function format(liveRange, nodeName) { + fixupRange(liveRange, function (range, leftPoint, rightPoint) { + mutate(range, makeNodeFormatter(nodeName, leftPoint, rightPoint)); + }); + } + + function unformat(liveRange, nodeName) { + fixupRange(liveRange, function (range, leftPoint, rightPoint) { + mutate(range, makeNodeUnformatter(nodeName, leftPoint, rightPoint), true); + }); + } + + function findReusableAncestor(range, hasContext, getOverride, isUpperBoundary, isReusable) { + var obstruction = null; + function untilIncl(node) { + return (null != getOverride(node) + || hasContext(node) + || isReusable(node) + || isUpperBoundary(node)); } + function beforeAfter(node) { + obstruction = obstruction || !Html.isIgnorableWhitespace(node); + } + var start = Dom.nodeAtOffset(range.startContainer, range.startOffset); + var end = Dom.nodeAtOffset(range.endContainer, range.endOffset); + var startEnd = Dom.isAtEnd(range.startContainer, range.startOffset); + var endEnd = Dom.isAtEnd(range.endContainer, range.endOffset); + var ascStart = Dom.childAndParentsUntilIncl(start, untilIncl); + var ascEnd = Dom.childAndParentsUntilIncl(end, untilIncl); + var reusable = Arrays.last(ascStart); + function at(node) { + // Because the start node is inside the range. + if (node === start && !startEnd) { + return; + } + // Because the end node is outside the range. + if (node === end && !endEnd) { + beforeAfter(node); + return; + } + obstruction = obstruction || !Html.isInlineFormattable(node); + } + if (!reusable || !isReusable(reusable) || reusable !== Arrays.last(ascEnd)) { + return null; + } + ascendWalkSiblings(ascStart, startEnd, Fn.noop, beforeAfter, at, Fn.noop); + if (obstruction) { + return null; + } + ascendWalkSiblings(ascEnd, endEnd, Fn.noop, Fn.noop, at, beforeAfter); + if (obstruction) { + return null; + } + return reusable; + } - // Because trimRangeClosingOpening(), mutate() and - // adjustPointMoveBackWithinRange() require boundary points to - // be between nodes. - Dom.splitTextContainers(range); + function formatStyle(liveRange, styleName, styleValue, createWrapper, isStyleEq, isReusable, isPrunable) { + createWrapper = createWrapper || createStyleWrapper_default; + isStyleEq = isStyleEq || isStyleEq_default; + isReusable = isReusable || isStyleWrapperReusable_default; + isPrunable = isPrunable || isStyleWrapperPrunable_default; + fixupRange(liveRange, function (range, leftPoint, rightPoint) { + var formatter = makeStyleFormatter( + styleName, + styleValue, + createWrapper, + isStyleEq, + isReusable, + isPrunable, + leftPoint, + rightPoint + ); + var reusableAncestor = findReusableAncestor( + range, + formatter.hasContext, + formatter.getOverride, + formatter.isUpperBoundary, + isReusable + ); + if (reusableAncestor) { + formatter.setContext(reusableAncestor); + } else { + mutate(range, formatter, false); + } + }); + } - // 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 - // and because adjustPointMoveBackWithinRange() requires the - // left boundary point to be next to a non-ignorable node. - Dom.trimRangeClosingOpening(range, Html.isIgnorableWhitespace); + function splitBoundary(liveRange, pred, clone) { + clone = clone || Dom.cloneShallow; + fixupRange(liveRange, function (range, leftPoint, rightPoint) { - // Because mutation needs to keep track and adjust boundary - // points. - leftPoint = Dom.cursorFromBoundaryPoint(range.startContainer, range.startOffset); - rightPoint = Dom.cursorFromBoundaryPoint(range.endContainer, range.endOffset); + var wrapper = null; - mutate(range, isUpperBoundary, getOverride, clearOverride, clearOverrideRec, pushDownOverride, hasContext, setContext, unformat); + function carryDown(elem, stop) { + return stop || !pred(elem); + } - // Because we must reflect the adjusted boundary points in the - // given range. - Dom.setRangeStartFromCursor(liveRange, leftPoint); - Dom.setRangeEndFromCursor(liveRange, rightPoint); + function pushDown(node, stop) { + if (stop) { + return; + } + if (!wrapper || node.parentNode.previousSibling !== wrapper) { + wrapper = clone(node.parentNode); + insertAdjust(wrapper, node.parentNode, false, leftPoint, rightPoint); + } + insertAdjust(node, wrapper, true, leftPoint, rightPoint); + } + + var sc = range.startContainer; + var so = range.startOffset; + var ec = range.endContainer; + var eo = range.endOffset; + var cac = range.commonAncestorContainer; + var startEnd = Dom.isAtEnd(sc, so); + var endEnd = Dom.isAtEnd(ec, eo); + var ascStart = Dom.childAndParentsUntilNode(Dom.nodeAtOffset(sc, so), cac); + var ascEnd = Dom.childAndParentsUntilNode(Dom.nodeAtOffset(ec, eo), cac); + ascendWalkSiblings(ascStart, startEnd, carryDown, pushDown, Fn.noop, Fn.noop, null); + ascendWalkSiblings(ascEnd, endEnd, carryDown, pushDown, Fn.noop, Fn.noop, null); + }); } return { - mutate: mutate, - format: format + format: format, + unformat: unformat, + formatStyle: formatStyle, + splitBoundary: splitBoundary }; });