/* * @(#)HTMLWriter.java 1.36 03/12/19 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package javax.swing.text.html; import javax.swing.text.*; import java.io.Writer; import java.util.Stack; import java.util.Enumeration; import java.util.Vector; import java.io.IOException; import java.util.StringTokenizer; import java.util.NoSuchElementException; import java.net.URL; /** * This is a writer for HTMLDocuments. * * @author Sunita Mani * @version 1.26, 02/02/00 */ public class HTMLWriter extends AbstractWriter { /* * Stores all elements for which end tags have to * be emitted. */ private Stack blockElementStack = new Stack(); private boolean inContent = false; private boolean inPre = false; /** When inPre is true, this will indicate the end offset of the pre * element. */ private int preEndOffset; private boolean inTextArea = false; private boolean newlineOutputed = false; private boolean completeDoc; /* * Stores all embedded tags. Embedded tags are tags that are * stored as attributes in other tags. Generally they're * character level attributes. Examples include * <b>, <i>, <font>, and <a>. */ private Vector tags = new Vector(10); /** * Values for the tags. */ private Vector tagValues = new Vector(10); /** * Used when writing out content. */ private Segment segment; /* * This is used in closeOutUnwantedEmbeddedTags. */ private Vector tagsToRemove = new Vector(10); /** * Set to true after the head has been output. */ private boolean wroteHead; /** * Set to true when entities (such as <) should be replaced. */ private boolean replaceEntities; /** * Temporary buffer. */ private char[] tempChars; /** * Creates a new HTMLWriter. * * @param w a Writer * @param doc an HTMLDocument * */ public HTMLWriter(Writer w, HTMLDocument doc) { this(w, doc, 0, doc.getLength()); } /** * Creates a new HTMLWriter. * * @param w a Writer * @param doc an HTMLDocument * @param pos the document location from which to fetch the content * @param len the amount to write out */ public HTMLWriter(Writer w, HTMLDocument doc, int pos, int len) { super(w, doc, pos, len); completeDoc = (pos == 0 && len == doc.getLength()); setLineLength(80); } /** * Iterates over the * Element tree and controls the writing out of * all the tags and its attributes. * * @exception IOException on any I/O error * @exception BadLocationException if pos represents an invalid * location within the document. * */ public void write() throws IOException, BadLocationException { ElementIterator it = getElementIterator(); Element current = null; Element next = null; wroteHead = false; setCurrentLineLength(0); replaceEntities = false; setCanWrapLines(false); if (segment == null) { segment = new Segment(); } inPre = false; boolean forcedBody = false; while ((next = it.next()) != null) { if (!inRange(next)) { if (completeDoc && next.getAttributes().getAttribute( StyleConstants.NameAttribute) == HTML.Tag.BODY) { forcedBody = true; } else { continue; } } if (current != null) { /* if next is child of current increment indent */ if (indentNeedsIncrementing(current, next)) { incrIndent(); } else if (current.getParentElement() != next.getParentElement()) { /* next and current are not siblings so emit end tags for items on the stack until the item on top of the stack, is the parent of the next. */ Element top = (Element)blockElementStack.peek(); while (top != next.getParentElement()) { /* pop() will return top. */ blockElementStack.pop(); if (!synthesizedElement(top)) { AttributeSet attrs = top.getAttributes(); if (!matchNameAttribute(attrs, HTML.Tag.PRE) && !isFormElementWithContent(attrs)) { decrIndent(); } endTag(top); } top = (Element)blockElementStack.peek(); } } else if (current.getParentElement() == next.getParentElement()) { /* if next and current are siblings the indent level is correct. But, we need to make sure that if current is on the stack, we pop it off, and put out its end tag. */ Element top = (Element)blockElementStack.peek(); if (top == current) { blockElementStack.pop(); endTag(top); } } } if (!next.isLeaf() || isFormElementWithContent(next.getAttributes())) { blockElementStack.push(next); startTag(next); } else { emptyTag(next); } current = next; } /* Emit all remaining end tags */ /* A null parameter ensures that all embedded tags currently in the tags vector have their corresponding end tags written out. */ closeOutUnwantedEmbeddedTags(null); if (forcedBody) { blockElementStack.pop(); endTag(current); } while (!blockElementStack.empty()) { current = (Element)blockElementStack.pop(); if (!synthesizedElement(current)) { AttributeSet attrs = current.getAttributes(); if (!matchNameAttribute(attrs, HTML.Tag.PRE) && !isFormElementWithContent(attrs)) { decrIndent(); } endTag(current); } } if (completeDoc) { writeAdditionalComments(); } segment.array = null; } /** * Writes out the attribute set. Ignores all * attributes with a key of type HTML.Tag, * attributes with a key of type StyleConstants, * and attributes with a key of type * HTML.Attribute.ENDTAG. * * @param attr an AttributeSet * @exception IOException on any I/O error * */ protected void writeAttributes(AttributeSet attr) throws IOException { // translate css attributes to html convAttr.removeAttributes(convAttr); convertToHTML32(attr, convAttr); Enumeration names = convAttr.getAttributeNames(); while (names.hasMoreElements()) { Object name = names.nextElement(); if (name instanceof HTML.Tag || name instanceof StyleConstants || name == HTML.Attribute.ENDTAG) { continue; } write(" " + name + "=\"" + convAttr.getAttribute(name) + "\""); } } /** * Writes out all empty elements (all tags that have no * corresponding end tag). * * @param elem an Element * @exception IOException on any I/O error * @exception BadLocationException if pos represents an invalid * location within the document. */ protected void emptyTag(Element elem) throws BadLocationException, IOException { if (!inContent && !inPre) { indent(); } AttributeSet attr = elem.getAttributes(); closeOutUnwantedEmbeddedTags(attr); writeEmbeddedTags(attr); if (matchNameAttribute(attr, HTML.Tag.CONTENT)) { inContent = true; text(elem); } else if (matchNameAttribute(attr, HTML.Tag.COMMENT)) { comment(elem); } else { boolean isBlock = isBlockTag(elem.getAttributes()); if (inContent && isBlock ) { writeLineSeparator(); indent(); } Object nameTag = (attr != null) ? attr.getAttribute (StyleConstants.NameAttribute) : null; Object endTag = (attr != null) ? attr.getAttribute (HTML.Attribute.ENDTAG) : null; boolean outputEndTag = false; // If an instance of an UNKNOWN Tag, or an instance of a // tag that is only visible during editing // if (nameTag != null && endTag != null && (endTag instanceof String) && ((String)endTag).equals("true")) { outputEndTag = true; } if (completeDoc && matchNameAttribute(attr, HTML.Tag.HEAD)) { if (outputEndTag) { // Write out any styles. writeStyles(((HTMLDocument)getDocument()).getStyleSheet()); } wroteHead = true; } write('<'); if (outputEndTag) { write('/'); } write(elem.getName()); writeAttributes(attr); write('>'); if (matchNameAttribute(attr, HTML.Tag.TITLE) && !outputEndTag) { Document doc = elem.getDocument(); String title = (String)doc.getProperty(Document.TitleProperty); write(title); } else if (!inContent || isBlock) { writeLineSeparator(); if (isBlock && inContent) { indent(); } } } } /** * Determines if the HTML.Tag associated with the * element is a block tag. * * @param attr an AttributeSet * @return true if tag is block tag, false otherwise. */ protected boolean isBlockTag(AttributeSet attr) { Object o = attr.getAttribute(StyleConstants.NameAttribute); if (o instanceof HTML.Tag) { HTML.Tag name = (HTML.Tag) o; return name.isBlock(); } return false; } /** * Writes out a start tag for the element. * Ignores all synthesized elements. * * @param elem an Element * @exception IOException on any I/O error */ protected void startTag(Element elem) throws IOException, BadLocationException { if (synthesizedElement(elem)) { return; } // Determine the name, as an HTML.Tag. AttributeSet attr = elem.getAttributes(); Object nameAttribute = attr.getAttribute(StyleConstants.NameAttribute); HTML.Tag name; if (nameAttribute instanceof HTML.Tag) { name = (HTML.Tag)nameAttribute; } else { name = null; } if (name == HTML.Tag.PRE) { inPre = true; preEndOffset = elem.getEndOffset(); } // write out end tags for item on stack closeOutUnwantedEmbeddedTags(attr); if (inContent) { writeLineSeparator(); inContent = false; newlineOutputed = false; } if (completeDoc && name == HTML.Tag.BODY && !wroteHead) { // If the head has not been output, output it and the styles. wroteHead = true; indent(); write("
"); writeLineSeparator(); incrIndent(); writeStyles(((HTMLDocument)getDocument()).getStyleSheet()); decrIndent(); writeLineSeparator(); indent(); write(""); writeLineSeparator(); } indent(); write('<'); write(elem.getName()); writeAttributes(attr); write('>'); if (name != HTML.Tag.PRE) { writeLineSeparator(); } if (name == HTML.Tag.TEXTAREA) { textAreaContent(elem.getAttributes()); } else if (name == HTML.Tag.SELECT) { selectContent(elem.getAttributes()); } else if (completeDoc && name == HTML.Tag.BODY) { // Write out the maps, which is not stored as Elements in // the Document. writeMaps(((HTMLDocument)getDocument()).getMaps()); } else if (name == HTML.Tag.HEAD) { wroteHead = true; incrIndent(); writeStyles(((HTMLDocument)getDocument()).getStyleSheet()); decrIndent(); } HTMLDocument document = null; if (name == HTML.Tag.BODY && (document = (HTMLDocument)getDocument()).hasBaseTag()) { incrIndent(); indent(); write("outputStyle
indicates
* whether or not a style has been output yet. This will return
* true if a style is written.
*/
boolean writeStyle(String name, Style style, boolean outputStyle)
throws IOException{
boolean didOutputStyle = false;
Enumeration attributes = style.getAttributeNames();
if (attributes != null) {
while (attributes.hasMoreElements()) {
Object attribute = attributes.nextElement();
if (attribute instanceof CSS.Attribute) {
String value = style.getAttribute(attribute).toString();
if (value != null) {
if (!outputStyle) {
writeStyleStartTag();
outputStyle = true;
}
if (!didOutputStyle) {
didOutputStyle = true;
indent();
write(name);
write(" {");
}
else {
write(";");
}
write(' ');
write(attribute.toString());
write(": ");
write(value);
}
}
}
}
if (didOutputStyle) {
write(" }");
writeLineSeparator();
}
return didOutputStyle;
}
void writeStyleStartTag() throws IOException {
indent();
write("");
writeLineSeparator();
indent();
}
// --- conversion support ---------------------------
/**
* Convert the give set of attributes to be html for
* the purpose of writing them out. Any keys that
* have been converted will not appear in the resultant
* set. Any keys not converted will appear in the
* resultant set the same as the received set.
* This will put the converted values into to
, unless
* it is null in which case a temporary AttributeSet will be returned.
*/
AttributeSet convertToHTML(AttributeSet from, MutableAttributeSet to) {
if (to == null) {
to = convAttr;
}
to.removeAttributes(to);
if (writeCSS) {
convertToHTML40(from, to);
} else {
convertToHTML32(from, to);
}
return to;
}
/**
* If true, the writer will emit CSS attributes in preference
* to HTML tags/attributes (i.e. It will emit an HTML 4.0
* style).
*/
private boolean writeCSS = false;
/**
* Buffer for the purpose of attribute conversion
*/
private MutableAttributeSet convAttr = new SimpleAttributeSet();
/**
* Buffer for the purpose of attribute conversion. This can be
* used if convAttr is being used.
*/
private MutableAttributeSet oConvAttr = new SimpleAttributeSet();
/**
* Create an older style of HTML attributes. This will
* convert character level attributes that have a StyleConstants
* mapping over to an HTML tag/attribute. Other CSS attributes
* will be placed in an HTML style attribute.
*/
private static void convertToHTML32(AttributeSet from, MutableAttributeSet to) {
if (from == null) {
return;
}
Enumeration keys = from.getAttributeNames();
String value = "";
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
if (key instanceof CSS.Attribute) {
if ((key == CSS.Attribute.FONT_FAMILY) ||
(key == CSS.Attribute.FONT_SIZE) ||
(key == CSS.Attribute.COLOR)) {
createFontAttribute((CSS.Attribute)key, from, to);
} else if (key == CSS.Attribute.FONT_WEIGHT) {
// add a bold tag is weight is bold
CSS.FontWeight weightValue = (CSS.FontWeight)
from.getAttribute(CSS.Attribute.FONT_WEIGHT);
if ((weightValue != null) && (weightValue.getValue() > 400)) {
addAttribute(to, HTML.Tag.B, SimpleAttributeSet.EMPTY);
}
} else if (key == CSS.Attribute.FONT_STYLE) {
String s = from.getAttribute(key).toString();
if (s.indexOf("italic") >= 0) {
addAttribute(to, HTML.Tag.I, SimpleAttributeSet.EMPTY);
}
} else if (key == CSS.Attribute.TEXT_DECORATION) {
String decor = from.getAttribute(key).toString();
if (decor.indexOf("underline") >= 0) {
addAttribute(to, HTML.Tag.U, SimpleAttributeSet.EMPTY);
}
if (decor.indexOf("line-through") >= 0) {
addAttribute(to, HTML.Tag.STRIKE, SimpleAttributeSet.EMPTY);
}
} else if (key == CSS.Attribute.VERTICAL_ALIGN) {
String vAlign = from.getAttribute(key).toString();
if (vAlign.indexOf("sup") >= 0) {
addAttribute(to, HTML.Tag.SUP, SimpleAttributeSet.EMPTY);
}
if (vAlign.indexOf("sub") >= 0) {
addAttribute(to, HTML.Tag.SUB, SimpleAttributeSet.EMPTY);
}
} else if (key == CSS.Attribute.TEXT_ALIGN) {
addAttribute(to, HTML.Attribute.ALIGN,
from.getAttribute(key).toString());
} else {
// default is to store in a HTML style attribute
if (value.length() > 0) {
value = value + "; ";
}
value = value + key + ": " + from.getAttribute(key);
}
} else {
Object attr = from.getAttribute(key);
if (attr instanceof AttributeSet) {
attr = ((AttributeSet)attr).copyAttributes();
}
addAttribute(to, key, attr);
}
}
if (value.length() > 0) {
to.addAttribute(HTML.Attribute.STYLE, value);
}
}
/**
* Add an attribute only if it doesn't exist so that we don't
* loose information replacing it with SimpleAttributeSet.EMPTY
*/
private static void addAttribute(MutableAttributeSet to, Object key, Object value) {
Object attr = to.getAttribute(key);
if (attr == null || attr == SimpleAttributeSet.EMPTY) {
to.addAttribute(key, value);
} else {
if (attr instanceof MutableAttributeSet &&
value instanceof AttributeSet) {
((MutableAttributeSet)attr).addAttributes((AttributeSet)value);
}
}
}
/**
* Create/update an HTML <font> tag attribute. The
* value of the attribute should be a MutableAttributeSet so
* that the attributes can be updated as they are discovered.
*/
private static void createFontAttribute(CSS.Attribute a, AttributeSet from,
MutableAttributeSet to) {
MutableAttributeSet fontAttr = (MutableAttributeSet)
to.getAttribute(HTML.Tag.FONT);
if (fontAttr == null) {
fontAttr = new SimpleAttributeSet();
to.addAttribute(HTML.Tag.FONT, fontAttr);
}
// edit the parameters to the font tag
String htmlValue = from.getAttribute(a).toString();
if (a == CSS.Attribute.FONT_FAMILY) {
fontAttr.addAttribute(HTML.Attribute.FACE, htmlValue);
} else if (a == CSS.Attribute.FONT_SIZE) {
fontAttr.addAttribute(HTML.Attribute.SIZE, htmlValue);
} else if (a == CSS.Attribute.COLOR) {
fontAttr.addAttribute(HTML.Attribute.COLOR, htmlValue);
}
}
/**
* Copies the given AttributeSet to a new set, converting
* any CSS attributes found to arguments of an HTML style
* attribute.
*/
private static void convertToHTML40(AttributeSet from, MutableAttributeSet to) {
Enumeration keys = from.getAttributeNames();
String value = "";
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
if (key instanceof CSS.Attribute) {
value = value + " " + key + "=" + from.getAttribute(key) + ";";
} else {
to.addAttribute(key, from.getAttribute(key));
}
}
if (value.length() > 0) {
to.addAttribute(HTML.Attribute.STYLE, value);
}
}
//
// Overrides the writing methods to only break a string when
// canBreakString is true.
// In a future release it is likely AbstractWriter will get this
// functionality.
//
/**
* Writes the line separator. This is overriden to make sure we don't
* replace the newline content in case it is outside normal ascii.
*/
protected void writeLineSeparator() throws IOException {
boolean oldReplace = replaceEntities;
replaceEntities = false;
super.writeLineSeparator();
replaceEntities = oldReplace;
}
/**
* This method is overriden to map any character entities, such as
* < to <. super.output
will be invoked to
* write the content.
*/
protected void output(char[] chars, int start, int length)
throws IOException {
if (!replaceEntities) {
super.output(chars, start, length);
return;
}
int last = start;
length += start;
for (int counter = start; counter < length; counter++) {
// This will change, we need better support character level
// entities.
switch(chars[counter]) {
// Character level entities.
case '<':
if (counter > last) {
super.output(chars, last, counter - last);
}
last = counter + 1;
output("<");
break;
case '>':
if (counter > last) {
super.output(chars, last, counter - last);
}
last = counter + 1;
output(">");
break;
case '&':
if (counter > last) {
super.output(chars, last, counter - last);
}
last = counter + 1;
output("&");
break;
case '"':
if (counter > last) {
super.output(chars, last, counter - last);
}
last = counter + 1;
output(""");
break;
// Special characters
case '\n':
case '\t':
case '\r':
break;
default:
if (chars[counter] < ' ' || chars[counter] > 127) {
if (counter > last) {
super.output(chars, last, counter - last);
}
last = counter + 1;
// If the character is outside of ascii, write the
// numeric value.
output("");
output(String.valueOf((int)chars[counter]));
output(";");
}
break;
}
}
if (last < length) {
super.output(chars, last, length - last);
}
}
/**
* This directly invokes super's output
after converting
* string
to a char[].
*/
private void output(String string) throws IOException {
int length = string.length();
if (tempChars == null || tempChars.length < length) {
tempChars = new char[length];
}
string.getChars(0, length, tempChars, 0);
super.output(tempChars, 0, length);
}
}