org.dom4j.io.HTMLWriter Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
*
* This software is open source.
* See the bottom of this file for the licence.
*/
package org.dom4j.io;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Entity;
import org.dom4j.Node;
import org.xml.sax.SAXException;
/**
* HTMLWriter
takes a DOM4J tree and formats it to a stream as
* HTML. This formatter is similar to XMLWriter but it outputs the text of CDATA
* and Entity sections rather than the serialised format as in XML, it has an
* XHTML mode, it retains whitespace in certain elements such as <PRE>,
* and it supports certain elements which have no corresponding close tag such
* as for <BR> and <P>.
*
* The OutputFormat passed in to the constructor is checked for isXHTML() and
* isExpandEmptyElements(). See {@link OutputFormat OutputFormat}for details.
* Here are the rules for this class based on an OutputFormat, "format",
* passed in to the constructor:
*
*
* - If an element is in {@link #getOmitElementCloseSet()
* getOmitElementCloseSet}, then it is treated specially:
*
* - It never expands, since some browsers treat this as two separate
* Horizontal Rules: <HR></HR>
* - If {@link org.dom4j.io.OutputFormat#isXHTML() format.isXHTML()}, then
* it has a space before the closing single-tag slash, since Netscape 4.x-
* treats this: <HR /> as an element named "HR" with an attribute named
* "/", but that's better than when it refuses to recognize this: <hr/>
* which it thinks is an element named "HR/".
*
*
* - If {@link org.dom4j.io.OutputFormat#isXHTML() format.isXHTML()}, all
* elements must have either a close element, or be a closed single tag.
* - If {@link org.dom4j.io.OutputFormat#isExpandEmptyElements()
* format.isExpandEmptyElements()}() is true, all elements are expanded except
* as above.
*
* Examples
* If isXHTML == true, CDATA sections look like this:
*
* <myelement><![CDATA[My data]]></myelement>
*
* Otherwise, they look like this:
*
* <myelement>My data</myelement>
*
* Basically, {@link OutputFormat#isXHTML()} ==
* true
will produce valid XML, while {@link
* OutputFormat#isExpandEmptyElements()} determines whether empty elements are
* expanded if isXHTML is true, excepting the special HTML single tags.
*
* Also, HTMLWriter handles tags whose contents should be preformatted, that is,
* whitespace-preserved. By default, this set includes the tags <PRE>,
* <SCRIPT>, <STYLE>, and <TEXTAREA>, case insensitively. It
* does not include <IFRAME>. Other tags, such as <CODE>,
* <KBD>, <TT>, <VAR>, are usually rendered in a different
* font in most browsers, but don't preserve whitespace, so they also don't
* appear in the default list. HTML Comments are always whitespace-preserved.
* However, the parser you use may store comments with linefeed-only text nodes
* (\n) even if your platform uses another line.separator character, and
* HTMLWriter outputs Comment nodes exactly as the DOM is set up by the parser.
* See examples and discussion here: {@link #setPreformattedTags(java.util.Set)}
*
* Examples
* Pretty Printing
* This example shows how to pretty print a string containing a valid HTML
* document to a string. You can also just call the static methods of this
* class: {@link #prettyPrintHTML(String)}or {@link #prettyPrintHTML(String,boolean,boolean,boolean,boolean)}
* or {@link #prettyPrintXHTML(String)} for XHTML (note the X)
*
*
* String testPrettyPrint(String html) {
* StringWriter sw = new StringWriter();
* OutputFormat format = OutputFormat.createPrettyPrint();
* // These are the default values for createPrettyPrint,
* // so you needn't set them:
* // format.setNewlines(true);
* // format.setTrimText(true);</font>
* format.setXHTML(true);
* HTMLWriter writer = new HTMLWriter(sw, format);
* Document document = DocumentHelper.parseText(html);
* writer.write(document);
* writer.flush();
* return sw.toString();
* }
*
*
* This example shows how to create a "squeezed" document, but one that will
* work in browsers even if the browser line length is limited. No newlines are
* included, no extra whitespace at all, except where it it required by
* {@link #setPreformattedTags(java.util.Set) setPreformattedTags}.
*
*
* String testCrunch(String html) {
* StringWriter sw = new StringWriter();
* OutputFormat format = OutputFormat.createPrettyPrint();
* format.setNewlines(false);
* format.setTrimText(true);
* format.setIndent("");
* format.setXHTML(true);
* format.setExpandEmptyElements(false);
* format.setNewLineAfterNTags(20);
* org.dom4j.io.HTMLWriter writer = new HTMLWriter(sw, format);
* org.dom4j.Document document = DocumentHelper.parseText(html);
* writer.write(document);
* writer.flush();
* return sw.toString();
* }
*
*
* @author James Strachan
* @author Laramie Crocker
* @version $Revision: 1.21 $
*/
public class HTMLWriter extends XMLWriter {
private static String lineSeparator = System.getProperty("line.separator");
protected static final HashSet DEFAULT_PREFORMATTED_TAGS;
static {
// If you change this list, update the javadoc examples, above in the
// class javadoc, in writeElement, and in setPreformattedTags().
DEFAULT_PREFORMATTED_TAGS = new HashSet();
DEFAULT_PREFORMATTED_TAGS.add("PRE");
DEFAULT_PREFORMATTED_TAGS.add("SCRIPT");
DEFAULT_PREFORMATTED_TAGS.add("STYLE");
DEFAULT_PREFORMATTED_TAGS.add("TEXTAREA");
}
protected static final OutputFormat DEFAULT_HTML_FORMAT;
static {
DEFAULT_HTML_FORMAT = new OutputFormat(" ", true);
DEFAULT_HTML_FORMAT.setTrimText(true);
DEFAULT_HTML_FORMAT.setSuppressDeclaration(true);
}
private Stack formatStack = new Stack();
private String lastText = "";
private int tagsOuput = 0;
// legal values are 0+, but -1 signifies lazy initialization.
private int newLineAfterNTags = -1;
private HashSet preformattedTags = DEFAULT_PREFORMATTED_TAGS;
/**
* Used to store the qualified element names which should have no close
* element tag
*/
private HashSet omitElementCloseSet;
public HTMLWriter(Writer writer) {
super(writer, DEFAULT_HTML_FORMAT);
}
public HTMLWriter(Writer writer, OutputFormat format) {
super(writer, format);
}
public HTMLWriter() throws UnsupportedEncodingException {
super(DEFAULT_HTML_FORMAT);
}
public HTMLWriter(OutputFormat format) throws UnsupportedEncodingException {
super(format);
}
public HTMLWriter(OutputStream out) throws UnsupportedEncodingException {
super(out, DEFAULT_HTML_FORMAT);
}
public HTMLWriter(OutputStream out, OutputFormat format)
throws UnsupportedEncodingException {
super(out, format);
}
public void startCDATA() throws SAXException {
}
public void endCDATA() throws SAXException {
}
// Overloaded methods
// added isXHTML() stuff so you get the CDATA brackets if you desire.
protected void writeCDATA(String text) throws IOException {
// XXX: Should we escape entities?
// writer.write( escapeElementEntities( text ) );
if (getOutputFormat().isXHTML()) {
super.writeCDATA(text);
} else {
writer.write(text);
}
lastOutputNodeType = Node.CDATA_SECTION_NODE;
}
protected void writeEntity(Entity entity) throws IOException {
writer.write(entity.getText());
lastOutputNodeType = Node.ENTITY_REFERENCE_NODE;
}
protected void writeDeclaration() throws IOException {
}
protected void writeString(String text) throws IOException {
/*
* DOM stores \n at the end of text nodes that are newlines. This is
* significant if we are in a PRE section. However, we only want to
* output the system line.separator, not \n. This is a little brittle,
* but this function appears to be called with these lineseparators as a
* separate TEXT_NODE. If we are in a preformatted section, output the
* right line.separator, otherwise ditch. If the single \n character is
* not the text, then do the super thing to output the text.
*
* Also, we store the last text that was not a \n since it may be used
* by writeElement in this class to line up preformatted tags.
*/
if (text.equals("\n")) {
if (!formatStack.empty()) {
super.writeString(lineSeparator);
}
return;
}
lastText = text;
if (formatStack.empty()) {
super.writeString(text.trim());
} else {
super.writeString(text);
}
}
/**
* Overriden method to not close certain element names to avoid wierd
* behaviour from browsers for versions up to 5.x
*
* @param qualifiedName
* DOCUMENT ME!
*
* @throws IOException
* DOCUMENT ME!
*/
protected void writeClose(String qualifiedName) throws IOException {
if (!omitElementClose(qualifiedName)) {
super.writeClose(qualifiedName);
}
}
protected void writeEmptyElementClose(String qualifiedName)
throws IOException {
if (getOutputFormat().isXHTML()) {
// xhtml, always check with format object whether to expand or not.
if (omitElementClose(qualifiedName)) {
// it was a special omit tag, do it the XHTML way: "
",
// ignoring the expansion option, since
is OK XML,
// but produces twice the linefeeds desired in the browser.
// for netscape 4.7, though all are fine with it, write a space
// before the close slash.
writer.write(" />");
} else {
super.writeEmptyElementClose(qualifiedName);
}
} else {
// html, not xhtml
if (omitElementClose(qualifiedName)) {
// it was a special omit tag, do it the old html way: "
".
writer.write(">");
} else {
// it was NOT a special omit tag, check with format object
// whether to expand or not.
super.writeEmptyElementClose(qualifiedName);
}
}
}
protected boolean omitElementClose(String qualifiedName) {
return internalGetOmitElementCloseSet().contains(
qualifiedName.toUpperCase());
}
private HashSet internalGetOmitElementCloseSet() {
if (omitElementCloseSet == null) {
omitElementCloseSet = new HashSet();
loadOmitElementCloseSet(omitElementCloseSet);
}
return omitElementCloseSet;
}
// If you change this, change the javadoc for getOmitElementCloseSet.
protected void loadOmitElementCloseSet(Set set) {
set.add("AREA");
set.add("BASE");
set.add("BR");
set.add("COL");
set.add("HR");
set.add("IMG");
set.add("INPUT");
set.add("LINK");
set.add("META");
set.add("P");
set.add("PARAM");
}
// let the people see the set, but not modify it.
/**
* A clone of the Set of elements that can have their close-tags omitted. By
* default it should be "AREA", "BASE", "BR", "COL", "HR", "IMG", "INPUT",
* "LINK", "META", "P", "PARAM"
*
* @return A clone of the Set.
*/
@SuppressWarnings("unchecked")
public Set getOmitElementCloseSet() {
return (Set) (internalGetOmitElementCloseSet().clone());
}
/**
* To use the empty set, pass an empty Set, or null:
*
*
*
*
* setOmitElementCloseSet(new HashSet());
* or
* setOmitElementCloseSet(null);
*
*
*
*
* @param newSet
* DOCUMENT ME!
*/
public void setOmitElementCloseSet(Set newSet) {
// resets, and safely empties it out if newSet is null.
omitElementCloseSet = new HashSet();
if (newSet != null) {
omitElementCloseSet = new HashSet();
for (String aTag : newSet) {
if (aTag != null) {
omitElementCloseSet.add(aTag.toUpperCase());
}
}
}
}
/**
* @see #setPreformattedTags(java.util.Set) setPreformattedTags
*
* @return DOCUMENT ME!
*/
@SuppressWarnings("unchecked")
public Set getPreformattedTags() {
return (Set) (preformattedTags.clone());
}
/**
* Override the default set, which includes PRE, SCRIPT, STYLE, and
* TEXTAREA, case insensitively.
*
* Setting Preformatted Tags
* Pass in a Set of Strings, one for each tag name that should be treated
* like a PRE tag. You may pass in null or an empty Set to assign the empty
* set, in which case no tags will be treated as preformatted, except that
* HTML Comments will continue to be preformatted. If a tag is included in
* the set of preformatted tags, all whitespace within the tag will be
* preserved, including whitespace on the same line preceding the close tag.
* This will generally make the close tag not line up with the start tag,
* but it preserves the intention of the whitespace within the tag.
*
* The browser considers leading whitespace before the close tag to be
* significant, but leading whitespace before the open tag to be
* insignificant. For example, if the HTML author doesn't put the close
* TEXTAREA tag flush to the left margin, then the TEXTAREA control in the
* browser will have spaces on the last line inside the control. This may be
* the HTML author's intent. Similarly, in a PRE, the browser treats a
* flushed left close PRE tag as different from a close tag with leading
* whitespace. Again, this must be left up to the HTML author.
*
* Examples
* Here is an example of how you can set the PreformattedTags list using
* setPreformattedTags to include IFRAME, as well as the default set, if you
* have an instance of this class named myHTMLWriter:
*
*
* Set current = myHTMLWriter.getPreformattedTags();
* current.add("IFRAME");
* myHTMLWriter.setPreformattedTags(current);
*
* //The set is now <b>PRE, SCRIPT, STYLE, TEXTAREA, IFRAME</b>
*
*
*
*
* Similarly, you can simply replace it with your own:
*
*
*
*
* HashSet newset = new HashSet();
* newset.add("PRE");
* newset.add("TEXTAREA");
* myHTMLWriter.setPreformattedTags(newset);
*
* //The set is now <b>{PRE, TEXTAREA}</b>
*
*
*
*
* You can remove all tags from the preformatted tags list, with an empty
* set, like this:
*
*
*
*
* myHTMLWriter.setPreformattedTags(new HashSet());
*
* //The set is now <b>{}</b>
*
*
*
*
* or with null, like this:
*
*
*
*
* myHTMLWriter.setPreformattedTags(null);
*
* //The set is now <b>{}</b>
*
*
*
*
* @param newSet
* DOCUMENT ME!
*/
public void setPreformattedTags(Set newSet) {
// no fancy merging, just set it, assuming they did a
// getExcludeTrimTags() first if they wanted to preserve the default
// set.
// resets, and safely empties it out if newSet is null.
preformattedTags = new HashSet();
if (newSet != null) {
for (String aTag : newSet) {
if (aTag != null) {
preformattedTags.add(aTag.toUpperCase());
}
}
}
}
/**
* DOCUMENT ME!
*
* @param qualifiedName
* DOCUMENT ME!
*
* @return true if the qualifiedName passed in matched (case-insensitively)
* a tag in the preformattedTags set, or false if not found or if
* the set is empty or null.
*
* @see #setPreformattedTags(java.util.Set) setPreformattedTags
*/
public boolean isPreformattedTag(String qualifiedName) {
// A null set implies that the user called setPreformattedTags(null),
// which means they want no tags to be preformatted.
return (preformattedTags != null)
&& (preformattedTags.contains(qualifiedName.toUpperCase()));
}
/**
* This override handles any elements that should not remove whitespace,
* such as <PRE>, <SCRIPT>, <STYLE>, and <TEXTAREA>.
* Note: the close tags won't line up with the open tag, but we can't alter
* that. See javadoc note at setPreformattedTags.
*
* @param element
* DOCUMENT ME!
*
* @throws IOException
* When the stream could not be written to.
*
* @see #setPreformattedTags(java.util.Set) setPreformattedTags
*/
protected void writeElement(Element element) throws IOException {
if (newLineAfterNTags == -1) { // lazy initialization check
lazyInitNewLinesAfterNTags();
}
if (newLineAfterNTags > 0) {
if ((tagsOuput > 0) && ((tagsOuput % newLineAfterNTags) == 0)) {
super.writer.write(lineSeparator);
}
}
tagsOuput++;
String qualifiedName = element.getQualifiedName();
String saveLastText = lastText;
if (isPreformattedTag(qualifiedName)) {
OutputFormat currentFormat = getOutputFormat();
boolean saveNewlines = currentFormat.isNewlines();
boolean saveTrimText = currentFormat.isTrimText();
String currentIndent = currentFormat.getIndent();
// You could have nested PREs, or SCRIPTS within PRE... etc.,
// therefore use push and pop.
formatStack.push(new FormatState(saveNewlines, saveTrimText,
currentIndent));
try {
// do this manually, since it won't be done while outputting
// the tag.
super.writePrintln();
if ((saveLastText.trim().length() == 0)
&& (currentIndent != null)
&& (currentIndent.length() > 0)) {
// We are indenting, but we want to line up with the close
// tag. lastText was the indent (whitespace, no \n) before
// the preformatted start tag. So write it out instead of
// the current indent level. This makes it line up with its
// close tag.
super.writer.write(justSpaces(saveLastText));
}
// actually, newlines are handled in this class by writeString,
// depending on if the stack is empty.
currentFormat.setNewlines(false);
currentFormat.setTrimText(false);
currentFormat.setIndent("");
// This line is the recursive one:
super.writeElement(element);
} finally {
FormatState state = (FormatState) formatStack.pop();
currentFormat.setNewlines(state.isNewlines());
currentFormat.setTrimText(state.isTrimText());
currentFormat.setIndent(state.getIndent());
}
} else {
super.writeElement(element);
}
}
private String justSpaces(String text) {
int size = text.length();
StringBuffer res = new StringBuffer(size);
char c;
for (int i = 0; i < size; i++) {
c = text.charAt(i);
switch (c) {
case '\r':
case '\n':
continue;
default:
res.append(c);
}
}
return res.toString();
}
private void lazyInitNewLinesAfterNTags() {
if (getOutputFormat().isNewlines()) {
// don't bother, newlines are going to happen anyway.
newLineAfterNTags = 0;
} else {
newLineAfterNTags = getOutputFormat().getNewLineAfterNTags();
}
}
// Convenience methods, static, with bunch-o-defaults
/**
* Convenience method to just get a String result.
*
* @param html
* DOCUMENT ME!
*
* @return a pretty printed String from the source string, preserving
* whitespace in the defaultPreformattedTags set, and leaving the
* close tags off of the default omitElementCloseSet set. Use one of
* the write methods if you want stream output.
*
* @throws java.io.IOException DOCUMENT ME!
* @throws java.io.UnsupportedEncodingException DOCUMENT ME!
* @throws org.dom4j.DocumentException DOCUMENT ME!
*/
public static String prettyPrintHTML(String html)
throws java.io.IOException, java.io.UnsupportedEncodingException,
org.dom4j.DocumentException {
return prettyPrintHTML(html, true, true, false, true);
}
/**
* Convenience method to just get a String result, but As XHTML .
*
* @param html
* DOCUMENT ME!
*
* @return a pretty printed String from the source string, preserving
* whitespace in the defaultPreformattedTags set, but conforming to
* XHTML: no close tags are omitted (though if empty, they will be
* converted to XHTML empty tags: <HR/> Use one of the write
* methods if you want stream output.
*
* @throws java.io.IOException DOCUMENT ME!
* @throws java.io.UnsupportedEncodingException DOCUMENT ME!
* @throws org.dom4j.DocumentException DOCUMENT ME!
*/
public static String prettyPrintXHTML(String html)
throws java.io.IOException, java.io.UnsupportedEncodingException,
org.dom4j.DocumentException {
return prettyPrintHTML(html, true, true, true, false);
}
/**
* DOCUMENT ME!
*
* @param html
* DOCUMENT ME!
* @param newlines
* DOCUMENT ME!
* @param trim
* DOCUMENT ME!
* @param isXHTML
* DOCUMENT ME!
* @param expandEmpty
* DOCUMENT ME!
*
* @return a pretty printed String from the source string, preserving
* whitespace in the defaultPreformattedTags set, and leaving the
* close tags off of the default omitElementCloseSet set. This
* override allows you to specify various formatter options. Use one
* of the write methods if you want stream output.
*
* @throws java.io.IOException DOCUMENT ME!
* @throws java.io.UnsupportedEncodingException DOCUMENT ME!
* @throws org.dom4j.DocumentException DOCUMENT ME!
*/
public static String prettyPrintHTML(String html, boolean newlines,
boolean trim, boolean isXHTML, boolean expandEmpty)
throws java.io.IOException, java.io.UnsupportedEncodingException,
org.dom4j.DocumentException {
StringWriter sw = new StringWriter();
OutputFormat format = OutputFormat.createPrettyPrint();
format.setNewlines(newlines);
format.setTrimText(trim);
format.setXHTML(isXHTML);
format.setExpandEmptyElements(expandEmpty);
HTMLWriter writer = new HTMLWriter(sw, format);
Document document = DocumentHelper.parseText(html);
writer.write(document);
writer.flush();
return sw.toString();
}
// Allows us to the current state of the format in this struct on the
// formatStack.
private class FormatState {
private boolean newlines = false;
private boolean trimText = false;
private String indent = "";
public FormatState(boolean newLines, boolean trimText, String indent) {
this.newlines = newLines;
this.trimText = trimText;
this.indent = indent;
}
public boolean isNewlines() {
return newlines;
}
public boolean isTrimText() {
return trimText;
}
public String getIndent() {
return indent;
}
}
}
/*
* My Title entities: &
* " < > %23
* line0
line1 line2, should line up, indent-wise line
* 3 line 4
*/
/*
* Redistribution and use of this software and associated documentation
* ("Software"), with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain copyright statements and
* notices. Redistributions must also contain a copy of this document.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The name "DOM4J" must not be used to endorse or promote products derived
* from this Software without prior written permission of MetaStuff, Ltd. For
* written permission, please contact [email protected].
*
* 4. Products derived from this Software may not be called "DOM4J" nor may
* "DOM4J" appear in their names without prior written permission of MetaStuff,
* Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
*
* 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
*
* THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
*/