/*******************************************************************************
* Copyright (c) 2004, 2008 John Krasnay 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:
* John Krasnay - initial API and implementation
*******************************************************************************/
package net.sf.vex.layout;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import net.sf.vex.core.IntRange;
import net.sf.vex.css.CSS;
import net.sf.vex.css.StyleSheet;
import net.sf.vex.css.Styles;
import net.sf.vex.dom.Element;
import net.sf.vex.dom.Node;
/**
* Tools for layout and rendering of CSS-styled boxes
*/
public class LayoutUtils {
/**
* Create a List of generated inline boxes for the given pseudo-element.
* @param context LayoutContext in use
* @param pseudoElement Element representing the generated content.
*/
public static List createGeneratedInlines(LayoutContext context, Element pseudoElement) {
String text = getGeneratedContent(context, pseudoElement);
List list = new ArrayList();
if (text.length() > 0) {
list.add(new StaticTextBox(context, pseudoElement, text));
}
return list;
}
/**
* Returns true if the given offset falls within the given element or range.
*
* @param elementOrRange Element or IntRange object representing a range
* of offsets.
* @param offset Offset to test.
*/
public static boolean elementOrRangeContains(Object elementOrRange, int offset) {
if (elementOrRange instanceof Element) {
Element element = (Element) elementOrRange;
return offset > element.getStartOffset() && offset <= element.getEndOffset();
} else {
IntRange range = (IntRange) elementOrRange;
return offset >= range.getStart() && offset <= range.getEnd();
}
}
/**
* Creates a string representing the generated content for the given
* pseudo-element.
* @param context LayoutContext in use
* @param pseudoElement PseudoElement for which the generated content
* is to be returned.
*/
private static String getGeneratedContent(LayoutContext context, Element pseudoElement) {
Styles styles = context.getStyleSheet().getStyles(pseudoElement);
List content = styles.getContent();
StringBuffer sb = new StringBuffer();
for (Iterator it = content.iterator(); it.hasNext(); ) {
sb.append((String) it.next()); // TODO: change to ContentPart
}
return sb.toString();
}
/**
* Call the given callback for each child matching one of the given
* display styles. Any nodes that do not match one of the given display types
* cause the onRange callback to be called, with a range covering all such
* contiguous nodes.
*
* @param context LayoutContext to use.
* @param displayStyles Display types to be explicitly recognized.
* @param element Element containing the children over which to iterate.
* @param startOffset Starting offset of the range containing nodes in which we're interested.
* @param endOffset Ending offset of the range containing nodes in which we're interested.
* @param callback DisplayStyleCallback through which the caller is notified
* of matching elements and non-matching ranges.
*/
public static void iterateChildrenByDisplayStyle(StyleSheet styleSheet, Set displayStyles, Element element, int startOffset, int endOffset, ElementOrRangeCallback callback) {
List nonMatching = new ArrayList();
Node[] nodes = element.getChildNodes();
for (int i = 0; i < nodes.length; i++) {
if (nodes[i].getEndOffset() <= startOffset) {
continue;
} else if (nodes[i].getStartOffset() >= endOffset) {
break;
} else {
Node node = nodes[i];
if (node instanceof Element) {
Element childElement = (Element) node;
String display = styleSheet.getStyles(childElement).getDisplay();
if (displayStyles.contains(display)) {
if (nonMatching.size() > 0) {
Node firstNode = (Node) nonMatching.get(0);
Node lastNode = (Node) nonMatching.get(nonMatching.size() - 1);
if (lastNode instanceof Element) {
callback.onRange(element, firstNode.getStartOffset(), lastNode.getEndOffset() + 1);
} else {
callback.onRange(element, firstNode.getStartOffset(), lastNode.getEndOffset());
}
nonMatching.clear();
}
callback.onElement(childElement, display);
} else {
nonMatching.add(node);
}
} else {
nonMatching.add(node);
}
}
}
if (nonMatching.size() > 0) {
Node firstNode = (Node) nonMatching.get(0);
Node lastNode = (Node) nonMatching.get(nonMatching.size() - 1);
if (lastNode instanceof Element) {
callback.onRange(element, firstNode.getStartOffset(), lastNode.getEndOffset() + 1);
} else {
callback.onRange(element, firstNode.getStartOffset(), lastNode.getEndOffset());
}
}
}
/**
* Call the given callback for each child matching one of the given
* display styles. Any nodes that do not match one of the given display types
* cause the onRange callback to be called, with a range covering all such
* contiguous nodes.
*
* @param context LayoutContext to use.
* @param displayStyles Display types to be explicitly recognized.
* @param element Element containing the children over which to iterate.
* @param callback DisplayStyleCallback through which the caller is notified
* of matching elements and non-matching ranges.
*/
public static void iterateChildrenByDisplayStyle(StyleSheet styleSheet, Set displayStyles, Element element, ElementOrRangeCallback callback) {
iterateChildrenByDisplayStyle(styleSheet, displayStyles, element, element.getStartOffset() + 1, element.getEndOffset(), callback);
}
/**
* Returns true if the given styles represent an element that can be
* the child of a table element.
*
* @param styleSheet StyleSheet to use.
* @param element Element to test.
*/
public static boolean isTableChild(StyleSheet styleSheet, Element element) {
String display = styleSheet.getStyles(element).getDisplay();
return TABLE_CHILD_STYLES.contains(display);
}
public static void iterateTableRows(final StyleSheet styleSheet, final Element element, int startOffset, int endOffset, final ElementOrRangeCallback callback) {
iterateChildrenByDisplayStyle(styleSheet, nonRowStyles, element, startOffset, endOffset, new ElementOrRangeCallback() {
public void onElement(Element child, String displayStyle) {
if (displayStyle.equals(CSS.TABLE_ROW_GROUP)
|| displayStyle.equals(CSS.TABLE_HEADER_GROUP)
|| displayStyle.equals(CSS.TABLE_FOOTER_GROUP)) {
// iterate rows in group
iterateChildrenByDisplayStyle(styleSheet, rowStyles, child, child.getStartOffset() + 1, child.getEndOffset(), callback);
} else {
// other element's can't contain rows
}
}
public void onRange(Element parent, int startOffset, int endOffset) {
// iterate over rows in range
iterateChildrenByDisplayStyle(styleSheet, rowStyles, element, startOffset, endOffset, callback);
}
});
}
public static void iterateTableCells(StyleSheet styleSheet, Element element, int startOffset, int endOffset, final ElementOrRangeCallback callback) {
iterateChildrenByDisplayStyle(styleSheet, cellStyles, element, startOffset, endOffset, callback);
}
public static void iterateTableCells(StyleSheet styleSheet, Element element, final ElementOrRangeCallback callback) {
iterateChildrenByDisplayStyle(styleSheet, cellStyles, element, element.getStartOffset(), element.getEndOffset(), callback);
}
/**
* Set of CSS display values that represent elements that can be children
* of table elements.
*/
public static Set TABLE_CHILD_STYLES = new HashSet();
private static Set nonRowStyles = new HashSet();
private static Set rowStyles = new HashSet();
private static Set cellStyles = new HashSet();
static {
nonRowStyles.add(CSS.TABLE_CAPTION);
nonRowStyles.add(CSS.TABLE_COLUMN);
nonRowStyles.add(CSS.TABLE_COLUMN_GROUP);
nonRowStyles.add(CSS.TABLE_ROW_GROUP);
nonRowStyles.add(CSS.TABLE_HEADER_GROUP);
nonRowStyles.add(CSS.TABLE_FOOTER_GROUP);
rowStyles.add(CSS.TABLE_ROW);
cellStyles.add(CSS.TABLE_CELL);
TABLE_CHILD_STYLES.addAll(nonRowStyles);
TABLE_CHILD_STYLES.addAll(rowStyles);
TABLE_CHILD_STYLES.addAll(cellStyles);
}
}