blob: d7bd274828b3b4151a38f58c4157731df7ef5e75 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012 Florian Thienel 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:
* Florian Thienel - initial API and implementation
*******************************************************************************/
package org.eclipse.vex.core.internal.dom;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.Assert;
/**
* A Parent node is a Node which can contain other nodes as children. This class defines the tree-like structure of the
* DOM. It handles the mergin of the child nodes and the textual content of one node within the structure of the
* document.
*
* @author Florian Thienel
*/
public abstract class Parent extends Node {
private final List<Node> children = new ArrayList<Node>();
/**
* Append the given child node to the end of the list of children. The parent attribute of the child is set to this
* node.
*
* @param child
* the new child node
*/
public void addChild(final Node child) {
children.add(child);
child.setParent(this);
}
/**
* Insert the given child node into the list of children at the given index. The children from the child with the
* given index until the last child will be moved forward by one position. The parent attribute of the child is set
* to this node.
*
* @param index
* the index at which the child should be inserted.
* @param child
* the child node to insert
*/
public void insertChild(final int index, final Node child) {
children.add(index, child);
child.setParent(this);
}
public int getIndexOfChildNextTo(final int offset) {
final ContentRange insertionRange = getRange().resizeBy(1, 0);
Assert.isTrue(insertionRange.contains(offset), MessageFormat.format("The offset must be within {0}.", insertionRange));
int i = 0;
for (final Iterator<Node> iterator = children.iterator(); iterator.hasNext(); i++) {
final Node child = iterator.next();
if (offset <= child.getStartOffset()) {
return i;
}
}
return children.size();
}
/**
* Remove the given child node from the list of children. The parent attribute of the child will be set to null.
*
* @param child
* the child node to remove
*/
public void removeChild(final Node child) {
children.remove(child);
child.setParent(null);
}
/**
* Returns a list of all child nodes of this parent node, including Text nodes for the textual content.
*
* @return all child nodes including Text nodes
*/
public List<Node> getChildNodes() {
if (!isAssociated()) {
return Collections.unmodifiableList(children);
}
return getChildNodes(getRange());
}
/**
* Returns a list of all child nodes (including Text nodes) in the given range. The Text nodes are cut at the edges,
* all other nodes must be fully contained in the range (i.e. the start tag and the end tag). The returned list is
* not modifyable.
*
* @param startOffset
* the start offset of the range
* @param endOffset
* the end offset of the range
* @return all child nodes which are completely within the given range plus the textual content
*/
public List<Node> getChildNodes(final ContentRange range) {
final List<Node> result = new ArrayList<Node>();
final ContentRange trimmedRange = range.intersection(getRange());
int textStart = trimmedRange.getStartOffset();
for (final Node child : children) {
if (!child.isAssociated()) {
result.add(child);
} else if (child.isInRange(trimmedRange)) {
mergeTextIntoResult(textStart, child.getStartOffset(), result);
result.add(child);
textStart = child.getEndOffset() + 1;
}
}
mergeTextIntoResult(textStart, trimmedRange.getEndOffset(), result);
return Collections.unmodifiableList(result);
}
private void mergeTextIntoResult(final int startOffset, final int endOffset, final List<Node> result) {
final int textStart = findNextTextStart(startOffset, endOffset);
final int textEnd = findNextTextEnd(endOffset, textStart);
if (textStart < textEnd) {
result.add(new Text(this, getContent(), new ContentRange(textStart, textEnd)));
} else if (textStart == textEnd && !getContent().isElementMarker(textStart)) {
result.add(new Text(this, getContent(), new ContentRange(textStart, textEnd)));
}
}
private int findNextTextStart(int currentOffset, final int maximumOffset) {
while (currentOffset < maximumOffset && getContent().isElementMarker(currentOffset)) {
currentOffset++;
}
return currentOffset;
}
private int findNextTextEnd(int currentOffset, final int minimumOffset) {
while (currentOffset > minimumOffset && getContent().isElementMarker(currentOffset)) {
currentOffset--;
}
return currentOffset;
}
public List<Node> getChildNodesBefore(final int offset) {
if (offset <= getStartOffset()) {
return Collections.emptyList();
}
return getChildNodes(new ContentRange(getStartOffset() + 1, offset));
}
public List<Node> getChildNodesAfter(final int offset) {
if (offset >= getEndOffset()) {
return Collections.emptyList();
}
return getChildNodes(new ContentRange(offset, getEndOffset() - 1));
}
/**
* An Iterator of all child nodes. The underlying collection is not modifyable.
*
* @see Parent#getChildNodes()
* @see Iterator
* @return an Iterator of all child nodes
*/
public Iterator<Node> getChildIterator() {
return getChildNodes().iterator();
}
/**
* Returns the child node at the given index. This index is based on the list of children without the Text nodes, so
* the child node might have a different index in the list returned by getChildNodes().
*
* @see Parent#getChildNodes()
* @return the child node at the given index
*/
public Node getChildNode(final int index) {
return children.get(index);
}
/**
* Returns the number of child nodes (<b>not</b> including Text nodes) of this parent node.
*
* @return the number of child nodes
*/
public int getChildCount() {
return children.size();
}
/**
* Returns the node at the given offset.
*
* @param offset
* the offset
* @return the node at the given offset
*/
public Node getChildNodeAt(final int offset) {
Assert.isTrue(containsOffset(offset), MessageFormat.format("Offset must be within {0}.", getRange()));
final List<Node> childNodes = getChildNodes();
for (final Node child : childNodes) {
if (child.containsOffset(offset)) {
if (child instanceof Parent) {
return ((Parent) child).getChildNodeAt(offset);
} else {
return child;
}
}
}
return this;
}
/**
* Indicates if this parent node has child nodes. Text nodes are ignored, i.e. this method will return false if this
* parent node contains only text.
*
* @return true if this parent node has child nodes
*/
public boolean hasChildren() {
return !children.isEmpty();
}
}