View Javadoc

1   /*
2    * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
3    *
4    * This software is open source.
5    * See the bottom of this file for the licence.
6    */
7   
8   package org.dom4j.io;
9   
10  import java.io.IOException;
11  import java.io.OutputStream;
12  import java.io.StringWriter;
13  import java.io.UnsupportedEncodingException;
14  import java.io.Writer;
15  import java.util.HashSet;
16  import java.util.Iterator;
17  import java.util.Set;
18  import java.util.Stack;
19  
20  import org.dom4j.Document;
21  import org.dom4j.DocumentHelper;
22  import org.dom4j.Element;
23  import org.dom4j.Entity;
24  import org.dom4j.Node;
25  
26  import org.xml.sax.SAXException;
27  
28  /***
29   * <p>
30   * <code>HTMLWriter</code> takes a DOM4J tree and formats it to a stream as
31   * HTML. This formatter is similar to XMLWriter but it outputs the text of CDATA
32   * and Entity sections rather than the serialised format as in XML, it has an
33   * XHTML mode, it retains whitespace in certain elements such as &lt;PRE&gt;,
34   * and it supports certain elements which have no corresponding close tag such
35   * as for &lt;BR&gt; and &lt;P&gt;.
36   * </p>
37   * 
38   * <p>
39   * The OutputFormat passed in to the constructor is checked for isXHTML() and
40   * isExpandEmptyElements(). See {@link OutputFormat OutputFormat}for details.
41   * Here are the rules for <b>this class </b> based on an OutputFormat, "format",
42   * passed in to the constructor: <br/><br/>
43   * 
44   * <ul>
45   * <li>If an element is in {@link #getOmitElementCloseSet()
46   * getOmitElementCloseSet}, then it is treated specially:
47   * 
48   * <ul>
49   * <li>It never expands, since some browsers treat this as two separate
50   * Horizontal Rules: &lt;HR&gt;&lt;/HR&gt;</li>
51   * <li>If {@link org.dom4j.io.OutputFormat#isXHTML() format.isXHTML()}, then
52   * it has a space before the closing single-tag slash, since Netscape 4.x-
53   * treats this: &lt;HR /&gt; as an element named "HR" with an attribute named
54   * "/", but that's better than when it refuses to recognize this: &lt;hr/&gt;
55   * which it thinks is an element named "HR/".</li>
56   * </ul>
57   * 
58   * </li>
59   * <li>If {@link org.dom4j.io.OutputFormat#isXHTML() format.isXHTML()}, all
60   * elements must have either a close element, or be a closed single tag.</li>
61   * <li>If {@link org.dom4j.io.OutputFormat#isExpandEmptyElements()
62   * format.isExpandEmptyElements()}() is true, all elements are expanded except
63   * as above.</li>
64   * </ul>
65   * 
66   * <b>Examples </b>
67   * </p>
68   * 
69   * <p>
70   * </p>
71   * 
72   * <p>
73   * If isXHTML == true, CDATA sections look like this:
74   * 
75   * <PRE>
76   * 
77   * <b>&lt;myelement&gt;&lt;![CDATA[My data]]&gt;&lt;/myelement&gt; </b>
78   * 
79   * </PRE>
80   * 
81   * Otherwise, they look like this:
82   * 
83   * <PRE>
84   * 
85   * <b>&lt;myelement&gt;My data&lt;/myelement&gt; </b>
86   * 
87   * </PRE>
88   * 
89   * </p>
90   * 
91   * <p>
92   * Basically, {@link OutputFormat.isXHTML() OutputFormat.isXHTML()} ==
93   * <code>true</code> will produce valid XML, while {@link
94   * org.dom4j.io.OutputFormat#isExpandEmptyElements()
95   * format.isExpandEmptyElements()} determines whether empty elements are
96   * expanded if isXHTML is true, excepting the special HTML single tags.
97   * </p>
98   * 
99   * <p>
100  * Also, HTMLWriter handles tags whose contents should be preformatted, that is,
101  * whitespace-preserved. By default, this set includes the tags &lt;PRE&gt;,
102  * &lt;SCRIPT&gt;, &lt;STYLE&gt;, and &lt;TEXTAREA&gt;, case insensitively. It
103  * does not include &lt;IFRAME&gt;. Other tags, such as &lt;CODE&gt;,
104  * &lt;KBD&gt;, &lt;TT&gt;, &lt;VAR&gt;, are usually rendered in a different
105  * font in most browsers, but don't preserve whitespace, so they also don't
106  * appear in the default list. HTML Comments are always whitespace-preserved.
107  * However, the parser you use may store comments with linefeed-only text nodes
108  * (\n) even if your platform uses another line.separator character, and
109  * HTMLWriter outputs Comment nodes exactly as the DOM is set up by the parser.
110  * See examples and discussion here: {@link#setPreformattedTags(java.util.Set)
111  * setPreformattedTags}
112  * </p>
113  * 
114  * <p>
115  * <b>Examples </b>
116  * </p>
117  * <blockquote>
118  * <p>
119  * <b>Pretty Printing </b>
120  * </p>
121  * 
122  * <p>
123  * This example shows how to pretty print a string containing a valid HTML
124  * document to a string. You can also just call the static methods of this
125  * class: <br>
126  * {@link #prettyPrintHTML(String) prettyPrintHTML(String)}or <br>
127  * {@link #prettyPrintHTML(String,boolean,boolean,boolean,boolean)
128  * prettyPrintHTML(String,boolean,boolean,boolean,boolean)} or, <br>
129  * {@link #prettyPrintXHTML(String) prettyPrintXHTML(String)}for XHTML (note
130  * the X)
131  * </p>
132  * 
133  * <pre>
134  * String testPrettyPrint(String html) {
135  *     StringWriter sw = new StringWriter();
136  *     OutputFormat format = OutputFormat.createPrettyPrint();
137  *     // These are the default values for createPrettyPrint,
138  *     // so you needn't set them:
139  *     // format.setNewlines(true);
140  *     // format.setTrimText(true);&lt;/font&gt;
141  *     format.setXHTML(true);
142  *     HTMLWriter writer = new HTMLWriter(sw, format);
143  *     Document document = DocumentHelper.parseText(html);
144  *     writer.write(document);
145  *     writer.flush();
146  *     return sw.toString();
147  * }
148  * </pre>
149  * 
150  * <p>
151  * This example shows how to create a "squeezed" document, but one that will
152  * work in browsers even if the browser line length is limited. No newlines are
153  * included, no extra whitespace at all, except where it it required by
154  * {@link #setPreformattedTags(java.util.Set) setPreformattedTags}.
155  * </p>
156  * 
157  * <pre>
158  * String testCrunch(String html) {
159  *     StringWriter sw = new StringWriter();
160  *     OutputFormat format = OutputFormat.createPrettyPrint();
161  *     format.setNewlines(false);
162  *     format.setTrimText(true);
163  *     format.setIndent(&quot;&quot;);
164  *     format.setXHTML(true);
165  *     format.setExpandEmptyElements(false);
166  *     format.setNewLineAfterNTags(20);
167  *     org.dom4j.io.HTMLWriter writer = new HTMLWriter(sw, format);
168  *     org.dom4j.Document document = DocumentHelper.parseText(html);
169  *     writer.write(document);
170  *     writer.flush();
171  *     return sw.toString();
172  * }
173  * </pre>
174  * 
175  * </blockquote>
176  * 
177  * @author <a href="mailto:james.strachan@metastuff.com">James Strachan </a>
178  * @author Laramie Crocker
179  * @version $Revision: 1.21 $
180  */
181 public class HTMLWriter extends XMLWriter {
182     private static String lineSeparator = System.getProperty("line.separator");
183 
184     protected static final HashSet DEFAULT_PREFORMATTED_TAGS;
185 
186     static {
187         // If you change this list, update the javadoc examples, above in the
188         // class javadoc, in writeElement, and in setPreformattedTags().
189         DEFAULT_PREFORMATTED_TAGS = new HashSet();
190         DEFAULT_PREFORMATTED_TAGS.add("PRE");
191         DEFAULT_PREFORMATTED_TAGS.add("SCRIPT");
192         DEFAULT_PREFORMATTED_TAGS.add("STYLE");
193         DEFAULT_PREFORMATTED_TAGS.add("TEXTAREA");
194     }
195 
196     protected static final OutputFormat DEFAULT_HTML_FORMAT;
197 
198     static {
199         DEFAULT_HTML_FORMAT = new OutputFormat("  ", true);
200         DEFAULT_HTML_FORMAT.setTrimText(true);
201         DEFAULT_HTML_FORMAT.setSuppressDeclaration(true);
202     }
203 
204     private Stack formatStack = new Stack();
205 
206     private String lastText = "";
207 
208     private int tagsOuput = 0;
209 
210     // legal values are 0+, but -1 signifies lazy initialization.
211     private int newLineAfterNTags = -1;
212 
213     private HashSet preformattedTags = DEFAULT_PREFORMATTED_TAGS;
214 
215     /***
216      * Used to store the qualified element names which should have no close
217      * element tag
218      */
219     private HashSet omitElementCloseSet;
220 
221     public HTMLWriter(Writer writer) {
222         super(writer, DEFAULT_HTML_FORMAT);
223     }
224 
225     public HTMLWriter(Writer writer, OutputFormat format) {
226         super(writer, format);
227     }
228 
229     public HTMLWriter() throws UnsupportedEncodingException {
230         super(DEFAULT_HTML_FORMAT);
231     }
232 
233     public HTMLWriter(OutputFormat format) throws UnsupportedEncodingException {
234         super(format);
235     }
236 
237     public HTMLWriter(OutputStream out) throws UnsupportedEncodingException {
238         super(out, DEFAULT_HTML_FORMAT);
239     }
240 
241     public HTMLWriter(OutputStream out, OutputFormat format)
242             throws UnsupportedEncodingException {
243         super(out, format);
244     }
245 
246     public void startCDATA() throws SAXException {
247     }
248 
249     public void endCDATA() throws SAXException {
250     }
251 
252     // Overloaded methods
253     // added isXHTML() stuff so you get the CDATA brackets if you desire.
254     protected void writeCDATA(String text) throws IOException {
255         // XXX: Should we escape entities?
256         // writer.write( escapeElementEntities( text ) );
257         if (getOutputFormat().isXHTML()) {
258             super.writeCDATA(text);
259         } else {
260             writer.write(text);
261         }
262 
263         lastOutputNodeType = Node.CDATA_SECTION_NODE;
264     }
265 
266     protected void writeEntity(Entity entity) throws IOException {
267         writer.write(entity.getText());
268         lastOutputNodeType = Node.ENTITY_REFERENCE_NODE;
269     }
270 
271     protected void writeDeclaration() throws IOException {
272     }
273 
274     protected void writeString(String text) throws IOException {
275         /*
276          * DOM stores \n at the end of text nodes that are newlines. This is
277          * significant if we are in a PRE section. However, we only want to
278          * output the system line.separator, not \n. This is a little brittle,
279          * but this function appears to be called with these lineseparators as a
280          * separate TEXT_NODE. If we are in a preformatted section, output the
281          * right line.separator, otherwise ditch. If the single \n character is
282          * not the text, then do the super thing to output the text.
283          * 
284          * Also, we store the last text that was not a \n since it may be used
285          * by writeElement in this class to line up preformatted tags.
286          */
287         if (text.equals("\n")) {
288             if (!formatStack.empty()) {
289                 super.writeString(lineSeparator);
290             }
291 
292             return;
293         }
294 
295         lastText = text;
296 
297         if (formatStack.empty()) {
298             super.writeString(text.trim());
299         } else {
300             super.writeString(text);
301         }
302     }
303 
304     /***
305      * Overriden method to not close certain element names to avoid wierd
306      * behaviour from browsers for versions up to 5.x
307      * 
308      * @param qualifiedName
309      *            DOCUMENT ME!
310      * 
311      * @throws IOException
312      *             DOCUMENT ME!
313      */
314     protected void writeClose(String qualifiedName) throws IOException {
315         if (!omitElementClose(qualifiedName)) {
316             super.writeClose(qualifiedName);
317         }
318     }
319 
320     protected void writeEmptyElementClose(String qualifiedName)
321             throws IOException {
322         if (getOutputFormat().isXHTML()) {
323             // xhtml, always check with format object whether to expand or not.
324             if (omitElementClose(qualifiedName)) {
325                 // it was a special omit tag, do it the XHTML way: "<br/>",
326                 // ignoring the expansion option, since <br></br> is OK XML,
327                 // but produces twice the linefeeds desired in the browser.
328                 // for netscape 4.7, though all are fine with it, write a space
329                 // before the close slash.
330                 writer.write(" />");
331             } else {
332                 super.writeEmptyElementClose(qualifiedName);
333             }
334         } else {
335             // html, not xhtml
336             if (omitElementClose(qualifiedName)) {
337                 // it was a special omit tag, do it the old html way: "<br>".
338                 writer.write(">");
339             } else {
340                 // it was NOT a special omit tag, check with format object
341                 // whether to expand or not.
342                 super.writeEmptyElementClose(qualifiedName);
343             }
344         }
345     }
346 
347     protected boolean omitElementClose(String qualifiedName) {
348         return internalGetOmitElementCloseSet().contains(
349                 qualifiedName.toUpperCase());
350     }
351 
352     private HashSet internalGetOmitElementCloseSet() {
353         if (omitElementCloseSet == null) {
354             omitElementCloseSet = new HashSet();
355             loadOmitElementCloseSet(omitElementCloseSet);
356         }
357 
358         return omitElementCloseSet;
359     }
360 
361     // If you change this, change the javadoc for getOmitElementCloseSet.
362     protected void loadOmitElementCloseSet(Set set) {
363         set.add("AREA");
364         set.add("BASE");
365         set.add("BR");
366         set.add("COL");
367         set.add("HR");
368         set.add("IMG");
369         set.add("INPUT");
370         set.add("LINK");
371         set.add("META");
372         set.add("P");
373         set.add("PARAM");
374     }
375 
376     // let the people see the set, but not modify it.
377 
378     /***
379      * A clone of the Set of elements that can have their close-tags omitted. By
380      * default it should be "AREA", "BASE", "BR", "COL", "HR", "IMG", "INPUT",
381      * "LINK", "META", "P", "PARAM"
382      * 
383      * @return A clone of the Set.
384      */
385     public Set getOmitElementCloseSet() {
386         return (Set) (internalGetOmitElementCloseSet().clone());
387     }
388 
389     /***
390      * To use the empty set, pass an empty Set, or null:
391      * 
392      * <pre>
393      * 
394      * 
395      *       setOmitElementCloseSet(new HashSet());
396      *     or
397      *       setOmitElementCloseSet(null);
398      * 
399      *  
400      * </pre>
401      * 
402      * @param newSet
403      *            DOCUMENT ME!
404      */
405     public void setOmitElementCloseSet(Set newSet) {
406         // resets, and safely empties it out if newSet is null.
407         omitElementCloseSet = new HashSet();
408 
409         if (newSet != null) {
410             omitElementCloseSet = new HashSet();
411 
412             Object aTag;
413             Iterator iter = newSet.iterator();
414 
415             while (iter.hasNext()) {
416                 aTag = iter.next();
417 
418                 if (aTag != null) {
419                     omitElementCloseSet.add(aTag.toString().toUpperCase());
420                 }
421             }
422         }
423     }
424 
425     /***
426      * @see #setPreformattedTags(java.util.Set) setPreformattedTags
427      */
428     public Set getPreformattedTags() {
429         return (Set) (preformattedTags.clone());
430     }
431 
432     /***
433      * <p>
434      * Override the default set, which includes PRE, SCRIPT, STYLE, and
435      * TEXTAREA, case insensitively.
436      * </p>
437      * 
438      * <p>
439      * <b>Setting Preformatted Tags </b>
440      * </p>
441      * 
442      * <p>
443      * Pass in a Set of Strings, one for each tag name that should be treated
444      * like a PRE tag. You may pass in null or an empty Set to assign the empty
445      * set, in which case no tags will be treated as preformatted, except that
446      * HTML Comments will continue to be preformatted. If a tag is included in
447      * the set of preformatted tags, all whitespace within the tag will be
448      * preserved, including whitespace on the same line preceding the close tag.
449      * This will generally make the close tag not line up with the start tag,
450      * but it preserves the intention of the whitespace within the tag.
451      * </p>
452      * 
453      * <p>
454      * The browser considers leading whitespace before the close tag to be
455      * significant, but leading whitespace before the open tag to be
456      * insignificant. For example, if the HTML author doesn't put the close
457      * TEXTAREA tag flush to the left margin, then the TEXTAREA control in the
458      * browser will have spaces on the last line inside the control. This may be
459      * the HTML author's intent. Similarly, in a PRE, the browser treats a
460      * flushed left close PRE tag as different from a close tag with leading
461      * whitespace. Again, this must be left up to the HTML author.
462      * </p>
463      * 
464      * <p>
465      * <b>Examples </b>
466      * </p>
467      * <blockquote>
468      * <p>
469      * Here is an example of how you can set the PreformattedTags list using
470      * setPreformattedTags to include IFRAME, as well as the default set, if you
471      * have an instance of this class named myHTMLWriter:
472      * 
473      * <pre>
474      * Set current = myHTMLWriter.getPreformattedTags();
475      * current.add(&quot;IFRAME&quot;);
476      * myHTMLWriter.setPreformattedTags(current);
477      * 
478      * //The set is now &lt;b&gt;PRE, SCRIPT, STYLE, TEXTAREA, IFRAME&lt;/b&gt;
479      * 
480      * 
481      * </pre>
482      * 
483      * Similarly, you can simply replace it with your own:
484      * 
485      * <pre>
486      * 
487      * 
488      *       HashSet newset = new HashSet();
489      *       newset.add(&quot;PRE&quot;);
490      *       newset.add(&quot;TEXTAREA&quot;);
491      *       myHTMLWriter.setPreformattedTags(newset);
492      * 
493      *       //The set is now &lt;b&gt;{PRE, TEXTAREA}&lt;/b&gt;
494      * 
495      *  
496      * </pre>
497      * 
498      * You can remove all tags from the preformatted tags list, with an empty
499      * set, like this:
500      * 
501      * <pre>
502      * 
503      * 
504      *       myHTMLWriter.setPreformattedTags(new HashSet());
505      * 
506      *       //The set is now &lt;b&gt;{}&lt;/b&gt;
507      * 
508      *  
509      * </pre>
510      * 
511      * or with null, like this:
512      * 
513      * <pre>
514      * 
515      * 
516      *       myHTMLWriter.setPreformattedTags(null);
517      * 
518      *       //The set is now &lt;b&gt;{}&lt;/b&gt;
519      * 
520      *  
521      * </pre>
522      * 
523      * </p>
524      * </blockquote>
525      * 
526      * @param newSet
527      *            DOCUMENT ME!
528      */
529     public void setPreformattedTags(Set newSet) {
530         // no fancy merging, just set it, assuming they did a
531         // getExcludeTrimTags() first if they wanted to preserve the default
532         // set.
533         // resets, and safely empties it out if newSet is null.
534         preformattedTags = new HashSet();
535 
536         if (newSet != null) {
537             Object aTag;
538             Iterator iter = newSet.iterator();
539 
540             while (iter.hasNext()) {
541                 aTag = iter.next();
542 
543                 if (aTag != null) {
544                     preformattedTags.add(aTag.toString().toUpperCase());
545                 }
546             }
547         }
548     }
549 
550     /***
551      * DOCUMENT ME!
552      * 
553      * @param qualifiedName
554      *            DOCUMENT ME!
555      * 
556      * @return true if the qualifiedName passed in matched (case-insensitively)
557      *         a tag in the preformattedTags set, or false if not found or if
558      *         the set is empty or null.
559      * 
560      * @see #setPreformattedTags(java.util.Set) setPreformattedTags
561      */
562     public boolean isPreformattedTag(String qualifiedName) {
563         // A null set implies that the user called setPreformattedTags(null),
564         // which means they want no tags to be preformatted.
565         return (preformattedTags != null)
566                 && (preformattedTags.contains(qualifiedName.toUpperCase()));
567     }
568 
569     /***
570      * This override handles any elements that should not remove whitespace,
571      * such as &lt;PRE&gt;, &lt;SCRIPT&gt;, &lt;STYLE&gt;, and &lt;TEXTAREA&gt;.
572      * Note: the close tags won't line up with the open tag, but we can't alter
573      * that. See javadoc note at setPreformattedTags.
574      * 
575      * @param element
576      *            DOCUMENT ME!
577      * 
578      * @throws IOException
579      *             When the stream could not be written to.
580      * 
581      * @see #setPreformattedTags(java.util.Set) setPreformattedTags
582      */
583     protected void writeElement(Element element) throws IOException {
584         if (newLineAfterNTags == -1) { // lazy initialization check
585             lazyInitNewLinesAfterNTags();
586         }
587 
588         if (newLineAfterNTags > 0) {
589             if ((tagsOuput > 0) && ((tagsOuput % newLineAfterNTags) == 0)) {
590                 super.writer.write(lineSeparator);
591             }
592         }
593 
594         tagsOuput++;
595 
596         String qualifiedName = element.getQualifiedName();
597         String saveLastText = lastText;
598         int size = element.nodeCount();
599 
600         if (isPreformattedTag(qualifiedName)) {
601             OutputFormat currentFormat = getOutputFormat();
602             boolean saveNewlines = currentFormat.isNewlines();
603             boolean saveTrimText = currentFormat.isTrimText();
604             String currentIndent = currentFormat.getIndent();
605 
606             // You could have nested PREs, or SCRIPTS within PRE... etc.,
607             // therefore use push and pop.
608             formatStack.push(new FormatState(saveNewlines, saveTrimText,
609                     currentIndent));
610 
611             try {
612                 // do this manually, since it won't be done while outputting
613                 // the tag.
614                 super.writePrintln();
615 
616                 if ((saveLastText.trim().length() == 0)
617                         && (currentIndent != null)
618                         && (currentIndent.length() > 0)) {
619                     // We are indenting, but we want to line up with the close
620                     // tag. lastText was the indent (whitespace, no \n) before
621                     // the preformatted start tag. So write it out instead of
622                     // the current indent level. This makes it line up with its
623                     // close tag.
624                     super.writer.write(justSpaces(saveLastText));
625                 }
626 
627                 // actually, newlines are handled in this class by writeString,
628                 // depending on if the stack is empty.
629                 currentFormat.setNewlines(false);
630                 currentFormat.setTrimText(false);
631                 currentFormat.setIndent("");
632 
633                 // This line is the recursive one:
634                 super.writeElement(element);
635             } finally {
636                 FormatState state = (FormatState) formatStack.pop();
637                 currentFormat.setNewlines(state.isNewlines());
638                 currentFormat.setTrimText(state.isTrimText());
639                 currentFormat.setIndent(state.getIndent());
640             }
641         } else {
642             super.writeElement(element);
643         }
644     }
645 
646     private String justSpaces(String text) {
647         int size = text.length();
648         StringBuffer res = new StringBuffer(size);
649         char c;
650 
651         for (int i = 0; i < size; i++) {
652             c = text.charAt(i);
653 
654             switch (c) {
655                 case '\r':
656                 case '\n':
657 
658                     continue;
659 
660                 default:
661                     res.append(c);
662             }
663         }
664 
665         return res.toString();
666     }
667 
668     private void lazyInitNewLinesAfterNTags() {
669         if (getOutputFormat().isNewlines()) {
670             // don't bother, newlines are going to happen anyway.
671             newLineAfterNTags = 0;
672         } else {
673             newLineAfterNTags = getOutputFormat().getNewLineAfterNTags();
674         }
675     }
676 
677     // Convenience methods, static, with bunch-o-defaults
678 
679     /***
680      * Convenience method to just get a String result.
681      * 
682      * @param html
683      *            DOCUMENT ME!
684      * 
685      * @return a pretty printed String from the source string, preserving
686      *         whitespace in the defaultPreformattedTags set, and leaving the
687      *         close tags off of the default omitElementCloseSet set. Use one of
688      *         the write methods if you want stream output.
689      * 
690      * @throws java.io.IOException
691      * @throws java.io.UnsupportedEncodingException
692      * @throws org.dom4j.DocumentException
693      */
694     public static String prettyPrintHTML(String html)
695             throws java.io.IOException, java.io.UnsupportedEncodingException,
696             org.dom4j.DocumentException {
697         return prettyPrintHTML(html, true, true, false, true);
698     }
699 
700     /***
701      * Convenience method to just get a String result, but <b>As XHTML </b>.
702      * 
703      * @param html
704      *            DOCUMENT ME!
705      * 
706      * @return a pretty printed String from the source string, preserving
707      *         whitespace in the defaultPreformattedTags set, but conforming to
708      *         XHTML: no close tags are omitted (though if empty, they will be
709      *         converted to XHTML empty tags: &lt;HR/&gt; Use one of the write
710      *         methods if you want stream output.
711      * 
712      * @throws java.io.IOException
713      * @throws java.io.UnsupportedEncodingException
714      * @throws org.dom4j.DocumentException
715      */
716     public static String prettyPrintXHTML(String html)
717             throws java.io.IOException, java.io.UnsupportedEncodingException,
718             org.dom4j.DocumentException {
719         return prettyPrintHTML(html, true, true, true, false);
720     }
721 
722     /***
723      * DOCUMENT ME!
724      * 
725      * @param html
726      *            DOCUMENT ME!
727      * @param newlines
728      *            DOCUMENT ME!
729      * @param trim
730      *            DOCUMENT ME!
731      * @param isXHTML
732      *            DOCUMENT ME!
733      * @param expandEmpty
734      *            DOCUMENT ME!
735      * 
736      * @return a pretty printed String from the source string, preserving
737      *         whitespace in the defaultPreformattedTags set, and leaving the
738      *         close tags off of the default omitElementCloseSet set. This
739      *         override allows you to specify various formatter options. Use one
740      *         of the write methods if you want stream output.
741      * 
742      * @throws java.io.IOException
743      * @throws java.io.UnsupportedEncodingException
744      * @throws org.dom4j.DocumentException
745      */
746     public static String prettyPrintHTML(String html, boolean newlines,
747             boolean trim, boolean isXHTML, boolean expandEmpty)
748             throws java.io.IOException, java.io.UnsupportedEncodingException,
749             org.dom4j.DocumentException {
750         StringWriter sw = new StringWriter();
751         OutputFormat format = OutputFormat.createPrettyPrint();
752         format.setNewlines(newlines);
753         format.setTrimText(trim);
754         format.setXHTML(isXHTML);
755         format.setExpandEmptyElements(expandEmpty);
756 
757         HTMLWriter writer = new HTMLWriter(sw, format);
758         Document document = DocumentHelper.parseText(html);
759         writer.write(document);
760         writer.flush();
761 
762         return sw.toString();
763     }
764 
765     // Allows us to the current state of the format in this struct on the
766     // formatStack.
767     private class FormatState {
768         private boolean newlines = false;
769 
770         private boolean trimText = false;
771 
772         private String indent = "";
773 
774         public FormatState(boolean newLines, boolean trimText, String indent) {
775             this.newlines = newLines;
776             this.trimText = trimText;
777             this.indent = indent;
778         }
779 
780         public boolean isNewlines() {
781             return newlines;
782         }
783 
784         public boolean isTrimText() {
785             return trimText;
786         }
787 
788         public String getIndent() {
789             return indent;
790         }
791     }
792 }
793 
794 /*
795  * <html> <head> <title>My Title </title> <style> .foo { text-align: Right; }
796  * </style> <script> function mojo(){ return "bar"; } </script> <script
797  * language="JavaScript"> <!-- //this is the canonical javascript hiding.
798  * function foo(){ return "foo"; } //--> </script> </head> <!-- this is a
799  * comment --> <body bgcolor="#A4BFDD" mojo="&amp;"> entities: &#160; &amp;
800  * &quot; &lt; &gt; %23 <p></p> <mojo> </mojo> <foo /> <table border="1"> <tr>
801  * <td><pre> line0 <hr /> line1 <b>line2, should line up, indent-wise </b> line
802  * 3 line 4 </pre></td><td></td></tr> </table> <myCDATAElement> <![CDATA[My
803  * data]]> </myCDATAElement> </body> </html>
804  */
805 
806 /*
807  * Redistribution and use of this software and associated documentation
808  * ("Software"), with or without modification, are permitted provided that the
809  * following conditions are met:
810  * 
811  * 1. Redistributions of source code must retain copyright statements and
812  * notices. Redistributions must also contain a copy of this document.
813  * 
814  * 2. Redistributions in binary form must reproduce the above copyright notice,
815  * this list of conditions and the following disclaimer in the documentation
816  * and/or other materials provided with the distribution.
817  * 
818  * 3. The name "DOM4J" must not be used to endorse or promote products derived
819  * from this Software without prior written permission of MetaStuff, Ltd. For
820  * written permission, please contact dom4j-info@metastuff.com.
821  * 
822  * 4. Products derived from this Software may not be called "DOM4J" nor may
823  * "DOM4J" appear in their names without prior written permission of MetaStuff,
824  * Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
825  * 
826  * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
827  * 
828  * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
829  * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
830  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
831  * ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
832  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
833  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
834  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
835  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
836  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
837  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
838  * POSSIBILITY OF SUCH DAMAGE.
839  * 
840  * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
841  */