/**
* Copyright 2008 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.model.document.util;
import org.waveprotocol.wave.model.document.ReadableDocument;
import java.util.Map;
/**
* Various pretty printing methods for dom structures
*
* TODO(user): include selection markers | []
*
* TODO(user): consider merging this with XmlStringBuilder, but
* with care (this code will probably only exist in debug builds whereas
* ContentXmlString will be in release builds as well...)
*
*/
public class Pretty<N> {
/**
* A string builder for building up output
*/
private StringBuilder builder = new StringBuilder();
/**
* Current indentation level
*/
private int indent = 0;
/**
* Selection start + end
*/
private Point.El<N> selStartEl = null, selEndEl = null;
private Point.Tx<N> selStartTx = null, selEndTx = null;
/**
* Flag if selection is collapsed
*/
private boolean collapsed = false;
/**
* Constructor
*/
public Pretty() {}
/**
* Set selection for future prints
*
* @param selStart
* @param selEnd
* @return this
*/
public Pretty<N> select(Point<N> selStart, Point<N> selEnd) {
this.selStartEl = selStart != null ? selStart.asElementPoint() : null;
this.selStartTx = selStart != null ? selStart.asTextPoint() : null;
this.selEndEl = selEnd != null ? selEnd.asElementPoint() : null;
this.selEndTx = selEnd != null ? selEnd.asTextPoint() : null;
collapsed = selStart != null && selStart.equals(selEnd);
return this;
}
/**
* Clear selection
*
* @return this
*/
public Pretty<N> noselect() {
return select(null, null);
}
/**
* @param doc
* @return pretty-print string describing doc + selection
*/
public <E extends N, T extends N> String print(ReadableDocument<N, E, T> doc) {
clear();
appendDocument(doc);
return builder.toString();
}
/**
* @param doc
* @param node
* @return pretty-print string describing node doc + selection
*/
public <E extends N, T extends N> String print(ReadableDocument<N, E, T> doc, N node) {
clear();
appendNode(doc, node);
return builder.toString();
}
/**
* Clears the builder
*/
private void clear() {
indent = 0;
if (builder.length() > 0) {
builder = new StringBuilder();
}
}
/**
* Appends a char sequence to builder
*
* @param sequence
*/
private void append(CharSequence sequence) {
builder.append(sequence);
}
/**
* @param c
*/
private void append(char c) {
builder.append(c);
}
/**
* Appends a newline and spaces to indent next line
*/
private void appendNewLine() {
append("\n");
for (int i = 0; i < indent; ++i) {
append(" ");
}
}
/**
* Appends a document to builder
*
* @param doc
*/
private <E extends N, T extends N>
void appendDocument(ReadableDocument<N, E, T> doc) {
appendElement(doc, doc.getDocumentElement());
}
/**
* @param doc
* @param node
* @return true if node prefers to be output inline
*/
private <E extends N, T extends N> boolean isInline(
ReadableDocument<N, E, T> doc, N node) {
E element = doc.asElement(node);
T text = doc.asText(node);
return text != null || "|b|u|i|".contains("|" + doc.getTagName(element).toLowerCase() + "|");
}
/**
* Appends a node to builder
*
* @param doc
* @param node
*/
private <E extends N, T extends N> void appendNode(
ReadableDocument<N, E, T> doc, N node) {
E element = doc.asElement(node);
if (element != null) {
appendElement(doc, element);
return;
}
T text = doc.asText(node);
if (text != null) {
appendText(doc, text);
return;
}
assert(false);
}
/**
* Appends element's tag name
*
* @param doc
* @param element
*/
private <E extends N, T extends N> void appendTagName(
ReadableDocument<N, E, T> doc, E element) {
append(doc.getTagName(element).toLowerCase());
}
/**
* Appends element's attributes
*
* @param doc
* @param element
*/
private <E extends N, T extends N> void appendAttributes(
ReadableDocument<N, E, T> doc, E element) {
try {
Map<String, String> attributes = doc.getAttributes(element);
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
append(" " + attribute.getKey() + "='" + attribute.getValue() + "'");
}
} catch (Exception e) {
// TODO(user): remove this when + if HtmlViewImpl implements getAttributes
for (String name : new String[] {"class", "src", "id", "type", "name", "for", "href",
"target"}) {
String value = doc.getAttribute(element, name);
if (value != null && value.length() > 0) {
append(" " + name + "='" + value + "'");
}
}
}
}
/**
* Appends an element start tag
*
* @param doc
* @param element
*/
private <E extends N, T extends N> void appendStartTag(
ReadableDocument<N, E, T> doc, E element) {
appendStartTag(doc, element, false);
}
/**
* Appends a potentially self-closing element start tag
*
* @param doc
* @param element
* @param selfClosing
*/
private <E extends N, T extends N> void appendStartTag(
ReadableDocument<N, E, T> doc, E element, boolean selfClosing) {
append("<");
appendTagName(doc, element);
appendAttributes(doc, element);
append(selfClosing ? "/>" : ">");
}
/**
* Appends an element end tag
*
* @param doc
* @param element
*/
private <E extends N, T extends N> void appendEndTag(
ReadableDocument<N, E, T> doc, E element) {
append("</");
appendTagName(doc, element);
append(">");
}
/**
* Appends an element
*
* @param doc
* @param element
*/
private <E extends N, T extends N> void appendElement(
ReadableDocument<N, E, T> doc, E element) {
// Inspect selection's relevance to element
boolean printStartMarker =
selStartEl != null && doc.isSameNode(selStartEl.getContainer(), element);
boolean printEndMarker =
selEndEl != null && !collapsed && doc.isSameNode(selEndEl.getContainer(), element);
String startMarker = collapsed ? "|" : "[";
String endMarker = "]";
// First deal with childless elements
N firstChild = doc.getFirstChild(element);
if (firstChild == null) {
if (printStartMarker && selStartEl.getNodeAfter() == null) {
appendStartTag(doc, element);
append(startMarker);
appendEndTag(doc, element);
} else if (printEndMarker && selEndEl.getNodeAfter() == null) {
appendStartTag(doc, element);
append(endMarker);
appendEndTag(doc, element);
} else {
appendStartTag(doc, element, true);
}
} else {
// Start tag
appendStartTag(doc, element);
// Children
N child = firstChild;
boolean first = true;
++indent;
while (child != null) {
N next = doc.getNextSibling(child);
if ((first && !isInline(doc, element)) || !isInline(doc, child)) {
appendNewLine();
}
if (printStartMarker && doc.isSameNode(selStartEl.getNodeAfter(), child)) {
append(startMarker);
}
appendNode(doc, child);
if (printEndMarker && doc.isSameNode(selEndEl.getNodeAfter(), next)) {
append(endMarker);
}
first = false;
child = next;
}
--indent;
// End tag
if ((!isInline(doc, element)) || !isInline(doc, firstChild)) {
appendNewLine();
}
appendEndTag(doc, element);
}
}
/**
* Appends a text node
*
* @param doc
* @param text
*/
private <E extends N, T extends N> void appendText(
ReadableDocument<N, E, T> doc, T text) {
// The text value to append
String value = displayWhitespace(doc.getData(text));
// Inspect selection's relevance to text node
boolean printStartMarker =
selStartTx != null && doc.isSameNode(selStartTx.getContainer(), text);
boolean printEndMarker =
selEndTx != null && !collapsed && doc.isSameNode(selEndTx.getContainer(), text);
String startMarker =
printStartMarker ? (collapsed ? "|" : "[") : "";
String endMarker = printEndMarker ? "]" : "";
int startOffset = printStartMarker
? Math.min(selStartTx.getTextOffset(), value.length()) : 0;
int endOffset = printEndMarker
? Math.min(selEndTx.getTextOffset(), value.length()) : value.length();
// Append text and selection markers
append('\'');
append(value.substring(0, startOffset));
append(startMarker);
append(value.substring(startOffset, endOffset));
append(endMarker);
append(value.substring(endOffset, value.length()));
append('\'');
}
/**
* @param xml
* @return XML-escaped string
*/
public static String xmlEscape(String xml) {
return xml
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">");
}
/**
* Notice that this function only escape entity reference and not character reference.
* @param xml
* @return the unescaped xml string.
*/
public static String xmlUnescape(String xml) {
return xml.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll(""", "\"")
.replaceAll("'", "'")
.replaceAll("&", "&");
}
/**
* @param attrValue
* @return The escaped xml attribute value
*/
public static String attrEscape(String attrValue) {
return xmlEscape(attrValue)
.replaceAll("\"", """)
.replaceAll("'", "'");
}
/**
* Debug method.
*
* @param string
* @return The input string as an html string that correctly displays
* xml special characters, and spaces
*/
public static String stringToHtml(String string) {
return displayWhitespace(xmlEscape(string));
}
/**
* Debug method.
*
* @param string
* @return html string that displays white spaces;
* space -> a small square
* non-breaking space -> a small, solid square.
* TODO(user): other whitespace?
*/
public static String displayWhitespace(String string) {
return string.replaceAll("\u00A0", "\u25aa")
.replaceAll(" ", "\u25ab");
}
}