/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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 com.android.ide.eclipse.adt.internal.editors.layout.gle2;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.util.Pair;
import org.eclipse.jface.text.IDocument;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.HashSet;
import java.util.Set;
@SuppressWarnings("restriction") // No replacement for restricted XML model yet
public class DomUtilities {
/**
* Returns the XML DOM node corresponding to the given offset of the given
* document.
*
* @param document The document to look in
* @param offset The offset to look up the node for
* @return The node containing the offset, or null
*/
public static Node getNode(IDocument document, int offset) {
Node node = null;
IModelManager modelManager = StructuredModelManager.getModelManager();
if (modelManager == null) {
return null;
}
try {
IStructuredModel model = modelManager.getExistingModelForRead(document);
if (model != null) {
try {
for (; offset >= 0 && node == null; --offset) {
node = (Node) model.getIndexedRegion(offset);
}
} finally {
model.releaseFromRead();
}
}
} catch (Exception e) {
// Ignore exceptions.
}
return node;
}
/**
* Like {@link #getNode(IDocument, int)}, but has a bias parameter which lets you
* indicate whether you want the search to look forwards or backwards.
* This is vital when trying to compute a node range. Consider the following
* XML fragment:
* {@code
* <a/><b/>[<c/><d/><e/>]<f/><g/>
* }
* Suppose we want to locate the nodes in the range indicated by the brackets above.
* If we want to search for the node corresponding to the start position, should
* we pick the node on its left or the node on its right? Similarly for the end
* position. Clearly, we'll need to bias the search towards the right when looking
* for the start position, and towards the left when looking for the end position.
* The following method lets us do just that. When passed an offset which sits
* on the edge of the computed node, it will pick the neighbor based on whether
* "forward" is true or false, where forward means searching towards the right
* and not forward is obviously towards the left.
* @param document the document to search in
* @param offset the offset to search for
* @param forward if true, search forwards, otherwise search backwards when on node boundaries
* @return the node which surrounds the given offset, or the node adjacent to the offset
* where the side depends on the forward parameter
*/
public static Node getNode(IDocument document, int offset, boolean forward) {
Node node = getNode(document, offset);
if (node instanceof IndexedRegion) {
IndexedRegion region = (IndexedRegion) node;
if (!forward && offset <= region.getStartOffset()) {
Node left = node.getPreviousSibling();
if (left == null) {
left = node.getParentNode();
}
node = left;
} else if (forward && offset >= region.getEndOffset()) {
Node right = node.getNextSibling();
if (right == null) {
right = node.getParentNode();
}
node = right;
}
}
return node;
}
/**
* Returns a range of elements for the given caret range. Note that the two elements
* may not be at the same level so callers may want to perform additional input
* filtering.
*
* @param document the document to search in
* @param beginOffset the beginning offset of the range
* @param endOffset the ending offset of the range
* @return a pair of begin+end elements, or null
*/
public static Pair<Element, Element> getElementRange(IDocument document, int beginOffset,
int endOffset) {
Element beginElement = null;
Element endElement = null;
Node beginNode = getNode(document, beginOffset, true);
Node endNode = beginNode;
if (endOffset > beginOffset) {
endNode = getNode(document, endOffset, false);
}
if (beginNode == null || endNode == null) {
return null;
}
// Adjust offsets if you're pointing at text
if (beginNode.getNodeType() != Node.ELEMENT_NODE) {
// <foo> <bar1/> | <bar2/> </foo> => should pick <bar2/>
beginElement = getNextElement(beginNode);
if (beginElement == null) {
// Might be inside the end of a parent, e.g.
// <foo> <bar/> | </foo> => should pick <bar/>
beginElement = getPreviousElement(beginNode);
if (beginElement == null) {
// We must be inside an empty element,
// <foo> | </foo>
// In that case just pick the parent.
beginElement = getParentElement(beginNode);
}
}
} else {
beginElement = (Element) beginNode;
}
if (endNode.getNodeType() != Node.ELEMENT_NODE) {
// In the following, | marks the caret position:
// <foo> <bar1/> | <bar2/> </foo> => should pick <bar1/>
endElement = getPreviousElement(endNode);
if (endElement == null) {
// Might be inside the beginning of a parent, e.g.
// <foo> | <bar/></foo> => should pick <bar/>
endElement = getNextElement(endNode);
if (endElement == null) {
// We must be inside an empty element,
// <foo> | </foo>
// In that case just pick the parent.
endElement = getParentElement(endNode);
}
}
} else {
endElement = (Element) endNode;
}
if (beginElement != null && endElement != null) {
return Pair.of(beginElement, endElement);
}
return null;
}
/**
* Returns the next sibling element of the node, or null if there is no such element
*
* @param node the starting node
* @return the next sibling element, or null
*/
public static Element getNextElement(Node node) {
while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
node = node.getNextSibling();
}
return (Element) node; // may be null as well
}
/**
* Returns the previous sibling element of the node, or null if there is no such element
*
* @param node the starting node
* @return the previous sibling element, or null
*/
public static Element getPreviousElement(Node node) {
while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
node = node.getPreviousSibling();
}
return (Element) node; // may be null as well
}
/**
* Returns the closest ancestor element, or null if none
*
* @param node the starting node
* @return the closest parent element, or null
*/
public static Element getParentElement(Node node) {
while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
node = node.getParentNode();
}
return (Element) node; // may be null as well
}
/**
* Converts the given attribute value to an XML-attribute-safe value, meaning that
* single and double quotes are replaced with their corresponding XML entities.
*
* @param attrValue the value to be escaped
* @return the escaped value
*/
public static String toXmlAttributeValue(String attrValue) {
// Must escape ' and "
if (attrValue.indexOf('"') == -1 && attrValue.indexOf('\'') == -1) {
return attrValue;
}
int n = attrValue.length();
StringBuilder sb = new StringBuilder(2 * n);
for (int i = 0; i < n; i++) {
char c = attrValue.charAt(i);
if (c == '"') {
sb.append("""); //$NON-NLS-1$
} else if (c == '\'') {
sb.append("'"); //$NON-NLS-1$
} else {
sb.append(c);
}
}
return sb.toString();
}
/** Utility used by {@link #getFreeWidgetId(Element)} */
private static void addLowercaseIds(Element root, Set<String> seen) {
if (root.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
String id = root.getAttributeNS(ANDROID_URI, ATTR_ID);
seen.add(id.toLowerCase());
}
}
/**
* Returns a suitable new widget id (not including the {@code @id/} prefix) for the
* given element, which is guaranteed to be unique in this document
*
* @param element the element to compute a new widget id for
* @return a unique id, never null, which does not include the {@code @id/} prefix
* @see DescriptorsUtils#getFreeWidgetId
*/
public static String getFreeWidgetId(Element element) {
Set<String> ids = new HashSet<String>();
addLowercaseIds(element.getOwnerDocument().getDocumentElement(), ids);
String prefix = element.getTagName();
String generated;
int num = 1;
do {
num++;
generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
} while (ids.contains(generated.toLowerCase()));
return generated;
}
}