/** * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.client.editor.extract; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Text; import org.waveprotocol.wave.client.editor.content.ContentElement; import org.waveprotocol.wave.client.editor.content.ContentNode; import org.waveprotocol.wave.client.editor.extract.SelectionMatcher.LazyPoint.Type; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.util.Preconditions; /** * Helper class to note html point corresponding to given content point. * * The class is constructed with a pair of content points. Then, it provides methods * that takes a ContentPoint and corresponding html point. If it the given content point * matches the initial content points, then it'll note the html points as a match. * */ public final class SelectionMatcher { /** * The selection in the content that needs to be matched. */ private final Point<ContentNode> contentStart; private final Point<ContentNode> contentEnd; /** * The root container for new selection. */ private Node htmlRootContainer; /** * The new matching selection. */ private LazyPoint htmlStart = null; private LazyPoint htmlEnd = null; /** * Lazy point so we can provide a reference to a point, before the DOM is * fully constructed, i.e. * <p><br/>|</p> * * The Point here has parent <p> with nodeAfter null. However, if we express * it as point with nodeBefore = <br/> the point is valid even if nodes are * inserted after the <br/> */ interface LazyPoint { enum Type { BEFORE_NODE, AFTER_NODE, AT_START, } /** * @return the corresponding Point. */ Point<Node> getPoint(); } /** * A variant of LazyPoint where we are given the explicit point. */ private final class EagerPoint implements LazyPoint { private final Point<Node> explicit; EagerPoint(Point<Node> p) { assert p != null; assert htmlRootContainer.isOrHasChild(p.getContainer()) : "container not attached"; this.explicit = p; } @Override public Point<Node> getPoint() { return explicit; } } /** * @param n * @return a LazyPoint before the give node. */ LazyPoint beforeNode(Node n) { return new LazyPointImpl(n, Type.BEFORE_NODE); } /** * @param n * @return a LazyPoint after the give node. */ LazyPoint afterNode(Node n) { return new LazyPointImpl(n, Type.AFTER_NODE); } /** * @param n * @return a LazyPoint at the start of the given node. */ LazyPoint atStart(Node n) { return new LazyPointImpl(n, Type.AT_START); } private final class LazyPointImpl implements LazyPoint { private final Node ref; private final Type pointType; LazyPointImpl(Node ref, Type pointType) { assert ref != null; assert htmlRootContainer.isOrHasChild(ref) : "Reference node not attached"; this.ref = ref; this.pointType = pointType; } /** * Evalulates the LazyPoint to a Point. */ @Override public Point<Node> getPoint() { assert ref.getParentElement() != null : "Reference node must be attached when getting point"; switch (pointType) { case AFTER_NODE: return Point.inElement(ref.getParentElement(), ref.getNextSibling()); case BEFORE_NODE: return Point.inElement(ref.getParentElement(), ref); case AT_START: return Point.inElement(ref, ref.getFirstChild()); default: throw new RuntimeException("invalid case"); } } } /** * Initializes a SelectionMatcher with a contentSelection. * * @param contentStart * @param contentEnd */ SelectionMatcher(Point<ContentNode> contentStart, Point<ContentNode> contentEnd) { this.contentStart = contentStart; this.contentEnd = contentEnd; } /** * Note the points if matched. * * If the initial content start or content end match the boundary of source, * then note the html point on the corresponding boundary of clone. * * If source is a text node, or is empty, note the html point inside the * corresponding position in clone. * * @param source * @param clone */ public void maybeNoteHtml(ContentNode source, Node clone) { Preconditions.checkArgument(source != null, "Source must be non-null"); Preconditions.checkArgument(source.getParentElement() != null, "Source must have parent."); assert htmlRootContainer.isOrHasChild(clone) : "Reference node must be attached"; if (htmlStart == null) { maybeSetStartAtBoundary(source, clone); maybeSetStartInText(source, clone); maybeSetStartInEmptyElement(source, clone); } if (htmlEnd == null) { maybeSetEndAtBoundary(source, clone); maybeSetEndInText(source, clone); maybeSetEndInEmptyElement(source, clone); } } /** * Ensures the selection contains this node, if the original selection lies inside its subtree. * * @param node * @param dstParent * @param onSubtree */ public void noteSelectionInNode(ContentNode node, Element dstParent, boolean onSubtree) { if (contentStart.getContainer() == node || contentStart.equals(Point.inElement(node.getParentElement(), node))) { htmlStart = dstParent.hasChildNodes() ? afterNode(dstParent.getLastChild()) : atStart(dstParent); } if (contentEnd.getContainer() == node || contentEnd.equals(Point.inElement(node.getParentElement(), node))) { htmlEnd = dstParent.hasChildNodes() ? afterNode(dstParent.getLastChild()) : atStart(dstParent); } } /** * Sets the root html container. The new selection must lie inside the subtree * of this element. This is primarily used for checking that the selection is * valid. * * @param n */ public void setHtmlRootContainer(Node n) { htmlRootContainer = n; } /** * Gets the noted start point. */ public Point<Node> getHtmlStart() { return htmlStart != null ? htmlStart.getPoint() : null; } /** * Gets the noted end point */ public Point<Node> getHtmlEnd() { return htmlEnd != null ? htmlEnd.getPoint() : null; } private void maybeSetStartAtBoundary(ContentNode c, Node node) { if (contentStart.equals(Point.inElement(c.getParentElement(), c))) { htmlStart = beforeNode(node); } else if (contentStart.equals((Point.inElement(c.getParentElement(), c.getNextSibling())))) { htmlStart = afterNode(node); } } private void maybeSetEndAtBoundary(ContentNode c, Node node) { if (contentEnd.equals(Point.inElement(c.getParentElement(), c))) { htmlEnd = beforeNode(node); } else if (contentEnd.equals((Point.inElement(c.getParentElement(), c.getNextSibling())))) { htmlEnd = afterNode(node); } } private void maybeSetStartInEmptyElement(ContentNode source, Node clone) { LazyPoint p = getCorespondingPointInEmptyElement(contentStart, source, clone); if (p != null) { htmlStart = p; } } private void maybeSetEndInEmptyElement(ContentNode source, Node clone) { LazyPoint p = getCorespondingPointInEmptyElement(contentEnd, source, clone); if (p != null) { htmlEnd = p; } } private LazyPoint getCorespondingPointInEmptyElement(Point<ContentNode> selection, ContentNode source, Node clone) { if (source instanceof ContentElement && source.getFirstChild() == null && selection.equals(Point.inElement(source, null))) { if (clone instanceof Element) { return new EagerPoint(Point.inElement(clone, null)); } else { return new EagerPoint(Point.inElement(clone.getParentElement(), clone)); } } else { return null; } } private void maybeSetEndInText(ContentNode source, Node clone) { LazyPoint matchedTextSelection = matchTextSelection(contentEnd, source, clone); if (matchedTextSelection != null) { htmlEnd = matchedTextSelection; } } private void maybeSetStartInText(ContentNode source, Node clone) { LazyPoint matchedTextSelection = matchTextSelection(contentStart, source, clone); if (matchedTextSelection != null) { htmlStart = matchedTextSelection; } } private LazyPoint matchTextSelection( Point<ContentNode> selection, ContentNode source, Node clone) { if (selection.isInTextNode() && selection.getContainer() == source) { assert clone instanceof Text; return new EagerPoint(Point.<Node>inText(clone, selection.getTextOffset())); } else { return null; } } }