/******************************************************************************* * Copyright (c) 2006 Sybase, Inc. and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Sybase, Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.jst.pagedesigner.dom; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Vector; import org.eclipse.core.runtime.Assert; import org.eclipse.gef.EditPart; import org.eclipse.jface.text.TextSelection; import org.eclipse.jst.jsf.common.ui.internal.logging.Logger; import org.eclipse.jst.pagedesigner.IHTMLConstants; import org.eclipse.jst.pagedesigner.PDPlugin; import org.eclipse.jst.pagedesigner.css2.ICSSStyle; import org.eclipse.jst.pagedesigner.css2.property.ICSSPropertyID; import org.eclipse.jst.pagedesigner.parts.ElementEditPart; import org.eclipse.jst.pagedesigner.parts.HTMLEditPartsFactory; import org.eclipse.jst.pagedesigner.parts.NodeEditPart; import org.eclipse.jst.pagedesigner.utils.HTMLUtil; import org.eclipse.jst.pagedesigner.validation.caret.Target; import org.eclipse.jst.pagedesigner.viewer.DesignPosition; import org.eclipse.jst.pagedesigner.viewer.DesignRange; import org.eclipse.swt.SWT; import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier; import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; /** * @author mengbo */ public final class EditModelQuery { private static Logger _log = PDPlugin.getLogger(EditModelQuery.class); private static EditModelQuery _instance; private static final int START_INDEX_BEFORE_TAG = 1; private static final int END_INDEX_WITHIN_TAG = 2; private static final HashSet SPECIAL_EMPTY_CHARS = new HashSet(); /** * Maps unicode Characters to html equivalents */ public static final HashMap CHAR_NODE_MAP = new HashMap(); // Cursor can't go outside of these container. private static final HashSet HTML_CONSTRAINED_CONTAINERS = new HashSet(); /** * HTML tags that control style */ public static final HashSet HTML_STYLE_NODES = new HashSet(); static final HashSet UNREMOVEBLE_TAGS = new HashSet(); static { UNREMOVEBLE_TAGS.add(IHTMLConstants.TAG_HTML); UNREMOVEBLE_TAGS.add(IHTMLConstants.TAG_HEAD); UNREMOVEBLE_TAGS.add(IHTMLConstants.TAG_BODY); EditModelQuery.CHAR_NODE_MAP.put(new Character(SWT.CR), IHTMLConstants.TAG_BR); EditModelQuery.CHAR_NODE_MAP.put(new Character(SWT.LF), IHTMLConstants.TAG_BR); EditModelQuery.SPECIAL_EMPTY_CHARS.add(" "); //$NON-NLS-1$ EditModelQuery.SPECIAL_EMPTY_CHARS.add("\t"); //$NON-NLS-1$ EditModelQuery.SPECIAL_EMPTY_CHARS.add("\r"); //$NON-NLS-1$ EditModelQuery.SPECIAL_EMPTY_CHARS.add("\n"); //$NON-NLS-1$ EditModelQuery.HTML_CONSTRAINED_CONTAINERS.add(IHTMLConstants.TAG_TD); EditModelQuery.HTML_CONSTRAINED_CONTAINERS.add(IHTMLConstants.TAG_TR); EditModelQuery.HTML_CONSTRAINED_CONTAINERS .add(IHTMLConstants.TAG_TABLE); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_B); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_EM); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_H1); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_H2); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_H3); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_H4); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_H5); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_H6); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_A); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_U); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_I); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_S); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_STRONG); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_TT); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_BIG); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_SMALL); EditModelQuery.HTML_STYLE_NODES.add(IHTMLConstants.TAG_FONT); } private EditModelQuery() { // no external instantiation } /** * @return the singleton instance */ public static EditModelQuery getInstance() { if (_instance == null) { _instance = new EditModelQuery(); } return _instance; } /** * Get previous sibling, or if sibling is null then get previous neighbor. * * @param node * @return the node */ public Node getPreviousNeighbor(Node node) { if (!EditValidateUtil.validNode(node)) { return null; } while (node != null && node.getNodeType() != Node.DOCUMENT_NODE && node.getPreviousSibling() == null) { node = node.getParentNode(); } return (node != null && node.getNodeType() != Node.DOCUMENT_NODE) ? node .getPreviousSibling() : null; } /** * Get privous sibling, or if sibling is null then get previous neighor's * rightmost child, which is adjacent to 'node'. * * @param node * @return the node */ public Node getPreviousLeafNeighbor(Node node) { return getLastLeafChild(getPreviousNeighbor(node)); } /** * Get next sibling, or if sibling is null get next neighbor. * * @param node * @return the node */ public Node getNextNeighbor(Node node) { if (!EditValidateUtil.validNode(node)) { return null; } while (node != null && node.getNodeType() != Node.DOCUMENT_NODE && node.getNextSibling() == null) { node = node.getParentNode(); } return (node != null && node.getNodeType() != Node.DOCUMENT_NODE) ? node .getNextSibling() : null; } /** * Get next sibling, or if sibling is null get next neighbor's leftmost leaf * child which will be adjacent to 'node'. * * @param node * @return the node */ public Node getNextLeafNeighbor(Node node) { return getFirstLeafChild(getNextNeighbor(node)); } /** * Get node's rightmost leaf child. * * @param node * @return the node */ private Node getLastLeafChild(Node node) { if (node == null) { return null; } if (node.getLastChild() != null) { return getLastLeafChild(node.getLastChild()); } return node; } /** * Get node's leftmost leaf child. * * @param node * @return the node */ private Node getFirstLeafChild(Node node) { if (node == null) { return null; } if (node.getFirstChild() != null) { return getFirstLeafChild(node.getFirstChild()); } return node; } /** * To see if node is within a indexed region that is started from 'start', * ended at 'end' */ static boolean within(int start, int end, Node theNode) { return getNodeStartIndex(theNode) >= start && getNodeEndIndex(theNode) <= end; } /** * To see whether the 'position' is within indexed location (start, end) * * @param start * @param end * @param position * @return */ boolean within(int start, int end, IDOMPosition position) { int pos = getIndexedRegionLocation(position); return start <= pos && pos <= end; } /** * To see whether the 'theNode' is not within indexed region (start, end) * * @param start * @param end * @param theNode * @return */ static boolean outOf(int start, int end, Node theNode) { if (getNodeLenth(theNode) > 0) { return getNodeStartIndex(theNode) >= end || getNodeEndIndex(theNode) <= start; } return !((getNodeStartIndex(theNode) >= start && getNodeEndIndex(theNode) <= end)); } /** * Determine whether the position is at node's edge. When the offset is at * edge, it is in the leftmost or rightmost offset of node's region. * * @param position * @param forward * @return true if at edge */ boolean atEdge(IDOMPosition position, boolean forward) { Node node = position.getContainerNode(); int offset = position.getOffset(); if (forward) { if (EditModelQuery.isText(node)) { return offset == node.getNodeValue().length(); } return offset == node.getChildNodes().getLength(); } return offset == 0; } /** * Get node's neighbor on the node tree, if forward, then get next, * otherwise go backward. * * @param node * @param forward * @return the node */ Node getNeighbor(Node node, boolean forward) { if (forward) { return getNextNeighbor(node); } return getPreviousNeighbor(node); } /** * Get neighbor which is descendent of root. * * @param node * @param root * @return the node */ Node getPreviousNeighbor(Node node, Node root) { if (!EditValidateUtil.validNode(node)) { return null; } while (node != null && node != root && node.getNodeType() != Node.DOCUMENT_NODE && node.getPreviousSibling() == null) { node = node.getParentNode(); } return (node != null && node != root && node.getNodeType() != Node.DOCUMENT_NODE) ? node .getPreviousSibling() : null; } /** * Get neighbor which is descendent of root. * * @param node * @param root * @return the node */ Node getNextNeighbor(Node node, Node root) { if (!EditValidateUtil.validNode(node)) { return null; } while (node != null && node != root && node.getNodeType() != Node.DOCUMENT_NODE && node.getNextSibling() == null) { node = node.getParentNode(); } return (node != null && node != root && node.getNodeType() != Node.DOCUMENT_NODE) ? node .getNextSibling() : null; } /** * Get neighbor which is descendent of root. * * @param node * @param forward * @param root * @return the node */ Node getNeighbor(Node node, boolean forward, Node root) { Assert.isTrue(root != null); if (forward) { return getNextNeighbor(node, root); } return getPreviousNeighbor(node, root); } /** * Get node's leaf child which is adjacent to 'node', according to * 'forward', it will search backward or forward. * * @param node * @param forward * @return the node */ Node getLeafNeighbor(Node node, boolean forward) { if (node == null) { return null; } if (forward) { return getNextLeafNeighbor(node); } return getPreviousLeafNeighbor(node); } /** * Get neighbor's leaf child, which is adjacent to 'node' * * @param node * @param childIndex * @param forward * @return the node */ Node getLeafNeighbor(Node node, int childIndex, boolean forward) { if (node == null) { return null; } Node neighbor = getNeighbor(node, childIndex, forward); if (neighbor != null) { if (forward) { return getFirstLeafChild(neighbor); } return getLastLeafChild(neighbor); } return null; } /** * First try sibling, if it retruns null, try search neighbor. * * @param parent * @param childIndex * @param forward * @return the node */ Node getNeighbor(Node parent, int childIndex, boolean forward) { if (!EditValidateUtil.validNode(parent)) { return null; } NodeList nodeList = parent.getChildNodes(); if (nodeList != null && nodeList.getLength() > 0) { if (nodeList.getLength() < childIndex) { return null; } Node childNode = null; if (!forward) { --childIndex; } childNode = nodeList.item(childIndex); if (childNode != null) { return childNode; } return getNeighbor(parent, forward); } if (parent.getNodeType() == Node.TEXT_NODE) { return getNeighbor(parent, forward); } return null; } /** * To see whether the textSelection start and end are on the same. * * @param model * @param textSelection * @return the node */ static boolean isSame(IStructuredModel model, TextSelection textSelection) { if (model != null && textSelection != null) { int t1 = textSelection.getOffset(); int t2 = textSelection.getLength() + t1; return model.getIndexedRegion(t1) == model.getIndexedRegion(t2); } return false; } /** * To see if the range and text selection covered the same range. * * @param model * @param range * @param textSelection * @return true if same */ static boolean isSame(IStructuredModel model, DesignRange range, TextSelection textSelection) { if (model != null && range != null && textSelection != null) { int t1 = textSelection.getOffset(); int t2 = textSelection.getLength() + t1; int r1 = getIndexedRegionLocation(DOMRangeHelper.toDOMRange(range) .getStartPosition()); int r2 = getIndexedRegionLocation(DOMRangeHelper.toDOMRange(range) .getEndPosition()); return (model.getIndexedRegion(t1) == model.getIndexedRegion(r1) && // model.getIndexedRegion(t2) == model.getIndexedRegion(r2)) || (model.getIndexedRegion(t2) == model .getIndexedRegion(r1) && // model.getIndexedRegion(t1) == model.getIndexedRegion(r2)); } return false; } /** * To see whether the selection is single point. * * @param textSelection * @return true if same point */ static boolean isSamePoint(TextSelection textSelection) { return textSelection.getLength() == 0; } /** * To see whether two IDOMPosition are pointed to a same location. * * @param p1 * @param p2 * @return true if same */ public static boolean isSame(IDOMPosition p1, IDOMPosition p2) { if (p1 == p2 || (p1.getContainerNode() == p2.getContainerNode() && p1 .getOffset() == p2.getOffset())) { return true; } return false; } /** * To see whether the range's start and end position are pointed to a same * location. * * @param range * @return true if is same */ public static boolean isSame(DOMRange range) { EditValidateUtil.validRange(range); return isSame(range.getStartPosition(), range.getEndPosition()); } /** * @param range * @return true if same */ public static boolean isSame(DesignRange range) { return isSame(range.getStartPosition(), range.getEndPosition()); } /** * @param p1 * @param p2 * @return true if same */ public static boolean isSame(DesignPosition p1, DesignPosition p2) { if (p1 == p2) { return true; } if (p1.getContainerNode() == p2.getContainerNode() && p1.getOffset() == p2.getOffset()) { return true; } return false; } /** * @param p1 * @param p2 * @return true if p1 and p2 are within same text node */ final boolean isWithinSameText(IDOMPosition p1, IDOMPosition p2) { if (p1 == null || p2 == null) { return false; } return p1.isText() && p2.isText() && p1.getContainerNode() == p2.getContainerNode(); } /** * Get the node absolute start location in its residing IStructuredModel. * * @param p * @return the location */ public static int getIndexedRegionLocation(IDOMPosition p) { if (!EditValidateUtil.validPosition(p)) { return -1; } Node parent = p.getContainerNode(); if (p.isText()) { return ((IndexedRegion) parent).getStartOffset() + p.getOffset(); } int index = p.getOffset(); if (!parent.hasChildNodes()) { // Element: if (!isDocument(parent)) { IStructuredDocumentRegion region = ((IDOMNode) parent) .getStartStructuredDocumentRegion(); return region.getEnd(); } // Document node: int offset = ((IndexedRegion) parent).getStartOffset(); return offset; } NodeList children = parent.getChildNodes(); // After rightmost child if (children.getLength() == index) { if (!isDocument(parent)) { int pos = getNodeEndNameStartIndex(parent); return pos; } int offset = ((IndexedRegion) parent).getEndOffset(); return offset; } Node node = children.item(index); return ((IndexedRegion) node).getStartOffset(); } /** * To determine whether the position is at the edge of a node. TODO: temp * func for later combination * @param nodePos * * @param position * @param left * @return true if linked */ boolean isLinked(IDOMPosition nodePos, IDOMPosition position, boolean left) { int index = getIndexedRegionLocation(position); if (left) { int pos = getIndexedRegionLocation(nodePos); return pos == index; } Node node = null; int end; if (nodePos.isText()) { node = nodePos.getContainerNode(); end = ((IndexedRegion) node).getEndOffset(); } else { node = nodePos.getNextSiblingNode(); Assert.isTrue(node != null); end = ((IndexedRegion) node).getEndOffset(); } return end == index; } /** * To see if the location is at the node's indexed pos, posType can be * START_INDEX_BEFORE_TAG, END_INDEX_WITHIN_TAG When location is at these * two position, webtools returns the container tag name, so we need to know * these. * * @param location * @param node * @return true if at edge */ boolean isAtNodeNameEdge(int location, Node node, int posType) { int start = getNodeEndNameStartIndex(node); return location == start; } /** * @param location * @param node * @return true if at edge */ public boolean isAtNodeNameEdge(int location, Node node) { return isAtNodeNameEdge(location, node, START_INDEX_BEFORE_TAG) || isAtNodeNameEdge(location, node, END_INDEX_WITHIN_TAG); } /** * If text only contains chars '\r' or '\n', it is considered to be * transparent. * * @param node * @return true if transparent text */ public static boolean isTransparentText(Node node) { // should valid non null? Assert.isTrue(node != null); if (node == null || !isText(node)) { return false; } if (!EditValidateUtil.validText(node)) { return false; } Text text = (Text) node; if (text.getLength() == 0) { return true; } String value = text.getNodeValue(); int i = 0; while (i < value.length() && HTMLUtil.isHTMLWhitespace(value.charAt(i))) { i++; } return i == value.length(); } /** * Get node index in its parent's children. * * @param node * @return the node index or -1 if not found */ static int getNodeIndex(Node node) { EditValidateUtil.validNode(node); Node parent = node.getParentNode(); int index = 0; for (Node child = parent.getFirstChild(); child != null; child = child .getNextSibling()) { if (child == node) { return index; } index++; } return -1; // error } /** * If parent has more than one children of which each node's localName is * the same as of 'node', return the index of 'node' in the same type * children list. * * @param node * @return the index */ public int getSameTypeNodeIndex(Node node) { EditValidateUtil.validNode(node); int i = 0; while (node != null) { Node sibling = node.getPreviousSibling(); if (sibling != null && sibling.getLocalName() != null && sibling.getLocalName().equals(node.getLocalName())) { i++; } node = sibling; } return i; // error } /** * Start from position, skip transparent chars, and returns the first * non-transparent char's index. based on 'forward', go previously or next . * * @param value * @param position * @param forward * @return the position */ int getNextConcretePosition(String value, int position, boolean forward) { if (value == null) { return -1; } if (value.length() == 0) { return 0; } // validate Assert.isTrue(position >= 0 && position <= value.length()); int i = -1; if (forward) { i = position; while (i < value.length() && (value.charAt(i) == SWT.CR || value.charAt(i) == SWT.LF)) { i++; } return i; } i = position - 1; while (i >= 0 && (value.charAt(i) == SWT.CR || value.charAt(i) == SWT.LF)) { i--; } return i + 1; } /** * Get two nodes' lowest common ancestor. * * @param node1 * @param node2 * @return the node */ public Node getCommonAncestor(Node node1, Node node2) { if (node1 == null || node2 == null) { return null; } for (Node na = node1; na != null; na = na.getParentNode()) { for (Node ta = node2; ta != null; ta = ta.getParentNode()) { if (ta == na) return ta; } } return null; // not found } /** * Get lowest common ancestor of two IDOMPositions' container nodes.. * * @param p1 * @param p2 * @return the nodeh */ public Node getCommonAncestor(IDOMPosition p1, IDOMPosition p2) { Node n1 = p1.getContainerNode(); Node n2 = p2.getContainerNode(); return getCommonAncestor(n1, n2); } /** * Get lowest ancestor of a 'node' which is block type. * * @param node * @return the node */ Node getBlockAncestor(Node node) { if (!EditValidateUtil.validNode(node)) { return null; } while (node != null && isChild(IHTMLConstants.TAG_BODY, node, true)) { if (isBlockNode(node)) { return node; } node = node.getParentNode(); } return null; } /** * To see whether a node is block type. * * @param node * @return true if is a block node */ public static boolean isBlockNode(Node node) { return !isInline(node); } /** * @param node * @return true if is table cell */ static boolean isTableCell(Node node) { if (node instanceof INodeNotifier) { Object adapter = ((INodeNotifier) node) .getAdapterFor(ICSSStyle.class); if (adapter != null) { ICSSStyle style = (ICSSStyle) adapter; String display = style.getDisplay(); return display.equalsIgnoreCase(ICSSPropertyID.VAL_TABLE_CELL); } } return false; } /** * To see if a node's display type is inline. * * @param refNode * @return true if is inline */ public static boolean isInline(Node refNode) { Node node = refNode; EditPart part = Target.resolvePart(node); if (part instanceof ElementEditPart) { node = ((ElementEditPart) part).getTagConvert().getResultElement(); } if (isText(node)) { return true; } else if (node instanceof INodeNotifier) { Object adapter = ((INodeNotifier) node) .getAdapterFor(ICSSStyle.class); if (adapter != null) { ICSSStyle style = (ICSSStyle) adapter; String display = style.getDisplay(); return (display.equalsIgnoreCase(ICSSPropertyID.VAL_INLINE) || // display .equalsIgnoreCase(ICSSPropertyID.VAL_INLINE_TABLE) || // display.equalsIgnoreCase(ICSSPropertyID.VAL_COMPACT) || // display.equalsIgnoreCase(ICSSPropertyID.VAL_RUN_IN)); } } return false; } /** * @param node * @return true if is list item */ public static boolean isListItem(Node node) { if (node instanceof INodeNotifier) { Object adapter = ((INodeNotifier) node) .getAdapterFor(ICSSStyle.class); if (adapter != null) { ICSSStyle style = (ICSSStyle) adapter; String display = style.getDisplay(); return (display.equalsIgnoreCase(ICSSPropertyID.VAL_LIST_ITEM)); } } return false; } /** * The net effect of this method is that (subject to flags), true * is returned if 'node' is the descendant (not really a child) * of a tag with a local name in the list of 'names'. * * TODO C.B: I hate method. Need to rename and possibly rewrite * * @param names -- check this list for valid local names * @param node -- the node to check * @param ignoreCase -- if true, each node name has toLowerCase applied to * it before checking for it names. NOTE: this assumes that names is already * in lowercase. TODO: this is crappy assumption * @param noSame -- if true, then node is skipped and only its parent nodes are * checked * @return true if the local name of node or one of its parents * is in the array of Strings called names. */ public static boolean isChild(final String names[], Node node, boolean ignoreCase, boolean noSame) { if (node == null) { return false; } if (noSame) { node = node.getParentNode(); } final List namesAsList = Arrays.asList(names); while (node != null && !isDocument(node)) { String nodeName = node.getLocalName(); if (nodeName != null) { if (ignoreCase) { nodeName = nodeName.toLowerCase(); } if (namesAsList.contains(nodeName)) { return true; } } Node oldNode = node; node = node.getParentNode(); if (oldNode == node) { throw new IllegalStateException("Infinite loop discovered in DOM tree"); //$NON-NLS-1$ } } return false; } /** * Determine whether a node is a child of node that is named as 'name', if * the node itself is named as 'name' return true also. * * @param name * @param node * @param ignoreCase * @return true if is child */ public static boolean isChild(String name, Node node, boolean ignoreCase) { if (node == null) { return false; } while (node != null && node.getNodeType() != Node.DOCUMENT_NODE) { String nodeName = node.getLocalName(); if (nodeName != null && (ignoreCase && name.equalsIgnoreCase(nodeName) || !ignoreCase && name.equalsIgnoreCase(nodeName))) { return true; } node = node.getParentNode(); } return false; } /** * To see whether 'node' is 'ancestor's child. * * @param ancestor * @param node * @return true if is child */ public static boolean isChild(Node ancestor, Node node) { if (node == null || ancestor == null) { return false; } if (isDocument(ancestor)) { return true; } while (node != null && !isDocument(ancestor)) { if (node == ancestor) { return true; } node = node.getParentNode(); } return false; } /** * Get next sibling node to position's container node. * * @param position * @return the node */ Node getNextSibling(IDOMPosition position) { if (position.isText()) { return position.getContainerNode().getNextSibling(); } return position.getNextSiblingNode(); } /** * Get previous sibling node to position's container node. * * @param position * @return the node */ Node getPreviousSibling(IDOMPosition position) { if (position.isText()) { return position.getContainerNode().getPreviousSibling(); } return position.getPreviousSiblingNode(); } /** * Get position's container node's parent. * * @param position * @return the parent node */ Node getParent(IDOMPosition position) { if (position.isText()) { return position.getContainerNode().getParentNode(); } return position.getContainerNode(); } /** * Get node's sibling according to 'forward' direction * * @param node * @param forward * @return the node */ public Node getSibling(Node node, boolean forward) { EditValidateUtil.validNode(node); if (forward) { return node.getNextSibling(); } return node.getPreviousSibling(); } /** * Get position's container node's sibling. * * @param position * @param forward * @return the node */ public Node getSibling(IDOMPosition position, boolean forward) { if (forward) { return getNextSibling(position); } return getPreviousSibling(position); } /** * Get position's container node's editable items number. this is temp * functions for future use. * * @param position * @return the size */ int getSize(IDOMPosition position) { EditValidateUtil.validPosition(position); if (position.isText()) { return ((Text) position.getContainerNode()).getLength(); } if (position.getContainerNode().hasChildNodes()) { return position.getContainerNode().getChildNodes().getLength(); } return 0; } /** * Valid position and return text, if it contains text node. * * @param position * @return the text */ public Text getText(IDOMPosition position) { if (position.isText()) { if (position.getContainerNode() != null) { return (Text) position.getContainerNode(); } } return null; } /** * @param node * @return the document for node */ public static Document getDocumentNode(Node node) { if (node != null) { return isDocument(node) ? (Document) node : node.getOwnerDocument(); } return null; } /** * To see whether a node is empty, here we can insert rules to see whether * it is empty, for delete operation, it could be deleted. * * @param node * @return true if node empty */ static boolean isEmptyNode(Node node) { if (node.getNodeType() == Node.TEXT_NODE) { return isTransparentText(node); } if (node.getChildNodes() == null || node.getChildNodes().getLength() == 0) { return true; } return false; } /** * To see whther a node is text node. * * @param node * @return true if the node is a text node */ public static boolean isText(Node node) { return node != null && node.getNodeType() == Node.TEXT_NODE; } /** * To see whether a node is Document node. * * @param node * @return true if the node is a doc node */ public static boolean isDocument(Node node) { return node != null && node.getNodeType() == Node.DOCUMENT_NODE; } /** * Get style from parent node. from first paret 'firstF', we will traverse * the tree up untile reaching Document node, get all style node's, we may * insert rules here to stop the search at a before paricular node. Style * nodes could <b>, <u>... * * @param children * @param firstF */ void assignFather(Vector children, Node firstF) { if (children.size() == 0) { return; } if (firstF != null && !isDocument(firstF)) { String name = firstF.getNodeName(); // To see whether it is a style node that is our anticipated node. if (name != null && HTML_STYLE_NODES.contains(name.toLowerCase())) { Node newParent = firstF.cloneNode(false); while (children.size() > 0) { newParent.appendChild((Node) children.remove(0)); } children.add(newParent); } assignFather(children, firstF.getParentNode()); } } /** * Get a node that is at Indexed position 'pos' in 'model'. * * @param model * @param pos * @return the */ IndexedRegion getPosNode(IStructuredModel model, int pos) { IndexedRegion inode = model.getIndexedRegion(pos); return inode; } /** * If the pos is at right edge within container. * * @param node * @param pos * @return true is at right edge */ boolean isAtRightMostWithin(Node node, int pos) { return getNodeEndNameStartIndex(node) == pos; } /** * Create the node, if 'refNode' is null, then position is at the edge of * 'container'. otherwize calculate refNode's related index in its parent's * children list and return DOMPosition. * * @param container * @param refNode * @param forward * @return the dom position */ IDOMPosition createDomposition(Node container, Node refNode, boolean forward) { if (refNode == null) { if (forward && container.hasChildNodes()) { return new DOMPosition(container, container.getChildNodes() .getLength()); } return new DOMPosition(container, 0); } Assert.isTrue(refNode.getParentNode() == container); int index = getNodeIndex(refNode); if (!forward) { index++; } return new DOMPosition(container, index); } /** * @param fModel * @param textSelection * @return the design range */ static DesignRange convertToDesignRange(IStructuredModel fModel, TextSelection textSelection) { int start = textSelection.getOffset(); int end = textSelection.getLength() + start; IDOMPosition startDomPos = EditModelQuery.getInstance() .createDomposition((IDOMModel) fModel, start, false); IDOMPosition endDomPos = EditModelQuery.getInstance() .createDomposition((IDOMModel) fModel, end, false); if (startDomPos == null) { startDomPos = endDomPos; } else if (endDomPos == null) { endDomPos = startDomPos; } if (startDomPos != null) { DesignPosition startPos = null, endPos = null; startPos = DOMPositionHelper.toDesignPosition(startDomPos); endPos = DOMPositionHelper.toDesignPosition(endDomPos); if (startPos != null) { return new DesignRange(startPos, endPos); } } return null; } /** * Create IDOMPosition based on Indexed 'position' in model. If node at * position is text, use position to calculate DOMPosition offset, * otherwize, simply create position pointed to container's children list's * edge. * * @param model * @param position * @param adjust * @return the dom position */ public IDOMPosition createDomposition(IDOMModel model, int position, boolean adjust) { return createDomposition1(model, position, adjust); } /** * Create IDOMPosition based on Indexed 'position' in model. If node at * position is text, use position to calculate DOMPosition offset, * otherwize, simply create position pointed to container's children list's * edge. * * @param model * @param position * @param adjust * @return the dom position */ public IDOMPosition createDomposition1(IDOMModel model, int position, boolean adjust) { try { // get the container Object object = getPosNode(model, position); if (object == null && position > 0) { // The end of file? object = getPosNode(model, position - 1); } Node container = null; if (object == null) { // empty file? return new DOMPosition(model.getDocument(), 0); } container = (Node) object; Object oppNode = getPosNode(model, position - 1); if (oppNode != null && !EditModelQuery.isChild((Node) oppNode, container) && // !EditModelQuery.isInline(container) && EditModelQuery.isInline((Node) oppNode)) { container = (Node) oppNode; } int location = EditHelper.getInstance().getLocation(container, position, false); IDOMPosition result = null; switch (location) { case 1: case 2: result = new DOMRefPosition(container, false); break; case 4: case 5: result = new DOMRefPosition(container, true); break; case 3: if (EditModelQuery.isText(container)) { result = new DOMPosition(container, position - EditModelQuery.getNodeStartIndex(container)); } else { result = new DOMPosition(container, container .getChildNodes().getLength()); } } return result; } catch (Exception e) { // "Error in position creation" _log.error("Error.EditModelQuery.0" + e); //$NON-NLS-1$ return null; } } /** * Calculate node's Indexed length in model. * * @param node * @return the node length */ public static int getNodeLenth(Node node) { if (node != null && EditValidateUtil.validNode(node)) { return ((IndexedRegion) node).getEndOffset() - ((IndexedRegion) node).getStartOffset(); } return 0; } /** * Return 'node' indexed start position Example: |<a></a>, the position is * indicated by '|' * * @param node * @return the start index */ public static int getNodeStartIndex(Node node) { if (EditValidateUtil.validNode(node) && node instanceof IndexedRegion) { return ((IndexedRegion) node).getStartOffset(); } return -1; } /** * Return 'node' indexed end position Example: <a></a>|, the position is * indicated by '|' * * @param node * @return the end index */ public static int getNodeEndIndex(Node node) { if (EditValidateUtil.validNode(node) && node instanceof IndexedRegion) { return ((IndexedRegion) node).getEndOffset(); } return -1; } /** * Get node at indexed position. * * @param model * @param position * @return the node at position */ static Node getNodeAt(IStructuredModel model, int position) { try { IndexedRegion region = model.getIndexedRegion(position); if (region instanceof Node) { return (Node) region; } return null; } catch (Exception e) { // "Error in region node creation" _log.error("Error.EditModelQuery.1", e); //$NON-NLS-1$ return null; } } /** * Return 'node' indexed start name's end position Example: <a>|aaa </a>, * the position is indicated by '|' * * @param node * @return the index */ public static int getNodeStartNameEndIndex(Node node) { if (isText(node)) { return getNodeStartIndex(node); } if (EditValidateUtil.validNode(node) && node instanceof IDOMNode) { IStructuredDocumentRegion region = ((IDOMNode) node) .getStartStructuredDocumentRegion(); if (region != null) { return region.getEndOffset(); } // else // { // // if (node.hasChildNodes()) // // { // // // Node should always have start name, so this part should // never reach, // // // the assert is for inner debug. // // Assert.isTrue(false); // // return getNodeStartIndex(node); // // } // } } // This should never happen. return getNodeStartIndex(node); } /** * Return 'node' indexed end name' start position Example: <a>aaa| </a>, the * position is indicated by '|' If node is <a /> style or there is no </a> * to pair with <a>, the function return -1. * * @param node * @return the start index */ public static int getNodeEndNameStartIndex(Node node) { if (isText(node)) { return getNodeEndIndex(node); } if (EditValidateUtil.validNode(node) && node instanceof IDOMNode) { IStructuredDocumentRegion region = ((IDOMNode) node) .getEndStructuredDocumentRegion(); if (region != null) { return region.getStartOffset(); } // else // { // if (node.hasChildNodes()) // { // return getNodeEndIndex(node); // } // } } return getNodeEndIndex(node); } /** * To see if a node is <a/>style. * * @param node * @return the single region node */ public boolean isSingleRegionNode(Node node) { if (getNodeEndNameStartIndex(node) == getNodeEndIndex(node) && !node.hasChildNodes()) { return true; } return false; } /** * To see if a node has child that is not transparent child only. * * @param node * @return true if node has transparent children */ boolean hasNonTransparentChild(Node node) { NodeList children = node.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Object child = children.item(i); if (isText((Node) child)) { if (!isTransparentText((Node) child)) { return true; } } else { return true; } } return false; } /** * To see if a node has child that is not transparent child only. * * @param node * @param excludes * @return true if has transparent child */ public boolean hasNonTransparentChild(Node node, String[] excludes) { if (!node.hasChildNodes()) { return false; } NodeList children = node.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Object child = children.item(i); if (isText((Node) child)) { if (!isTransparentText((Node) child)) { return true; } } else if (!Arrays.asList(excludes).contains( ((Node) child).getLocalName())) { return true; } } return false; } /** * To see whether tag has whitespace char. * * @param node * @return true if has whitespace neighbor */ public boolean hasWhitespaceNeighbor(Node node) { node = getNeighbor(node, true); if (isWidget(node)) { return false; } node = getFirstLeafChild(node); return isTransparentText(node); } /** * @param host * @return true if host is a widget */ public static boolean isWidget(Object host) { boolean result = false; EditPart part = null; if (host instanceof EditPart) { part = (EditPart) host; } else if (host instanceof Node) { part = Target.resolvePart((Node) host); if (part == null) { part = new HTMLEditPartsFactory( (IDOMDocument) getDocumentNode((Node) host)) .createEditPart(null, host); } } if (part instanceof NodeEditPart) { result = ((NodeEditPart) part).isWidget(); } return result; } /** * To combind whitespace chars, only one whitespace string should be create. * * @param node * @return true if node is redundant whitespace */ boolean isRedundantWightspaces(Node node) { if (isTransparentText(node) && hasWhitespaceNeighbor(node)) { return true; } return false; } /** * @param node * @param names * @param ignoreCase * @return true if node has ancestors in the name list * TODO: bad practice */ public static boolean hasAncestor(Node node, String names[], boolean ignoreCase) { Assert.isTrue(names != null); while (node != null && !EditModelQuery.isDocument(node)) { if (isElement(node)) if (containItem(names, node, ignoreCase)) { return true; } node = node.getParentNode(); } return false; } /** * To see if 'node' has ancestor that has name as 'name' * * @param node * @param name * @param ignoreCase * @return true if node has the named ancestor */ public static boolean hasAncestor(Node node, String name, boolean ignoreCase) { Assert.isTrue(name != null); while (node != null && !EditModelQuery.isDocument(node)) { if (node.getNodeName() != null) if ((ignoreCase && name.equalsIgnoreCase(node.getNodeName())) || // (!ignoreCase && name.equals(node.getNodeName()))) { return true; } node = node.getParentNode(); } return false; } /** * To see if 'node' has direct ancestors that has names listed in 'name[]' * * @param node * @param top * @param ignoreCase * @return the list of ancestors */ public static List getAncestors(Node node, String top, boolean ignoreCase) { List result = new ArrayList(); Assert.isTrue(node != null); while (node != null && !EditModelQuery.isDocument(node)) { result.add(node); String name = node.getLocalName(); if (ignoreCase && top.equalsIgnoreCase(name) || // (!ignoreCase && top.equals(name))) { break; } node = node.getParentNode(); } return result; } /** * Copy old node's children to newNode.If the newNode is the father of the * old node,then the old node's children will be inserted before the old * node,otherwise,the old node's children just append to the newNode. * * @param old * @param newNode */ public static void copyChildren(Node old, Node newNode) { Node child = old.getFirstChild(); while (child != null) { Node next = child.getNextSibling(); child = old.removeChild(child); if (old.getParentNode() == newNode) { newNode.insertBefore(child, old); } else { newNode.appendChild(child); } child = next; } } /** * @param node * @return true if node is an element */ private static boolean isElement(Node node) { return node.getNodeType() == Node.ELEMENT_NODE; } /** * Return a offspring of ancestor, the offsprint has a name listed in * childrenNames. * * @param ancestor * @param childrenNames * @param maxLevelToSearch * the max level from ancestor to the offspring in family tree. * @param ignoreCase * @return the node */ public static Node getChild(Node ancestor, String childrenNames[], int maxLevelToSearch, boolean ignoreCase) { if (ancestor == null || maxLevelToSearch < 0) { return null; } if (ancestor.getLocalName() != null && ignoreCase && Arrays.asList(childrenNames).contains( ancestor.getLocalName().toLowerCase()) || !ignoreCase && Arrays.asList(childrenNames).contains( ancestor.getLocalName())) { return ancestor; } NodeList children = ancestor.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node result = getChild(children.item(i), childrenNames, maxLevelToSearch - 1, ignoreCase); if (result != null) { return result; } } return null; } /** * Return a offspring of ancestor, the nodes on the tree are type of * DeferredElementImpl, the offsprint has a name listed in childrenNames. * * @param ancestor * @param childrenNames * @param maxLevelToSearch * the max level from ancestor to the offspring in family tree. * @param ignoreCase * @return the node */ static Node getChildDeferredNode(Node ancestor, String childrenNames[], int maxLevelToSearch, boolean ignoreCase) { if (ancestor == null || maxLevelToSearch < 0) { return null; } String nodeName = ancestor.getNodeName(); if (nodeName != null && ignoreCase && Arrays.asList(childrenNames).contains(nodeName) || !ignoreCase && Arrays.asList(childrenNames).contains(nodeName)) { return ancestor; } NodeList children = ancestor.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node result = getChildDeferredNode(children.item(i), childrenNames, maxLevelToSearch - 1, ignoreCase); if (result != null) { return result; } } return null; } /** * @param node * @return if has tranparent node only */ public static boolean hasTransparentNodeOnly(Node node) { NodeList children = node.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { if (!EditModelQuery.isTransparentText(children.item(i))) { return false; } } return true; } /** * @param name * @param node * @param ignoreCase * @return the node */ public static Node getParent(String name, Node node, boolean ignoreCase) { if (node == null) { return null; } while (node != null && node.getNodeType() != Node.DOCUMENT_NODE) { String nodeName = node.getLocalName(); if (nodeName != null && (ignoreCase && name.equalsIgnoreCase(nodeName) || !ignoreCase && name.equalsIgnoreCase(nodeName))) { return node; } node = node.getParentNode(); } return null; } /** * get Elements with the same localName as the input localName under the * rootNode,it is a recursive computation. * * @param rootNode * @param localName * @param caseSensitive * @param list * The input list to hold the matched elements. */ public static void getElementByLocalName(Node rootNode, String localName, boolean caseSensitive, List list) { if (list == null) { return; } NodeList nodeList = rootNode.getChildNodes(); if (nodeList != null && nodeList.getLength() > 0) { for (int i = 0, size = nodeList.getLength(); i < size; i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { String nodeLocalName = node.getLocalName(); if (caseSensitive && localName.equals(nodeLocalName)) { list.add(node); } else if (!caseSensitive && localName.equalsIgnoreCase(nodeLocalName)) { list.add(node); } getElementByLocalName(node, localName, true, list); } } } } /** * @param tags * @param node * @param ignoreCase * @return the true if contains item */ public static boolean containItem(String[] tags, Node node, boolean ignoreCase) { if (ignoreCase) { for (int i = 0, size = tags.length; i < size; i++) { if (tags[i] == null) { continue; } if (tags[i].equalsIgnoreCase(node.getNodeName())) { return true; } } } else { for (int i = 0, size = tags.length; i < size; i++) { if (tags[i] == null) { continue; } if (tags[i].equals(node.getNodeName())) { return true; } } } return false; } }