/*
* @(#)HTMLDocument.java 1.180 06/05/10
*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package javax.swing.text.html;
import java.awt.Color;
import java.awt.Component;
import java.awt.font.TextAttribute;
import java.util.*;
import java.net.URL;
import java.net.URLEncoder;
import java.net.MalformedURLException;
import java.io.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;
import java.text.Bidi;
import sun.swing.SwingUtilities2;
/**
* A document that models HTML. The purpose of this model
* is to support both browsing and editing. As a result,
* the structure described by an HTML document is not
* exactly replicated by default. The element structure that
* is modeled by default, is built by the class
* <code>HTMLDocument.HTMLReader</code>, which implements
* the <code>HTMLEditorKit.ParserCallback</code> protocol
* that the parser expects. To change the structure one
* can subclass <code>HTMLReader</code>, and reimplement the method
* {@link #getReader(int)} to return the new reader
* implementation. The documentation for <code>HTMLReader</code>
* should be consulted for the details of
* the default structure created. The intent is that
* the document be non-lossy (although reproducing the
* HTML format may result in a different format).
* <p>
* The document models only HTML, and makes no attempt to
* store view attributes in it. The elements are identified
* by the <code>StyleContext.NameAttribute</code> attribute,
* which should always have a value of type <code>HTML.Tag</code>
* that identifies the kind of element. Some of the elements
* (such as comments) are synthesized. The <code>HTMLFactory</code>
* uses this attribute to determine what kind of view to build.
* <p>
* This document supports incremental loading. The
* <code>TokenThreshold</code> property controls how
* much of the parse is buffered before trying to update
* the element structure of the document. This property
* is set by the <code>EditorKit</code> so that subclasses can disable
* it.
* <p>
* The <code>Base</code> property determines the URL
* against which relative URLs are resolved.
* By default, this will be the
* <code>Document.StreamDescriptionProperty</code> if
* the value of the property is a URL. If a <BASE>
* tag is encountered, the base will become the URL specified
* by that tag. Because the base URL is a property, it
* can of course be set directly.
* <p>
* The default content storage mechanism for this document
* is a gap buffer (<code>GapContent</code>).
* Alternatives can be supplied by using the constructor
* that takes a <code>Content</code> implementation.
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans<sup><font size="-2">TM</font></sup>
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*
* @author Timothy Prinzing
* @author Scott Violet
* @author Sunita Mani
* @version 1.180 05/10/06
*/
public class HTMLDocument extends DefaultStyledDocument {
/**
* Constructs an HTML document using the default buffer size
* and a default <code>StyleSheet</code>. This is a convenience
* method for the constructor
* <code>HTMLDocument(Content, StyleSheet)</code>.
*/
public HTMLDocument() {
this(new GapContent(BUFFER_SIZE_DEFAULT), new StyleSheet());
}
/**
* Constructs an HTML document with the default content
* storage implementation and the specified style/attribute
* storage mechanism. This is a convenience method for the
* constructor
* <code>HTMLDocument(Content, StyleSheet)</code>.
*
* @param styles the styles
*/
public HTMLDocument(StyleSheet styles) {
this(new GapContent(BUFFER_SIZE_DEFAULT), styles);
}
/**
* Constructs an HTML document with the given content
* storage implementation and the given style/attribute
* storage mechanism.
*
* @param c the container for the content
* @param styles the styles
*/
public HTMLDocument(Content c, StyleSheet styles) {
super(c, styles);
}
/**
* Fetches the reader for the parser to use when loading the document
* with HTML. This is implemented to return an instance of
* <code>HTMLDocument.HTMLReader</code>.
* Subclasses can reimplement this
* method to change how the document gets structured if desired.
* (For example, to handle custom tags, or structurally represent character
* style elements.)
*
* @param pos the starting position
* @return the reader used by the parser to load the document
*/
public HTMLEditorKit.ParserCallback getReader(int pos) {
Object desc = getProperty(Document.StreamDescriptionProperty);
if (desc instanceof URL) {
setBase((URL)desc);
}
HTMLReader reader = new HTMLReader(pos);
return reader;
}
/**
* Returns the reader for the parser to use to load the document
* with HTML. This is implemented to return an instance of
* <code>HTMLDocument.HTMLReader</code>.
* Subclasses can reimplement this
* method to change how the document gets structured if desired.
* (For example, to handle custom tags, or structurally represent character
* style elements.)
* <p>This is a convenience method for
* <code>getReader(int, int, int, HTML.Tag, TRUE)</code>.
*
* @param popDepth the number of <code>ElementSpec.EndTagTypes</code>
* to generate before inserting
* @param pushDepth the number of <code>ElementSpec.StartTagTypes</code>
* with a direction of <code>ElementSpec.JoinNextDirection</code>
* that should be generated before inserting,
* but after the end tags have been generated
* @param insertTag the first tag to start inserting into document
* @return the reader used by the parser to load the document
*/
public HTMLEditorKit.ParserCallback getReader(int pos, int popDepth,
int pushDepth,
HTML.Tag insertTag) {
return getReader(pos, popDepth, pushDepth, insertTag, true);
}
/**
* Fetches the reader for the parser to use to load the document
* with HTML. This is implemented to return an instance of
* HTMLDocument.HTMLReader. Subclasses can reimplement this
* method to change how the document get structured if desired
* (e.g. to handle custom tags, structurally represent character
* style elements, etc.).
*
* @param popDepth the number of <code>ElementSpec.EndTagTypes</code>
* to generate before inserting
* @param pushDepth the number of <code>ElementSpec.StartTagTypes</code>
* with a direction of <code>ElementSpec.JoinNextDirection</code>
* that should be generated before inserting,
* but after the end tags have been generated
* @param insertTag the first tag to start inserting into document
* @param insertInsertTag false if all the Elements after insertTag should
* be inserted; otherwise insertTag will be inserted
* @return the reader used by the parser to load the document
*/
HTMLEditorKit.ParserCallback getReader(int pos, int popDepth,
int pushDepth,
HTML.Tag insertTag,
boolean insertInsertTag) {
Object desc = getProperty(Document.StreamDescriptionProperty);
if (desc instanceof URL) {
setBase((URL)desc);
}
HTMLReader reader = new HTMLReader(pos, popDepth, pushDepth,
insertTag, insertInsertTag, false,
true);
return reader;
}
/**
* Returns the location to resolve relative URLs against. By
* default this will be the document's URL if the document
* was loaded from a URL. If a base tag is found and
* can be parsed, it will be used as the base location.
*
* @return the base location
*/
public URL getBase() {
return base;
}
/**
* Sets the location to resolve relative URLs against. By
* default this will be the document's URL if the document
* was loaded from a URL. If a base tag is found and
* can be parsed, it will be used as the base location.
* <p>This also sets the base of the <code>StyleSheet</code>
* to be <code>u</code> as well as the base of the document.
*
* @param u the desired base URL
*/
public void setBase(URL u) {
base = u;
getStyleSheet().setBase(u);
}
/**
* Inserts new elements in bulk. This is how elements get created
* in the document. The parsing determines what structure is needed
* and creates the specification as a set of tokens that describe the
* edit while leaving the document free of a write-lock. This method
* can then be called in bursts by the reader to acquire a write-lock
* for a shorter duration (i.e. while the document is actually being
* altered).
*
* @param offset the starting offset
* @param data the element data
* @exception BadLocationException if the given position does not
* represent a valid location in the associated document.
*/
protected void insert(int offset, ElementSpec[] data) throws BadLocationException {
super.insert(offset, data);
}
/**
* Updates document structure as a result of text insertion. This
* will happen within a write lock. This implementation simply
* parses the inserted content for line breaks and builds up a set
* of instructions for the element buffer.
*
* @param chng a description of the document change
* @param attr the attributes
*/
protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
if(attr == null) {
attr = contentAttributeSet;
}
// If this is the composed text element, merge the content attribute to it
else if (attr.isDefined(StyleConstants.ComposedTextAttribute)) {
((MutableAttributeSet)attr).addAttributes(contentAttributeSet);
}
if (attr.isDefined(IMPLIED_CR)) {
((MutableAttributeSet)attr).removeAttribute(IMPLIED_CR);
}
super.insertUpdate(chng, attr);
}
/**
* Replaces the contents of the document with the given
* element specifications. This is called before insert if
* the loading is done in bursts. This is the only method called
* if loading the document entirely in one burst.
*
* @param data the new contents of the document
*/
protected void create(ElementSpec[] data) {
super.create(data);
}
/**
* Sets attributes for a paragraph.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
* to Use Threads</A> for more information.
*
* @param offset the offset into the paragraph (must be at least 0)
* @param length the number of characters affected (must be at least 0)
* @param s the attributes
* @param replace whether to replace existing attributes, or merge them
*/
public void setParagraphAttributes(int offset, int length, AttributeSet s,
boolean replace) {
try {
writeLock();
// Make sure we send out a change for the length of the paragraph.
int end = Math.min(offset + length, getLength());
Element e = getParagraphElement(offset);
offset = e.getStartOffset();
e = getParagraphElement(end);
length = Math.max(0, e.getEndOffset() - offset);
DefaultDocumentEvent changes =
new DefaultDocumentEvent(offset, length,
DocumentEvent.EventType.CHANGE);
AttributeSet sCopy = s.copyAttributes();
int lastEnd = Integer.MAX_VALUE;
for (int pos = offset; pos <= end; pos = lastEnd) {
Element paragraph = getParagraphElement(pos);
if (lastEnd == paragraph.getEndOffset()) {
lastEnd++;
}
else {
lastEnd = paragraph.getEndOffset();
}
MutableAttributeSet attr =
(MutableAttributeSet) paragraph.getAttributes();
changes.addEdit(new AttributeUndoableEdit(paragraph, sCopy, replace));
if (replace) {
attr.removeAttributes(attr);
}
attr.addAttributes(s);
}
changes.end();
fireChangedUpdate(changes);
fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
} finally {
writeUnlock();
}
}
/**
* Fetches the <code>StyleSheet</code> with the document-specific display
* rules (CSS) that were specified in the HTML document itself.
*
* @return the <code>StyleSheet</code>
*/
public StyleSheet getStyleSheet() {
return (StyleSheet) getAttributeContext();
}
/**
* Fetches an iterator for the specified HTML tag.
* This can be used for things like iterating over the
* set of anchors contained, or iterating over the input
* elements.
*
* @param t the requested <code>HTML.Tag</code>
* @return the <code>Iterator</code> for the given HTML tag
* @see javax.swing.text.html.HTML.Tag
*/
public Iterator getIterator(HTML.Tag t) {
if (t.isBlock()) {
// TBD
return null;
}
return new LeafIterator(t, this);
}
/**
* Creates a document leaf element that directly represents
* text (doesn't have any children). This is implemented
* to return an element of type
* <code>HTMLDocument.RunElement</code>.
*
* @param parent the parent element
* @param a the attributes for the element
* @param p0 the beginning of the range (must be at least 0)
* @param p1 the end of the range (must be at least p0)
* @return the new element
*/
protected Element createLeafElement(Element parent, AttributeSet a, int p0, int p1) {
return new RunElement(parent, a, p0, p1);
}
/**
* Creates a document branch element, that can contain other elements.
* This is implemented to return an element of type
* <code>HTMLDocument.BlockElement</code>.
*
* @param parent the parent element
* @param a the attributes
* @return the element
*/
protected Element createBranchElement(Element parent, AttributeSet a) {
return new BlockElement(parent, a);
}
/**
* Creates the root element to be used to represent the
* default document structure.
*
* @return the element base
*/
protected AbstractElement createDefaultRoot() {
// grabs a write-lock for this initialization and
// abandon it during initialization so in normal
// operation we can detect an illegitimate attempt
// to mutate attributes.
writeLock();
MutableAttributeSet a = new SimpleAttributeSet();
a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.HTML);
BlockElement html = new BlockElement(null, a.copyAttributes());
a.removeAttributes(a);
a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.BODY);
BlockElement body = new BlockElement(html, a.copyAttributes());
a.removeAttributes(a);
a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.P);
getStyleSheet().addCSSAttributeFromHTML(a, CSS.Attribute.MARGIN_TOP, "0");
BlockElement paragraph = new BlockElement(body, a.copyAttributes());
a.removeAttributes(a);
a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT);
RunElement brk = new RunElement(paragraph, a, 0, 1);
Element[] buff = new Element[1];
buff[0] = brk;
paragraph.replace(0, 0, buff);
buff[0] = paragraph;
body.replace(0, 0, buff);
buff[0] = body;
html.replace(0, 0, buff);
writeUnlock();
return html;
}
/**
* Sets the number of tokens to buffer before trying to update
* the documents element structure.
*
* @param n the number of tokens to buffer
*/
public void setTokenThreshold(int n) {
putProperty(TokenThreshold, new Integer(n));
}
/**
* Gets the number of tokens to buffer before trying to update
* the documents element structure. The default value is
* <code>Integer.MAX_VALUE</code>.
*
* @return the number of tokens to buffer
*/
public int getTokenThreshold() {
Integer i = (Integer) getProperty(TokenThreshold);
if (i != null) {
return i.intValue();
}
return Integer.MAX_VALUE;
}
/**
* Determines how unknown tags are handled by the parser.
* If set to true, unknown
* tags are put in the model, otherwise they are dropped.
*
* @param preservesTags true if unknown tags should be
* saved in the model, otherwise tags are dropped
* @see javax.swing.text.html.HTML.Tag
*/
public void setPreservesUnknownTags(boolean preservesTags) {
preservesUnknownTags = preservesTags;
}
/**
* Returns the behavior the parser observes when encountering
* unknown tags.
*
* @see javax.swing.text.html.HTML.Tag
* @return true if unknown tags are to be preserved when parsing
*/
public boolean getPreservesUnknownTags() {
return preservesUnknownTags;
}
/**
* Processes <code>HyperlinkEvents</code> that
* are generated by documents in an HTML frame.
* The <code>HyperlinkEvent</code> type, as the parameter suggests,
* is <code>HTMLFrameHyperlinkEvent</code>.
* In addition to the typical information contained in a
* <code>HyperlinkEvent</code>,
* this event contains the element that corresponds to the frame in
* which the click happened (the source element) and the
* target name. The target name has 4 possible values:
* <ul>
* <li> _self
* <li> _parent
* <li> _top
* <li> a named frame
* </ul>
*
* If target is _self, the action is to change the value of the
* <code>HTML.Attribute.SRC</code> attribute and fires a
* <code>ChangedUpdate</code> event.
*<p>
* If the target is _parent, then it deletes the parent element,
* which is a <FRAMESET> element, and inserts a new <FRAME>
* element, and sets its <code>HTML.Attribute.SRC</code> attribute
* to have a value equal to the destination URL and fire a
* <code>RemovedUpdate</code> and <code>InsertUpdate</code>.
*<p>
* If the target is _top, this method does nothing. In the implementation
* of the view for a frame, namely the <code>FrameView</code>,
* the processing of _top is handled. Given that _top implies
* replacing the entire document, it made sense to handle this outside
* of the document that it will replace.
*<p>
* If the target is a named frame, then the element hierarchy is searched
* for an element with a name equal to the target, its
* <code>HTML.Attribute.SRC</code> attribute is updated and a
* <code>ChangedUpdate</code> event is fired.
*
* @param e the event
*/
public void processHTMLFrameHyperlinkEvent(HTMLFrameHyperlinkEvent e) {
String frameName = e.getTarget();
Element element = e.getSourceElement();
String urlStr = e.getURL().toString();
if (frameName.equals("_self")) {
/*
The source and destination elements
are the same.
*/
updateFrame(element, urlStr);
} else if (frameName.equals("_parent")) {
/*
The destination is the parent of the frame.
*/
updateFrameSet(element.getParentElement(), urlStr);
} else {
/*
locate a named frame
*/
Element targetElement = findFrame(frameName);
if (targetElement != null) {
updateFrame(targetElement, urlStr);
}
}
}
/**
* Searches the element hierarchy for an FRAME element
* that has its name attribute equal to the <code>frameName</code>.
*
* @param frameName
* @return the element whose NAME attribute has a value of
* <code>frameName</code>; returns <code>null</code>
* if not found
*/
private Element findFrame(String frameName) {
ElementIterator it = new ElementIterator(this);
Element next = null;
while ((next = it.next()) != null) {
AttributeSet attr = next.getAttributes();
if (matchNameAttribute(attr, HTML.Tag.FRAME)) {
String frameTarget = (String)attr.getAttribute(HTML.Attribute.NAME);
if (frameTarget != null && frameTarget.equals(frameName)) {
break;
}
}
}
return next;
}
/**
* Returns true if <code>StyleConstants.NameAttribute</code> is
* equal to the tag that is passed in as a parameter.
*
* @param attr the attributes to be matched
* @param tag the value to be matched
* @return true if there is a match, false otherwise
* @see javax.swing.text.html.HTML.Attribute
*/
static boolean matchNameAttribute(AttributeSet attr, HTML.Tag tag) {
Object o = attr.getAttribute(StyleConstants.NameAttribute);
if (o instanceof HTML.Tag) {
HTML.Tag name = (HTML.Tag) o;
if (name == tag) {
return true;
}
}
return false;
}
/**
* Replaces a frameset branch Element with a frame leaf element.
*
* @param element the frameset element to remove
* @param url the value for the SRC attribute for the
* new frame that will replace the frameset
*/
private void updateFrameSet(Element element, String url) {
try {
int startOffset = element.getStartOffset();
int endOffset = Math.min(getLength(), element.getEndOffset());
String html = "<frame";
if (url != null) {
html += " src=\"" + url + "\"";
}
html += ">";
installParserIfNecessary();
setOuterHTML(element, html);
} catch (BadLocationException e1) {
// Should handle this better
} catch (IOException ioe) {
// Should handle this better
}
}
/**
* Updates the Frame elements <code>HTML.Attribute.SRC attribute</code>
* and fires a <code>ChangedUpdate</code> event.
*
* @param element a FRAME element whose SRC attribute will be updated
* @param url a string specifying the new value for the SRC attribute
*/
private void updateFrame(Element element, String url) {
try {
writeLock();
DefaultDocumentEvent changes = new DefaultDocumentEvent(element.getStartOffset(),
1,
DocumentEvent.EventType.CHANGE);
AttributeSet sCopy = element.getAttributes().copyAttributes();
MutableAttributeSet attr = (MutableAttributeSet) element.getAttributes();
changes.addEdit(new AttributeUndoableEdit(element, sCopy, false));
attr.removeAttribute(HTML.Attribute.SRC);
attr.addAttribute(HTML.Attribute.SRC, url);
changes.end();
fireChangedUpdate(changes);
fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
} finally {
writeUnlock();
}
}
/**
* Returns true if the document will be viewed in a frame.
* @return true if document will be viewed in a frame, otherwise false
*/
boolean isFrameDocument() {
return frameDocument;
}
/**
* Sets a boolean state about whether the document will be
* viewed in a frame.
* @param frameDoc true if the document will be viewed in a frame,
* otherwise false
*/
void setFrameDocumentState(boolean frameDoc) {
this.frameDocument = frameDoc;
}
/**
* Adds the specified map, this will remove a Map that has been
* previously registered with the same name.
*
* @param map the <code>Map</code> to be registered
*/
void addMap(Map map) {
String name = map.getName();
if (name != null) {
Object maps = getProperty(MAP_PROPERTY);
if (maps == null) {
maps = new Hashtable(11);
putProperty(MAP_PROPERTY, maps);
}
if (maps instanceof Hashtable) {
((Hashtable)maps).put("#" + name, map);
}
}
}
/**
* Removes a previously registered map.
* @param map the <code>Map</code> to be removed
*/
void removeMap(Map map) {
String name = map.getName();
if (name != null) {
Object maps = getProperty(MAP_PROPERTY);
if (maps instanceof Hashtable) {
((Hashtable)maps).remove("#" + name);
}
}
}
/**
* Returns the Map associated with the given name.
* @param the name of the desired <code>Map</code>
* @return the <code>Map</code> or <code>null</code> if it can't
* be found, or if <code>name</code> is <code>null</code>
*/
Map getMap(String name) {
if (name != null) {
Object maps = getProperty(MAP_PROPERTY);
if (maps != null && (maps instanceof Hashtable)) {
return (Map)((Hashtable)maps).get(name);
}
}
return null;
}
/**
* Returns an <code>Enumeration</code> of the possible Maps.
* @return the enumerated list of maps, or <code>null</code>
* if the maps are not an instance of <code>Hashtable</code>
*/
Enumeration getMaps() {
Object maps = getProperty(MAP_PROPERTY);
if (maps instanceof Hashtable) {
return ((Hashtable)maps).elements();
}
return null;
}
/**
* Sets the content type language used for style sheets that do not
* explicitly specify the type. The default is text/css.
* @param contentType the content type language for the style sheets
*/
/* public */
void setDefaultStyleSheetType(String contentType) {
putProperty(StyleType, contentType);
}
/**
* Returns the content type language used for style sheets. The default
* is text/css.
* @return the content type language used for the style sheets
*/
/* public */
String getDefaultStyleSheetType() {
String retValue = (String)getProperty(StyleType);
if (retValue == null) {
return "text/css";
}
return retValue;
}
/**
* Sets the parser that is used by the methods that insert html
* into the existing document, such as <code>setInnerHTML</code>,
* and <code>setOuterHTML</code>.
* <p>
* <code>HTMLEditorKit.createDefaultDocument</code> will set the parser
* for you. If you create an <code>HTMLDocument</code> by hand,
* be sure and set the parser accordingly.
* @param parser the parser to be used for text insertion
*
* @since 1.3
*/
public void setParser(HTMLEditorKit.Parser parser) {
this.parser = parser;
putProperty("__PARSER__", null);
}
/**
* Returns the parser that is used when inserting HTML into the existing
* document.
* @return the parser used for text insertion
*
* @since 1.3
*/
public HTMLEditorKit.Parser getParser() {
Object p = getProperty("__PARSER__");
if (p instanceof HTMLEditorKit.Parser) {
return (HTMLEditorKit.Parser)p;
}
return parser;
}
/**
* Replaces the children of the given element with the contents
* specified as an HTML string.
* <p>This will be seen as at least two events, n inserts followed by
* a remove.
* <p>For this to work correcty, the document must have an
* <code>HTMLEditorKit.Parser</code> set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument</code> method.
*
* @param elem the branch element whose children will be replaced
* @param htmlText the string to be parsed and assigned to <code>elem</code>
* @throws IllegalArgumentException if <code>elem</code> is a leaf
* @throws IllegalStateException if an <code>HTMLEditorKit.Parser</code>
* has not been defined
* @since 1.3
*/
public void setInnerHTML(Element elem, String htmlText) throws
BadLocationException, IOException {
verifyParser();
if (elem != null && elem.isLeaf()) {
throw new IllegalArgumentException
("Can not set inner HTML of a leaf");
}
if (elem != null && htmlText != null) {
int oldCount = elem.getElementCount();
int insertPosition = elem.getStartOffset();
insertHTML(elem, elem.getStartOffset(), htmlText, true);
if (elem.getElementCount() > oldCount) {
// Elements were inserted, do the cleanup.
removeElements(elem, elem.getElementCount() - oldCount,
oldCount);
}
}
}
/**
* Replaces the given element in the parent with the contents
* specified as an HTML string.
* <p>This will be seen as at least two events, n inserts followed by
* a remove.
* <p>When replacing a leaf this will attempt to make sure there is
* a newline present if one is needed. This may result in an additional
* element being inserted. Consider, if you were to replace a character
* element that contained a newline with <img> this would create
* two elements, one for the image, ane one for the newline.
* <p>If you try to replace the element at length you will most likely
* end up with two elements, eg setOuterHTML(getCharacterElement
* (getLength()), "blah") will result in two leaf elements at the end,
* one representing 'blah', and the other representing the end element.
* <p>For this to work correcty, the document must have an
* HTMLEditorKit.Parser set. This will be the case if the document
* was created from an HTMLEditorKit via the
* <code>createDefaultDocument</code> method.
*
* @param elem the branch element whose children will be replaced
* @param htmlText the string to be parsed and assigned to <code>elem</code>
* @throws IllegalStateException if an HTMLEditorKit.Parser has not
* been set
* @since 1.3
*/
public void setOuterHTML(Element elem, String htmlText) throws
BadLocationException, IOException {
verifyParser();
if (elem != null && elem.getParentElement() != null &&
htmlText != null) {
int start = elem.getStartOffset();
int end = elem.getEndOffset();
int startLength = getLength();
// We don't want a newline if elem is a leaf, and doesn't contain
// a newline.
boolean wantsNewline = !elem.isLeaf();
if (!wantsNewline && (end > startLength ||
getText(end - 1, 1).charAt(0) == NEWLINE[0])){
wantsNewline = true;
}
Element parent = elem.getParentElement();
int oldCount = parent.getElementCount();
insertHTML(parent, start, htmlText, wantsNewline);
// Remove old.
int newLength = getLength();
if (oldCount != parent.getElementCount()) {
int removeIndex = parent.getElementIndex(start + newLength -
startLength);
removeElements(parent, removeIndex, 1);
}
}
}
/**
* Inserts the HTML specified as a string at the start
* of the element.
* <p>For this to work correcty, the document must have an
* <code>HTMLEditorKit.Parser</code> set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument</code> method.
*
* @param elem the branch element to be the root for the new text
* @param htmlText the string to be parsed and assigned to <code>elem</code>
* @throws IllegalStateException if an HTMLEditorKit.Parser has not
* been set on the document
* @since 1.3
*/
public void insertAfterStart(Element elem, String htmlText) throws
BadLocationException, IOException {
verifyParser();
if (elem != null && elem.isLeaf()) {
throw new IllegalArgumentException
("Can not insert HTML after start of a leaf");
}
insertHTML(elem, elem.getStartOffset(), htmlText, false);
}
/**
* Inserts the HTML specified as a string at the end of
* the element.
* <p> If <code>elem</code>'s children are leaves, and the
* character at a <code>elem.getEndOffset() - 1</code> is a newline,
* this will insert before the newline so that there isn't text after
* the newline.
* <p>For this to work correcty, the document must have an
* <code>HTMLEditorKit.Parser</code> set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument</code> method.
*
* @param elem the element to be the root for the new text
* @param htmlText the string to be parsed and assigned to <code>elem</code>
* @throws IllegalStateException if an HTMLEditorKit.Parser has not
* been set on the document
* @since 1.3
*/
public void insertBeforeEnd(Element elem, String htmlText) throws
BadLocationException, IOException {
verifyParser();
if (elem != null && elem.isLeaf()) {
throw new IllegalArgumentException
("Can not set inner HTML before end of leaf");
}
int offset = elem.getEndOffset();
if (elem.getElement(elem.getElementIndex(offset - 1)).isLeaf() &&
getText(offset - 1, 1).charAt(0) == NEWLINE[0]) {
offset--;
}
insertHTML(elem, offset, htmlText, false);
}
/**
* Inserts the HTML specified as a string before the start of
* the given element.
* <p>For this to work correcty, the document must have an
* <code>HTMLEditorKit.Parser</code> set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument</code> method.
*
* @param elem the element to be the root for the new text
* @param htmlText the string to be parsed and assigned to <code>elem</code>
* @throws IllegalStateException if an HTMLEditorKit.Parser has not
* been set on the document
* @since 1.3
*/
public void insertBeforeStart(Element elem, String htmlText) throws
BadLocationException, IOException {
verifyParser();
if (elem != null) {
Element parent = elem.getParentElement();
if (parent != null) {
insertHTML(parent, elem.getStartOffset(), htmlText, false);
}
}
}
/**
* Inserts the HTML specified as a string after the
* the end of the given element.
* <p>For this to work correcty, the document must have an
* <code>HTMLEditorKit.Parser</code> set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument</code> method.
*
* @param elem the element to be the root for the new text
* @param htmlText the string to be parsed and assigned to <code>elem</code>
* @throws IllegalStateException if an HTMLEditorKit.Parser has not
* been set on the document
* @since 1.3
*/
public void insertAfterEnd(Element elem, String htmlText) throws
BadLocationException, IOException {
verifyParser();
if (elem != null) {
Element parent = elem.getParentElement();
if (parent != null) {
int offset = elem.getEndOffset();
if (offset > getLength()) {
offset--;
}
else if (elem.isLeaf() && getText(offset - 1, 1).
charAt(0) == NEWLINE[0]) {
offset--;
}
insertHTML(parent, offset, htmlText, false);
}
}
}
/**
* Returns the element that has the given id <code>Attribute</code>.
* If the element can't be found, <code>null</code> is returned.
* Note that this method works on an <code>Attribute</code>,
* <i>not</i> a character tag. In the following HTML snippet:
* <code><a id="HelloThere"></code> the attribute is
* 'id' and the character tag is 'a'.
* This is a convenience method for
* <code>getElement(RootElement, HTML.Attribute.id, id)</code>.
* This is not thread-safe.
*
* @param id the string representing the desired <code>Attribute</code>
* @return the element with the specified <code>Attribute</code>
* or <code>null</code> if it can't be found,
* or <code>null</code> if <code>id</code> is <code>null</code>
* @see javax.swing.text.html.HTML.Attribute
* @since 1.3
*/
public Element getElement(String id) {
if (id == null) {
return null;
}
return getElement(getDefaultRootElement(), HTML.Attribute.ID, id,
true);
}
/**
* Returns the child element of <code>e</code> that contains the
* attribute, <code>attribute</code> with value <code>value</code>, or
* <code>null</code> if one isn't found. This is not thread-safe.
*
* @param e the root element where the search begins
* @param attribute the desired <code>Attribute</code>
* @param value the values for the specified <code>Attribute</code>
* @return the element with the specified <code>Attribute</code>
* and the specified <code>value</code>, or <code>null</code>
* if it can't be found
* @see javax.swing.text.html.HTML.Attribute
* @since 1.3
*/
public Element getElement(Element e, Object attribute, Object value) {
return getElement(e, attribute, value, true);
}
/**
* Returns the child element of <code>e</code> that contains the
* attribute, <code>attribute</code> with value <code>value</code>, or
* <code>null</code> if one isn't found. This is not thread-safe.
* <p>
* If <code>searchLeafAttributes</code> is true, and <code>e</code> is
* a leaf, any attributes that are instances of <code>HTML.Tag</code>
* with a value that is an <code>AttributeSet</code> will also be checked.
*
* @param e the root element where the search begins
* @param attribute the desired <code>Attribute</code>
* @param value the values for the specified <code>Attribute</code>
* @return the element with the specified <code>Attribute</code>
* and the specified <code>value</code>, or <code>null</code>
* if it can't be found
* @see javax.swing.text.html.HTML.Attribute
*/
private Element getElement(Element e, Object attribute, Object value,
boolean searchLeafAttributes) {
AttributeSet attr = e.getAttributes();
if (attr != null && attr.isDefined(attribute)) {
if (value.equals(attr.getAttribute(attribute))) {
return e;
}
}
if (!e.isLeaf()) {
for (int counter = 0, maxCounter = e.getElementCount();
counter < maxCounter; counter++) {
Element retValue = getElement(e.getElement(counter), attribute,
value, searchLeafAttributes);
if (retValue != null) {
return retValue;
}
}
}
else if (searchLeafAttributes && attr != null) {
// For some leaf elements we store the actual attributes inside
// the AttributeSet of the Element (such as anchors).
Enumeration names = attr.getAttributeNames();
if (names != null) {
while (names.hasMoreElements()) {
Object name = names.nextElement();
if ((name instanceof HTML.Tag) &&
(attr.getAttribute(name) instanceof AttributeSet)) {
AttributeSet check = (AttributeSet)attr.
getAttribute(name);
if (check.isDefined(attribute) &&
value.equals(check.getAttribute(attribute))) {
return e;
}
}
}
}
}
return null;
}
/**
* Verifies the document has an <code>HTMLEditorKit.Parser</code> set.
* If <code>getParser</code> returns <code>null</code>, this will throw an
* IllegalStateException.
*
* @throws IllegalStateException if the document does not have a Parser
*/
private void verifyParser() {
if (getParser() == null) {
throw new IllegalStateException("No HTMLEditorKit.Parser");
}
}
/**
* Installs a default Parser if one has not been installed yet.
*/
private void installParserIfNecessary() {
if (getParser() == null) {
setParser(new HTMLEditorKit().getParser());
}
}
/**
* Inserts a string of HTML into the document at the given position.
* <code>parent</code> is used to identify the location to insert the
* <code>html</code>. If <code>parent</code> is a leaf this can have
* unexpected results.
*/
private void insertHTML(Element parent, int offset, String html,
boolean wantsTrailingNewline)
throws BadLocationException, IOException {
if (parent != null && html != null) {
HTMLEditorKit.Parser parser = getParser();
if (parser != null) {
int lastOffset = Math.max(0, offset - 1);
Element charElement = getCharacterElement(lastOffset);
Element commonParent = parent;
int pop = 0;
int push = 0;
if (parent.getStartOffset() > lastOffset) {
while (commonParent != null &&
commonParent.getStartOffset() > lastOffset) {
commonParent = commonParent.getParentElement();
push++;
}
if (commonParent == null) {
throw new BadLocationException("No common parent",
offset);
}
}
while (charElement != null && charElement != commonParent) {
pop++;
charElement = charElement.getParentElement();
}
if (charElement != null) {
// Found it, do the insert.
HTMLReader reader = new HTMLReader(offset, pop - 1, push,
null, false, true,
wantsTrailingNewline);
parser.parse(new StringReader(html), reader, true);
reader.flush();
}
}
}
}
/**
* Removes child Elements of the passed in Element <code>e</code>. This
* will do the necessary cleanup to ensure the element representing the
* end character is correctly created.
* <p>This is not a general purpose method, it assumes that <code>e</code>
* will still have at least one child after the remove, and it assumes
* the character at <code>e.getStartOffset() - 1</code> is a newline and
* is of length 1.
*/
private void removeElements(Element e, int index, int count) throws BadLocationException {
writeLock();
try {
int start = e.getElement(index).getStartOffset();
int end = e.getElement(index + count - 1).getEndOffset();
if (end > getLength()) {
removeElementsAtEnd(e, index, count, start, end);
}
else {
removeElements(e, index, count, start, end);
}
} finally {
writeUnlock();
}
}
/**
* Called to remove child elements of <code>e</code> when one of the
* elements to remove is representing the end character.
* <p>Since the Content will not allow a removal to the end character
* this will do a remove from <code>start - 1</code> to <code>end</code>.
* The end Element(s) will be removed, and the element representing
* <code>start - 1</code> to <code>start</code> will be recreated. This
* Element has to be recreated as after the content removal its offsets
* become <code>start - 1</code> to <code>start - 1</code>.
*/
private void removeElementsAtEnd(Element e, int index, int count,
int start, int end) throws BadLocationException {
// index must be > 0 otherwise no insert would have happened.
boolean isLeaf = (e.getElement(index - 1).isLeaf());
DefaultDocumentEvent dde = new DefaultDocumentEvent(
start - 1, end - start + 1, DocumentEvent.
EventType.REMOVE);
if (isLeaf) {
Element endE = getCharacterElement(getLength());
// e.getElement(index - 1) should represent the newline.
index--;
if (endE.getParentElement() != e) {
// The hiearchies don't match, we'll have to manually
// recreate the leaf at e.getElement(index - 1)
replace(dde, e, index, ++count, start, end, true, true);
}
else {
// The hierarchies for the end Element and
// e.getElement(index - 1), match, we can safely remove
// the Elements and the end content will be aligned
// appropriately.
replace(dde, e, index, count, start, end, true, false);
}
}
else {
// Not a leaf, descend until we find the leaf representing
// start - 1 and remove it.
Element newLineE = e.getElement(index - 1);
while (!newLineE.isLeaf()) {
newLineE = newLineE.getElement(newLineE.getElementCount() - 1);
}
newLineE = newLineE.getParentElement();
replace(dde, e, index, count, start, end, false, false);
replace(dde, newLineE, newLineE.getElementCount() - 1, 1, start,
end, true, true);
}
postRemoveUpdate(dde);
dde.end();
fireRemoveUpdate(dde);
fireUndoableEditUpdate(new UndoableEditEvent(this, dde));
}
/**
* This is used by <code>removeElementsAtEnd</code>, it removes
* <code>count</code> elements starting at <code>start</code> from
* <code>e</code>. If <code>remove</code> is true text of length
* <code>start - 1</code> to <code>end - 1</code> is removed. If
* <code>create</code> is true a new leaf is created of length 1.
*/
private void replace(DefaultDocumentEvent dde, Element e, int index,
int count, int start, int end, boolean remove,
boolean create) throws BadLocationException {
Element[] added;
AttributeSet attrs = e.getElement(index).getAttributes();
Element[] removed = new Element[count];
for (int counter = 0; counter < count; counter++) {
removed[counter] = e.getElement(counter + index);
}
if (remove) {
UndoableEdit u = getContent().remove(start - 1, end - start);
if (u != null) {
dde.addEdit(u);
}
}
if (create) {
added = new Element[1];
added[0] = createLeafElement(e, attrs, start - 1, start);
}
else {
added = new Element[0];
}
dde.addEdit(new ElementEdit(e, index, removed, added));
((AbstractDocument.BranchElement)e).replace(
index, removed.length, added);
}
/**
* Called to remove child Elements when the end is not touched.
*/
private void removeElements(Element e, int index, int count,
int start, int end) throws BadLocationException {
Element[] removed = new Element[count];
Element[] added = new Element[0];
for (int counter = 0; counter < count; counter++) {
removed[counter] = e.getElement(counter + index);
}
DefaultDocumentEvent dde = new DefaultDocumentEvent
(start, end - start, DocumentEvent.EventType.REMOVE);
((AbstractDocument.BranchElement)e).replace(index, removed.length,
added);
dde.addEdit(new ElementEdit(e, index, removed, added));
UndoableEdit u = getContent().remove(start, end - start);
if (u != null) {
dde.addEdit(u);
}
postRemoveUpdate(dde);
dde.end();
fireRemoveUpdate(dde);
if (u != null) {
fireUndoableEditUpdate(new UndoableEditEvent(this, dde));
}
}
// These two are provided for inner class access. The are named different
// than the super class as the super class implementations are final.
void obtainLock() {
writeLock();
}
void releaseLock() {
writeUnlock();
}
//
// Provided for inner class access.
//
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the parameters passed into
* the fire method.
*
* @param e the event
* @see EventListenerList
*/
protected void fireChangedUpdate(DocumentEvent e) {
super.fireChangedUpdate(e);
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the parameters passed into
* the fire method.
*
* @param e the event
* @see EventListenerList
*/
protected void fireUndoableEditUpdate(UndoableEditEvent e) {
super.fireUndoableEditUpdate(e);
}
boolean hasBaseTag() {
return hasBaseTag;
}
String getBaseTarget() {
return baseTarget;
}
/*
* state defines whether the document is a frame document
* or not.
*/
private boolean frameDocument = false;
private boolean preservesUnknownTags = true;
/*
* Used to store button groups for radio buttons in
* a form.
*/
private HashMap radioButtonGroupsMap;
/**
* Document property for the number of tokens to buffer
* before building an element subtree to represent them.
*/
static final String TokenThreshold = "token threshold";
private static final int MaxThreshold = 10000;
private static final int StepThreshold = 5;
/**
* Document property key value. The value for the key will be a Vector
* of Strings that are comments not found in the body.
*/
public static final String AdditionalComments = "AdditionalComments";
/**
* Document property key value. The value for the key will be a
* String indicating the default type of stylesheet links.
*/
/* public */ static final String StyleType = "StyleType";
/**
* The location to resolve relative URLs against. By
* default this will be the document's URL if the document
* was loaded from a URL. If a base tag is found and
* can be parsed, it will be used as the base location.
*/
URL base;
/**
* does the document have base tag
*/
boolean hasBaseTag = false;
/**
* BASE tag's TARGET attribute value
*/
private String baseTarget = null;
/**
* The parser that is used when inserting html into the existing
* document.
*/
private HTMLEditorKit.Parser parser;
/**
* Used for inserts when a null AttributeSet is supplied.
*/
private static AttributeSet contentAttributeSet;
/**
* Property Maps are registered under, will be a Hashtable.
*/
static String MAP_PROPERTY = "__MAP__";
private static char[] NEWLINE;
private static final String IMPLIED_CR = "CR";
/**
* I18N property key.
*
* @see AbstractDocument.I18NProperty
*/
private static final String I18NProperty = "i18n";
static {
contentAttributeSet = new SimpleAttributeSet();
((MutableAttributeSet)contentAttributeSet).
addAttribute(StyleConstants.NameAttribute,
HTML.Tag.CONTENT);
NEWLINE = new char[1];
NEWLINE[0] = '\n';
}
/**
* An iterator to iterate over a particular type of
* tag. The iterator is not thread safe. If reliable
* access to the document is not already ensured by
* the context under which the iterator is being used,
* its use should be performed under the protection of
* Document.render.
*/
public static abstract class Iterator {
/**
* Return the attributes for this tag.
* @return the <code>AttributeSet</code> for this tag, or
* <code>null</code> if none can be found
*/
public abstract AttributeSet getAttributes();
/**
* Returns the start of the range for which the current occurrence of
* the tag is defined and has the same attributes.
*
* @return the start of the range, or -1 if it can't be found
*/
public abstract int getStartOffset();
/**
* Returns the end of the range for which the current occurrence of
* the tag is defined and has the same attributes.
*
* @return the end of the range
*/
public abstract int getEndOffset();
/**
* Move the iterator forward to the next occurrence
* of the tag it represents.
*/
public abstract void next();
/**
* Indicates if the iterator is currently
* representing an occurrence of a tag. If
* false there are no more tags for this iterator.
* @return true if the iterator is currently representing an
* occurrence of a tag, otherwise returns false
*/
public abstract boolean isValid();
/**
* Type of tag this iterator represents.
*/
public abstract HTML.Tag getTag();
}
/**
* An iterator to iterate over a particular type of tag.
*/
static class LeafIterator extends Iterator {
LeafIterator(HTML.Tag t, Document doc) {
tag = t;
pos = new ElementIterator(doc);
endOffset = 0;
next();
}
/**
* Returns the attributes for this tag.
* @return the <code>AttributeSet</code> for this tag,
* or <code>null</code> if none can be found
*/
public AttributeSet getAttributes() {
Element elem = pos.current();
if (elem != null) {
AttributeSet a = (AttributeSet)
elem.getAttributes().getAttribute(tag);
if (a == null) {
a = (AttributeSet)elem.getAttributes();
}
return a;
}
return null;
}
/**
* Returns the start of the range for which the current occurrence of
* the tag is defined and has the same attributes.
*
* @return the start of the range, or -1 if it can't be found
*/
public int getStartOffset() {
Element elem = pos.current();
if (elem != null) {
return elem.getStartOffset();
}
return -1;
}
/**
* Returns the end of the range for which the current occurrence of
* the tag is defined and has the same attributes.
*
* @return the end of the range
*/
public int getEndOffset() {
return endOffset;
}
/**
* Moves the iterator forward to the next occurrence
* of the tag it represents.
*/
public void next() {
for (nextLeaf(pos); isValid(); nextLeaf(pos)) {
Element elem = pos.current();
if (elem.getStartOffset() >= endOffset) {
AttributeSet a = pos.current().getAttributes();
if (a.isDefined(tag) ||
a.getAttribute(StyleConstants.NameAttribute) == tag) {
// we found the next one
setEndOffset();
break;
}
}
}
}
/**
* Returns the type of tag this iterator represents.
*
* @return the <code>HTML.Tag</code> that this iterator represents.
* @see javax.swing.text.html.HTML.Tag
*/
public HTML.Tag getTag() {
return tag;
}
/**
* Returns true if the current position is not <code>null</code>.
* @return true if current position is not <code>null</code>,
* otherwise returns false
*/
public boolean isValid() {
return (pos.current() != null);
}
/**
* Moves the given iterator to the next leaf element.
* @param iter the iterator to be scanned
*/
void nextLeaf(ElementIterator iter) {
for (iter.next(); iter.current() != null; iter.next()) {
Element e = iter.current();
if (e.isLeaf()) {
break;
}
}
}
/**
* Marches a cloned iterator forward to locate the end
* of the run. This sets the value of <code>endOffset</code>.
*/
void setEndOffset() {
AttributeSet a0 = getAttributes();
endOffset = pos.current().getEndOffset();
ElementIterator fwd = (ElementIterator) pos.clone();
for (nextLeaf(fwd); fwd.current() != null; nextLeaf(fwd)) {
Element e = fwd.current();
AttributeSet a1 = (AttributeSet) e.getAttributes().getAttribute(tag);
if ((a1 == null) || (! a1.equals(a0))) {
break;
}
endOffset = e.getEndOffset();
}
}
private int endOffset;
private HTML.Tag tag;
private ElementIterator pos;
}
/**
* An HTML reader to load an HTML document with an HTML
* element structure. This is a set of callbacks from
* the parser, implemented to create a set of elements
* tagged with attributes. The parse builds up tokens
* (ElementSpec) that describe the element subtree desired,
* and burst it into the document under the protection of
* a write lock using the insert method on the document
* outer class.
* <p>
* The reader can be configured by registering actions
* (of type <code>HTMLDocument.HTMLReader.TagAction</code>)
* that describe how to handle the action. The idea behind
* the actions provided is that the most natural text editing
* operations can be provided if the element structure boils
* down to paragraphs with runs of some kind of style
* in them. Some things are more naturally specified
* structurally, so arbitrary structure should be allowed
* above the paragraphs, but will need to be edited with structural
* actions. The implication of this is that some of the
* HTML elements specified in the stream being parsed will
* be collapsed into attributes, and in some cases paragraphs
* will be synthesized. When HTML elements have been
* converted to attributes, the attribute key will be of
* type HTML.Tag, and the value will be of type AttributeSet
* so that no information is lost. This enables many of the
* existing actions to work so that the user can type input,
* hit the return key, backspace, delete, etc and have a
* reasonable result. Selections can be created, and attributes
* applied or removed, etc. With this in mind, the work done
* by the reader can be categorized into the following kinds
* of tasks:
* <dl>
* <dt>Block
* <dd>Build the structure like it's specified in the stream.
* This produces elements that contain other elements.
* <dt>Paragraph
* <dd>Like block except that it's expected that the element
* will be used with a paragraph view so a paragraph element
* won't need to be synthesized.
* <dt>Character
* <dd>Contribute the element as an attribute that will start
* and stop at arbitrary text locations. This will ultimately
* be mixed into a run of text, with all of the currently
* flattened HTML character elements.
* <dt>Special
* <dd>Produce an embedded graphical element.
* <dt>Form
* <dd>Produce an element that is like the embedded graphical
* element, except that it also has a component model associated
* with it.
* <dt>Hidden
* <dd>Create an element that is hidden from view when the
* document is being viewed read-only, and visible when the
* document is being edited. This is useful to keep the
* model from losing information, and used to store things
* like comments and unrecognized tags.
*
* </dl>
* <p>
* Currently, <APPLET>, <PARAM>, <MAP>, <AREA>, <LINK>,
* <SCRIPT> and <STYLE> are unsupported.
*
* <p>
* The assignment of the actions described is shown in the
* following table for the tags defined in <code>HTML.Tag</code>.<P>
* <table border=1 summary="HTML tags and assigned actions">
* <tr><th>Tag</th><th>Action</th></tr>
* <tr><td><code>HTML.Tag.A</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.ADDRESS</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.APPLET</code> <td>HiddenAction
* <tr><td><code>HTML.Tag.AREA</code> <td>AreaAction
* <tr><td><code>HTML.Tag.B</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.BASE</code> <td>BaseAction
* <tr><td><code>HTML.Tag.BASEFONT</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.BIG</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.BLOCKQUOTE</code><td>BlockAction
* <tr><td><code>HTML.Tag.BODY</code> <td>BlockAction
* <tr><td><code>HTML.Tag.BR</code> <td>SpecialAction
* <tr><td><code>HTML.Tag.CAPTION</code> <td>BlockAction
* <tr><td><code>HTML.Tag.CENTER</code> <td>BlockAction
* <tr><td><code>HTML.Tag.CITE</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.CODE</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.DD</code> <td>BlockAction
* <tr><td><code>HTML.Tag.DFN</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.DIR</code> <td>BlockAction
* <tr><td><code>HTML.Tag.DIV</code> <td>BlockAction
* <tr><td><code>HTML.Tag.DL</code> <td>BlockAction
* <tr><td><code>HTML.Tag.DT</code> <td>ParagraphAction
* <tr><td><code>HTML.Tag.EM</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.FONT</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.FORM</code> <td>As of 1.4 a BlockAction
* <tr><td><code>HTML.Tag.FRAME</code> <td>SpecialAction
* <tr><td><code>HTML.Tag.FRAMESET</code> <td>BlockAction
* <tr><td><code>HTML.Tag.H1</code> <td>ParagraphAction
* <tr><td><code>HTML.Tag.H2</code> <td>ParagraphAction
* <tr><td><code>HTML.Tag.H3</code> <td>ParagraphAction
* <tr><td><code>HTML.Tag.H4</code> <td>ParagraphAction
* <tr><td><code>HTML.Tag.H5</code> <td>ParagraphAction
* <tr><td><code>HTML.Tag.H6</code> <td>ParagraphAction
* <tr><td><code>HTML.Tag.HEAD</code> <td>HeadAction
* <tr><td><code>HTML.Tag.HR</code> <td>SpecialAction
* <tr><td><code>HTML.Tag.HTML</code> <td>BlockAction
* <tr><td><code>HTML.Tag.I</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.IMG</code> <td>SpecialAction
* <tr><td><code>HTML.Tag.INPUT</code> <td>FormAction
* <tr><td><code>HTML.Tag.ISINDEX</code> <td>IsndexAction
* <tr><td><code>HTML.Tag.KBD</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.LI</code> <td>BlockAction
* <tr><td><code>HTML.Tag.LINK</code> <td>LinkAction
* <tr><td><code>HTML.Tag.MAP</code> <td>MapAction
* <tr><td><code>HTML.Tag.MENU</code> <td>BlockAction
* <tr><td><code>HTML.Tag.META</code> <td>MetaAction
* <tr><td><code>HTML.Tag.NOFRAMES</code> <td>BlockAction
* <tr><td><code>HTML.Tag.OBJECT</code> <td>SpecialAction
* <tr><td><code>HTML.Tag.OL</code> <td>BlockAction
* <tr><td><code>HTML.Tag.OPTION</code> <td>FormAction
* <tr><td><code>HTML.Tag.P</code> <td>ParagraphAction
* <tr><td><code>HTML.Tag.PARAM</code> <td>HiddenAction
* <tr><td><code>HTML.Tag.PRE</code> <td>PreAction
* <tr><td><code>HTML.Tag.SAMP</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.SCRIPT</code> <td>HiddenAction
* <tr><td><code>HTML.Tag.SELECT</code> <td>FormAction
* <tr><td><code>HTML.Tag.SMALL</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.STRIKE</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.S</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.STRONG</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.STYLE</code> <td>StyleAction
* <tr><td><code>HTML.Tag.SUB</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.SUP</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.TABLE</code> <td>BlockAction
* <tr><td><code>HTML.Tag.TD</code> <td>BlockAction
* <tr><td><code>HTML.Tag.TEXTAREA</code> <td>FormAction
* <tr><td><code>HTML.Tag.TH</code> <td>BlockAction
* <tr><td><code>HTML.Tag.TITLE</code> <td>TitleAction
* <tr><td><code>HTML.Tag.TR</code> <td>BlockAction
* <tr><td><code>HTML.Tag.TT</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.U</code> <td>CharacterAction
* <tr><td><code>HTML.Tag.UL</code> <td>BlockAction
* <tr><td><code>HTML.Tag.VAR</code> <td>CharacterAction
* </table>
* <p>
* Once </html> is encountered, the Actions are no longer notified.
*/
public class HTMLReader extends HTMLEditorKit.ParserCallback {
public HTMLReader(int offset) {
this(offset, 0, 0, null);
}
public HTMLReader(int offset, int popDepth, int pushDepth,
HTML.Tag insertTag) {
this(offset, popDepth, pushDepth, insertTag, true, false, true);
}
/**
* Generates a RuntimeException (will eventually generate
* a BadLocationException when API changes are alloced) if inserting
* into non empty document, <code>insertTag</code> is
* non-<code>null</code>, and <code>offset</code> is not in the body.
*/
// PENDING(sky): Add throws BadLocationException and remove
// RuntimeException
HTMLReader(int offset, int popDepth, int pushDepth,
HTML.Tag insertTag, boolean insertInsertTag,
boolean insertAfterImplied, boolean wantsTrailingNewline) {
emptyDocument = (getLength() == 0);
isStyleCSS = "text/css".equals(getDefaultStyleSheetType());
this.offset = offset;
threshold = HTMLDocument.this.getTokenThreshold();
tagMap = new Hashtable(57);
TagAction na = new TagAction();
TagAction ba = new BlockAction();
TagAction pa = new ParagraphAction();
TagAction ca = new CharacterAction();
TagAction sa = new SpecialAction();
TagAction fa = new FormAction();
TagAction ha = new HiddenAction();
TagAction conv = new ConvertAction();
// register handlers for the well known tags
tagMap.put(HTML.Tag.A, new AnchorAction());
tagMap.put(HTML.Tag.ADDRESS, ca);
tagMap.put(HTML.Tag.APPLET, ha);
tagMap.put(HTML.Tag.AREA, new AreaAction());
tagMap.put(HTML.Tag.B, conv);
tagMap.put(HTML.Tag.BASE, new BaseAction());
tagMap.put(HTML.Tag.BASEFONT, ca);
tagMap.put(HTML.Tag.BIG, ca);
tagMap.put(HTML.Tag.BLOCKQUOTE, ba);
tagMap.put(HTML.Tag.BODY, ba);
tagMap.put(HTML.Tag.BR, sa);
tagMap.put(HTML.Tag.CAPTION, ba);
tagMap.put(HTML.Tag.CENTER, ba);
tagMap.put(HTML.Tag.CITE, ca);
tagMap.put(HTML.Tag.CODE, ca);
tagMap.put(HTML.Tag.DD, ba);
tagMap.put(HTML.Tag.DFN, ca);
tagMap.put(HTML.Tag.DIR, ba);
tagMap.put(HTML.Tag.DIV, ba);
tagMap.put(HTML.Tag.DL, ba);
tagMap.put(HTML.Tag.DT, pa);
tagMap.put(HTML.Tag.EM, ca);
tagMap.put(HTML.Tag.FONT, conv);
tagMap.put(HTML.Tag.FORM, new FormTagAction());
tagMap.put(HTML.Tag.FRAME, sa);
tagMap.put(HTML.Tag.FRAMESET, ba);
tagMap.put(HTML.Tag.H1, pa);
tagMap.put(HTML.Tag.H2, pa);
tagMap.put(HTML.Tag.H3, pa);
tagMap.put(HTML.Tag.H4, pa);
tagMap.put(HTML.Tag.H5, pa);
tagMap.put(HTML.Tag.H6, pa);
tagMap.put(HTML.Tag.HEAD, new HeadAction());
tagMap.put(HTML.Tag.HR, sa);
tagMap.put(HTML.Tag.HTML, ba);
tagMap.put(HTML.Tag.I, conv);
tagMap.put(HTML.Tag.IMG, sa);
tagMap.put(HTML.Tag.INPUT, fa);
tagMap.put(HTML.Tag.ISINDEX, new IsindexAction());
tagMap.put(HTML.Tag.KBD, ca);
tagMap.put(HTML.Tag.LI, ba);
tagMap.put(HTML.Tag.LINK, new LinkAction());
tagMap.put(HTML.Tag.MAP, new MapAction());
tagMap.put(HTML.Tag.MENU, ba);
tagMap.put(HTML.Tag.META, new MetaAction());
tagMap.put(HTML.Tag.NOBR, ca);
tagMap.put(HTML.Tag.NOFRAMES, ba);
tagMap.put(HTML.Tag.OBJECT, sa);
tagMap.put(HTML.Tag.OL, ba);
tagMap.put(HTML.Tag.OPTION, fa);
tagMap.put(HTML.Tag.P, pa);
tagMap.put(HTML.Tag.PARAM, new ObjectAction());
tagMap.put(HTML.Tag.PRE, new PreAction());
tagMap.put(HTML.Tag.SAMP, ca);
tagMap.put(HTML.Tag.SCRIPT, ha);
tagMap.put(HTML.Tag.SELECT, fa);
tagMap.put(HTML.Tag.SMALL, ca);
tagMap.put(HTML.Tag.SPAN, ca);
tagMap.put(HTML.Tag.STRIKE, conv);
tagMap.put(HTML.Tag.S, ca);
tagMap.put(HTML.Tag.STRONG, ca);
tagMap.put(HTML.Tag.STYLE, new StyleAction());
tagMap.put(HTML.Tag.SUB, conv);
tagMap.put(HTML.Tag.SUP, conv);
tagMap.put(HTML.Tag.TABLE, ba);
tagMap.put(HTML.Tag.TD, ba);
tagMap.put(HTML.Tag.TEXTAREA, fa);
tagMap.put(HTML.Tag.TH, ba);
tagMap.put(HTML.Tag.TITLE, new TitleAction());
tagMap.put(HTML.Tag.TR, ba);
tagMap.put(HTML.Tag.TT, ca);
tagMap.put(HTML.Tag.U, conv);
tagMap.put(HTML.Tag.UL, ba);
tagMap.put(HTML.Tag.VAR, ca);
if (insertTag != null) {
this.insertTag = insertTag;
this.popDepth = popDepth;
this.pushDepth = pushDepth;
this.insertInsertTag = insertInsertTag;
foundInsertTag = false;
}
else {
foundInsertTag = true;
}
if (insertAfterImplied) {
this.popDepth = popDepth;
this.pushDepth = pushDepth;
this.insertAfterImplied = true;
foundInsertTag = false;
midInsert = false;
this.insertInsertTag = true;
this.wantsTrailingNewline = wantsTrailingNewline;
}
else {
midInsert = (!emptyDocument && insertTag == null);
if (midInsert) {
generateEndsSpecsForMidInsert();
}
}
/**
* This block initializes the <code>inParagraph</code> flag.
* It is left in <code>false</code> value automatically
* if the target document is empty or future inserts
* were positioned into the 'body' tag.
*/
if (!emptyDocument && !midInsert) {
int targetOffset = Math.max(this.offset - 1, 0);
Element elem =
HTMLDocument.this.getCharacterElement(targetOffset);
/* Going up by the left document structure path */
for (int i = 0; i <= this.popDepth; i++) {
elem = elem.getParentElement();
}
/* Going down by the right document structure path */
for (int i = 0; i < this.pushDepth; i++) {
int index = elem.getElementIndex(this.offset);
elem = elem.getElement(index);
}
AttributeSet attrs = elem.getAttributes();
if (attrs != null) {
HTML.Tag tagToInsertInto =
(HTML.Tag) attrs.getAttribute(StyleConstants.NameAttribute);
if (tagToInsertInto != null) {
this.inParagraph = tagToInsertInto.isParagraph();
}
}
}
}
/**
* Generates an initial batch of end <code>ElementSpecs</code>
* in parseBuffer to position future inserts into the body.
*/
private void generateEndsSpecsForMidInsert() {
int count = heightToElementWithName(HTML.Tag.BODY,
Math.max(0, offset - 1));
boolean joinNext = false;
if (count == -1 && offset > 0) {
count = heightToElementWithName(HTML.Tag.BODY, offset);
if (count != -1) {
// Previous isn't in body, but current is. Have to
// do some end specs, followed by join next.
count = depthTo(offset - 1) - 1;
joinNext = true;
}
}
if (count == -1) {
throw new RuntimeException("Must insert new content into body element-");
}
if (count != -1) {
// Insert a newline, if necessary.
try {
if (!joinNext && offset > 0 &&
!getText(offset - 1, 1).equals("\n")) {
SimpleAttributeSet newAttrs = new SimpleAttributeSet();
newAttrs.addAttribute(StyleConstants.NameAttribute,
HTML.Tag.CONTENT);
ElementSpec spec = new ElementSpec(newAttrs,
ElementSpec.ContentType, NEWLINE, 0, 1);
parseBuffer.addElement(spec);
}
// Should never throw, but will catch anyway.
} catch (BadLocationException ble) {}
while (count-- > 0) {
parseBuffer.addElement(new ElementSpec
(null, ElementSpec.EndTagType));
}
if (joinNext) {
ElementSpec spec = new ElementSpec(null, ElementSpec.
StartTagType);
spec.setDirection(ElementSpec.JoinNextDirection);
parseBuffer.addElement(spec);
}
}
// We should probably throw an exception if (count == -1)
// Or look for the body and reset the offset.
}
/**
* @return number of parents to reach the child at offset.
*/
private int depthTo(int offset) {
Element e = getDefaultRootElement();
int count = 0;
while (!e.isLeaf()) {
count++;
e = e.getElement(e.getElementIndex(offset));
}
return count;
}
/**
* @return number of parents of the leaf at <code>offset</code>
* until a parent with name, <code>name</code> has been
* found. -1 indicates no matching parent with
* <code>name</code>.
*/
private int heightToElementWithName(Object name, int offset) {
Element e = getCharacterElement(offset).getParentElement();
int count = 0;
while (e != null && e.getAttributes().getAttribute
(StyleConstants.NameAttribute) != name) {
count++;
e = e.getParentElement();
}
return (e == null) ? -1 : count;
}
/**
* This will make sure there aren't two BODYs (the second is
* typically created when you do a remove all, and then an insert).
*/
private void adjustEndElement() {
int length = getLength();
if (length == 0) {
return;
}
obtainLock();
try {
Element[] pPath = getPathTo(length - 1);
int pLength = pPath.length;
if (pLength > 1 && pPath[1].getAttributes().getAttribute
(StyleConstants.NameAttribute) == HTML.Tag.BODY &&
pPath[1].getEndOffset() == length) {
String lastText = getText(length - 1, 1);
DefaultDocumentEvent event = null;
Element[] added;
Element[] removed;
int index;
// Remove the fake second body.
added = new Element[0];
removed = new Element[1];
index = pPath[0].getElementIndex(length);
removed[0] = pPath[0].getElement(index);
((BranchElement)pPath[0]).replace(index, 1, added);
ElementEdit firstEdit = new ElementEdit(pPath[0], index,
removed, added);
// Insert a new element to represent the end that the
// second body was representing.
SimpleAttributeSet sas = new SimpleAttributeSet();
sas.addAttribute(StyleConstants.NameAttribute,
HTML.Tag.CONTENT);
sas.addAttribute(IMPLIED_CR, Boolean.TRUE);
added = new Element[1];
added[0] = createLeafElement(pPath[pLength - 1],
sas, length, length + 1);
index = pPath[pLength - 1].getElementCount();
((BranchElement)pPath[pLength - 1]).replace(index, 0,
added);
event = new DefaultDocumentEvent(length, 1,
DocumentEvent.EventType.CHANGE);
event.addEdit(new ElementEdit(pPath[pLength - 1],
index, new Element[0], added));
event.addEdit(firstEdit);
event.end();
fireChangedUpdate(event);
fireUndoableEditUpdate(new UndoableEditEvent(this, event));
if (lastText.equals("\n")) {
// We now have two \n's, one part of the Document.
// We need to remove one
event = new DefaultDocumentEvent(length - 1, 1,
DocumentEvent.EventType.REMOVE);
removeUpdate(event);
UndoableEdit u = getContent().remove(length - 1, 1);
if (u != null) {
event.addEdit(u);
}
postRemoveUpdate(event);
// Mark the edit as done.
event.end();
fireRemoveUpdate(event);
fireUndoableEditUpdate(new UndoableEditEvent(
this, event));
}
}
}
catch (BadLocationException ble) {
}
finally {
releaseLock();
}
}
private Element[] getPathTo(int offset) {
Stack elements = new Stack();
Element e = getDefaultRootElement();
int index;
while (!e.isLeaf()) {
elements.push(e);
e = e.getElement(e.getElementIndex(offset));
}
Element[] retValue = new Element[elements.size()];
elements.copyInto(retValue);
return retValue;
}
// -- HTMLEditorKit.ParserCallback methods --------------------
/**
* The last method called on the reader. It allows
* any pending changes to be flushed into the document.
* Since this is currently loading synchronously, the entire
* set of changes are pushed in at this point.
*/
public void flush() throws BadLocationException {
if (emptyDocument && !insertAfterImplied) {
if (HTMLDocument.this.getLength() > 0 ||
parseBuffer.size() > 0) {
flushBuffer(true);
adjustEndElement();
}
// We won't insert when
}
else {
flushBuffer(true);
}
}
/**
* Called by the parser to indicate a block of text was
* encountered.
*/
public void handleText(char[] data, int pos) {
if (receivedEndHTML || (midInsert && !inBody)) {
return;
}
// see if complex glyph layout support is needed
if(HTMLDocument.this.getProperty(I18NProperty).equals( Boolean.FALSE ) ) {
// if a default direction of right-to-left has been specified,
// we want complex layout even if the text is all left to right.
Object d = getProperty(TextAttribute.RUN_DIRECTION);
if ((d != null) && (d.equals(TextAttribute.RUN_DIRECTION_RTL))) {
HTMLDocument.this.putProperty( I18NProperty, Boolean.TRUE);
} else {
if (SwingUtilities2.isComplexLayout(data, 0, data.length)) {
HTMLDocument.this.putProperty( I18NProperty, Boolean.TRUE);
}
}
}
if (inTextArea) {
textAreaContent(data);
} else if (inPre) {
preContent(data);
} else if (inTitle) {
putProperty(Document.TitleProperty, new String(data));
} else if (option != null) {
option.setLabel(new String(data));
} else if (inStyle) {
if (styles != null) {
styles.addElement(new String(data));
}
} else if (inBlock > 0) {
if (!foundInsertTag && insertAfterImplied) {
// Assume content should be added.
foundInsertTag(false);
foundInsertTag = true;
inParagraph = impliedP = true;
}
if (data.length >= 1) {
addContent(data, 0, data.length);
}
}
}
/**
* Callback from the parser. Route to the appropriate
* handler for the tag.
*/
public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
if (receivedEndHTML) {
return;
}
if (midInsert && !inBody) {
if (t == HTML.Tag.BODY) {
inBody = true;
// Increment inBlock since we know we are in the body,
// this is needed incase an implied-p is needed. If
// inBlock isn't incremented, and an implied-p is
// encountered, addContent won't be called!
inBlock++;
}
return;
}
if (!inBody && t == HTML.Tag.BODY) {
inBody = true;
}
if (isStyleCSS && a.isDefined(HTML.Attribute.STYLE)) {
// Map the style attributes.
String decl = (String)a.getAttribute(HTML.Attribute.STYLE);
a.removeAttribute(HTML.Attribute.STYLE);
styleAttributes = getStyleSheet().getDeclaration(decl);
a.addAttributes(styleAttributes);
}
else {
styleAttributes = null;
}
TagAction action = (TagAction) tagMap.get(t);
if (action != null) {
action.start(t, a);
}
}
public void handleComment(char[] data, int pos) {
if (receivedEndHTML) {
addExternalComment(new String(data));
return;
}
if (inStyle) {
if (styles != null) {
styles.addElement(new String(data));
}
}
else if (getPreservesUnknownTags()) {
if (inBlock == 0 && (foundInsertTag ||
insertTag != HTML.Tag.COMMENT)) {
// Comment outside of body, will not be able to show it,
// but can add it as a property on the Document.
addExternalComment(new String(data));
return;
}
SimpleAttributeSet sas = new SimpleAttributeSet();
sas.addAttribute(HTML.Attribute.COMMENT, new String(data));
addSpecialElement(HTML.Tag.COMMENT, sas);
}
TagAction action = (TagAction)tagMap.get(HTML.Tag.COMMENT);
if (action != null) {
action.start(HTML.Tag.COMMENT, new SimpleAttributeSet());
action.end(HTML.Tag.COMMENT);
}
}
/**
* Adds the comment <code>comment</code> to the set of comments
* maintained outside of the scope of elements.
*/
private void addExternalComment(String comment) {
Object comments = getProperty(AdditionalComments);
if (comments != null && !(comments instanceof Vector)) {
// No place to put comment.
return;
}
if (comments == null) {
comments = new Vector();
putProperty(AdditionalComments, comments);
}
((Vector)comments).addElement(comment);
}
/**
* Callback from the parser. Route to the appropriate
* handler for the tag.
*/
public void handleEndTag(HTML.Tag t, int pos) {
if (receivedEndHTML || (midInsert && !inBody)) {
return;
}
if (t == HTML.Tag.HTML) {
receivedEndHTML = true;
}
if (t == HTML.Tag.BODY) {
inBody = false;
if (midInsert) {
inBlock--;
}
}
TagAction action = (TagAction) tagMap.get(t);
if (action != null) {
action.end(t);
}
}
/**
* Callback from the parser. Route to the appropriate
* handler for the tag.
*/
public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
if (receivedEndHTML || (midInsert && !inBody)) {
return;
}
if (isStyleCSS && a.isDefined(HTML.Attribute.STYLE)) {
// Map the style attributes.
String decl = (String)a.getAttribute(HTML.Attribute.STYLE);
a.removeAttribute(HTML.Attribute.STYLE);
styleAttributes = getStyleSheet().getDeclaration(decl);
a.addAttributes(styleAttributes);
}
else {
styleAttributes = null;
}
TagAction action = (TagAction) tagMap.get(t);
if (action != null) {
action.start(t, a);
action.end(t);
}
else if (getPreservesUnknownTags()) {
// unknown tag, only add if should preserve it.
addSpecialElement(t, a);
}
}
/**
* This is invoked after the stream has been parsed, but before
* <code>flush</code>. <code>eol</code> will be one of \n, \r
* or \r\n, which ever is encountered the most in parsing the
* stream.
*
* @since 1.3
*/
public void handleEndOfLineString(String eol) {
if (emptyDocument && eol != null) {
putProperty(DefaultEditorKit.EndOfLineStringProperty,
eol);
}
}
// ---- tag handling support ------------------------------
/**
* Registers a handler for the given tag. By default
* all of the well-known tags will have been registered.
* This can be used to change the handling of a particular
* tag or to add support for custom tags.
*/
protected void registerTag(HTML.Tag t, TagAction a) {
tagMap.put(t, a);
}
/**
* An action to be performed in response
* to parsing a tag. This allows customization
* of how each tag is handled and avoids a large
* switch statement.
*/
public class TagAction {
/**
* Called when a start tag is seen for the
* type of tag this action was registered
* to. The tag argument indicates the actual
* tag for those actions that are shared across
* many tags. By default this does nothing and
* completely ignores the tag.
*/
public void start(HTML.Tag t, MutableAttributeSet a) {
}
/**
* Called when an end tag is seen for the
* type of tag this action was registered
* to. The tag argument indicates the actual
* tag for those actions that are shared across
* many tags. By default this does nothing and
* completely ignores the tag.
*/
public void end(HTML.Tag t) {
}
}
public class BlockAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
blockOpen(t, attr);
}
public void end(HTML.Tag t) {
blockClose(t);
}
}
/**
* Action used for the actual element form tag. This is named such
* as there was already a public class named FormAction.
*/
private class FormTagAction extends BlockAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
super.start(t, attr);
// initialize a ButtonGroupsMap when
// FORM tag is encountered. This will
// be used for any radio buttons that
// might be defined in the FORM.
// for new group new ButtonGroup will be created (fix for 4529702)
// group name is a key in radioButtonGroupsMap
radioButtonGroupsMap = new HashMap();
}
public void end(HTML.Tag t) {
super.end(t);
// reset the button group to null since
// the form has ended.
radioButtonGroupsMap = null;
}
}
public class ParagraphAction extends BlockAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
super.start(t, a);
inParagraph = true;
}
public void end(HTML.Tag t) {
super.end(t);
inParagraph = false;
}
}
public class SpecialAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
addSpecialElement(t, a);
}
}
public class IsindexAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
blockOpen(HTML.Tag.IMPLIED, new SimpleAttributeSet());
addSpecialElement(t, a);
blockClose(HTML.Tag.IMPLIED);
}
}
public class HiddenAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
addSpecialElement(t, a);
}
public void end(HTML.Tag t) {
if (!isEmpty(t)) {
MutableAttributeSet a = new SimpleAttributeSet();
a.addAttribute(HTML.Attribute.ENDTAG, "true");
addSpecialElement(t, a);
}
}
boolean isEmpty(HTML.Tag t) {
if (t == HTML.Tag.APPLET ||
t == HTML.Tag.SCRIPT) {
return false;
}
return true;
}
}
/**
* Subclass of HiddenAction to set the content type for style sheets,
* and to set the name of the default style sheet.
*/
class MetaAction extends HiddenAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
Object equiv = a.getAttribute(HTML.Attribute.HTTPEQUIV);
if (equiv != null) {
equiv = ((String)equiv).toLowerCase();
if (equiv.equals("content-style-type")) {
String value = (String)a.getAttribute
(HTML.Attribute.CONTENT);
setDefaultStyleSheetType(value);
isStyleCSS = "text/css".equals
(getDefaultStyleSheetType());
}
else if (equiv.equals("default-style")) {
defaultStyle = (String)a.getAttribute
(HTML.Attribute.CONTENT);
}
}
super.start(t, a);
}
boolean isEmpty(HTML.Tag t) {
return true;
}
}
/**
* End if overridden to create the necessary stylesheets that
* are referenced via the link tag. It is done in this manner
* as the meta tag can be used to specify an alternate style sheet,
* and is not guaranteed to come before the link tags.
*/
class HeadAction extends BlockAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
inHead = true;
// This check of the insertTag is put in to avoid considering
// the implied-p that is generated for the head. This allows
// inserts for HR to work correctly.
if ((insertTag == null && !insertAfterImplied) ||
(insertTag == HTML.Tag.HEAD) ||
(insertAfterImplied &&
(foundInsertTag || !a.isDefined(IMPLIED)))) {
super.start(t, a);
}
}
public void end(HTML.Tag t) {
inHead = inStyle = false;
// See if there is a StyleSheet to link to.
if (styles != null) {
boolean isDefaultCSS = isStyleCSS;
for (int counter = 0, maxCounter = styles.size();
counter < maxCounter;) {
Object value = styles.elementAt(counter);
if (value == HTML.Tag.LINK) {
handleLink((AttributeSet)styles.
elementAt(++counter));
counter++;
}
else {
// Rule.
// First element gives type.
String type = (String)styles.elementAt(++counter);
boolean isCSS = (type == null) ? isDefaultCSS :
type.equals("text/css");
while (++counter < maxCounter &&
(styles.elementAt(counter)
instanceof String)) {
if (isCSS) {
addCSSRules((String)styles.elementAt
(counter));
}
}
}
}
}
if ((insertTag == null && !insertAfterImplied) ||
insertTag == HTML.Tag.HEAD ||
(insertAfterImplied && foundInsertTag)) {
super.end(t);
}
}
boolean isEmpty(HTML.Tag t) {
return false;
}
private void handleLink(AttributeSet attr) {
// Link.
String type = (String)attr.getAttribute(HTML.Attribute.TYPE);
if (type == null) {
type = getDefaultStyleSheetType();
}
// Only choose if type==text/css
// Select link if rel==stylesheet.
// Otherwise if rel==alternate stylesheet and
// title matches default style.
if (type.equals("text/css")) {
String rel = (String)attr.getAttribute(HTML.Attribute.REL);
String title = (String)attr.getAttribute
(HTML.Attribute.TITLE);
String media = (String)attr.getAttribute
(HTML.Attribute.MEDIA);
if (media == null) {
media = "all";
}
else {
media = media.toLowerCase();
}
if (rel != null) {
rel = rel.toLowerCase();
if ((media.indexOf("all") != -1 ||
media.indexOf("screen") != -1) &&
(rel.equals("stylesheet") ||
(rel.equals("alternate stylesheet") &&
title.equals(defaultStyle)))) {
linkCSSStyleSheet((String)attr.getAttribute
(HTML.Attribute.HREF));
}
}
}
}
}
/**
* A subclass to add the AttributeSet to styles if the
* attributes contains an attribute for 'rel' with value
* 'stylesheet' or 'alternate stylesheet'.
*/
class LinkAction extends HiddenAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
String rel = (String)a.getAttribute(HTML.Attribute.REL);
if (rel != null) {
rel = rel.toLowerCase();
if (rel.equals("stylesheet") ||
rel.equals("alternate stylesheet")) {
if (styles == null) {
styles = new Vector(3);
}
styles.addElement(t);
styles.addElement(a.copyAttributes());
}
}
super.start(t, a);
}
}
class MapAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
lastMap = new Map((String)a.getAttribute(HTML.Attribute.NAME));
addMap(lastMap);
}
public void end(HTML.Tag t) {
}
}
class AreaAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
if (lastMap != null) {
lastMap.addArea(a.copyAttributes());
}
}
public void end(HTML.Tag t) {
}
}
class StyleAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
if (inHead) {
if (styles == null) {
styles = new Vector(3);
}
styles.addElement(t);
styles.addElement(a.getAttribute(HTML.Attribute.TYPE));
inStyle = true;
}
}
public void end(HTML.Tag t) {
inStyle = false;
}
boolean isEmpty(HTML.Tag t) {
return false;
}
}
public class PreAction extends BlockAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
inPre = true;
blockOpen(t, attr);
attr.addAttribute(CSS.Attribute.WHITE_SPACE, "pre");
blockOpen(HTML.Tag.IMPLIED, attr);
}
public void end(HTML.Tag t) {
blockClose(HTML.Tag.IMPLIED);
// set inPre to false after closing, so that if a newline
// is added it won't generate a blockOpen.
inPre = false;
blockClose(t);
}
}
public class CharacterAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
pushCharacterStyle();
if (!foundInsertTag) {
// Note that the third argument should really be based off
// inParagraph and impliedP. If we're wrong (that is
// insertTagDepthDelta shouldn't be changed), we'll end up
// removing an extra EndSpec, which won't matter anyway.
boolean insert = canInsertTag(t, attr, false);
if (foundInsertTag) {
if (!inParagraph) {
inParagraph = impliedP = true;
}
}
if (!insert) {
return;
}
}
if (attr.isDefined(IMPLIED)) {
attr.removeAttribute(IMPLIED);
}
charAttr.addAttribute(t, attr.copyAttributes());
if (styleAttributes != null) {
charAttr.addAttributes(styleAttributes);
}
}
public void end(HTML.Tag t) {
popCharacterStyle();
}
}
/**
* Provides conversion of HTML tag/attribute
* mappings that have a corresponding StyleConstants
* and CSS mapping. The conversion is to CSS attributes.
*/
class ConvertAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
pushCharacterStyle();
if (!foundInsertTag) {
// Note that the third argument should really be based off
// inParagraph and impliedP. If we're wrong (that is
// insertTagDepthDelta shouldn't be changed), we'll end up
// removing an extra EndSpec, which won't matter anyway.
boolean insert = canInsertTag(t, attr, false);
if (foundInsertTag) {
if (!inParagraph) {
inParagraph = impliedP = true;
}
}
if (!insert) {
return;
}
}
if (attr.isDefined(IMPLIED)) {
attr.removeAttribute(IMPLIED);
}
if (styleAttributes != null) {
charAttr.addAttributes(styleAttributes);
}
// We also need to add attr, otherwise we lose custom
// attributes, including class/id for style lookups, and
// further confuse style lookup (doesn't have tag).
charAttr.addAttribute(t, attr.copyAttributes());
StyleSheet sheet = getStyleSheet();
if (t == HTML.Tag.B) {
sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_WEIGHT, "bold");
} else if (t == HTML.Tag.I) {
sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_STYLE, "italic");
} else if (t == HTML.Tag.U) {
Object v = charAttr.getAttribute(CSS.Attribute.TEXT_DECORATION);
String value = "underline";
value = (v != null) ? value + "," + v.toString() : value;
sheet.addCSSAttribute(charAttr, CSS.Attribute.TEXT_DECORATION, value);
} else if (t == HTML.Tag.STRIKE) {
Object v = charAttr.getAttribute(CSS.Attribute.TEXT_DECORATION);
String value = "line-through";
value = (v != null) ? value + "," + v.toString() : value;
sheet.addCSSAttribute(charAttr, CSS.Attribute.TEXT_DECORATION, value);
} else if (t == HTML.Tag.SUP) {
Object v = charAttr.getAttribute(CSS.Attribute.VERTICAL_ALIGN);
String value = "sup";
value = (v != null) ? value + "," + v.toString() : value;
sheet.addCSSAttribute(charAttr, CSS.Attribute.VERTICAL_ALIGN, value);
} else if (t == HTML.Tag.SUB) {
Object v = charAttr.getAttribute(CSS.Attribute.VERTICAL_ALIGN);
String value = "sub";
value = (v != null) ? value + "," + v.toString() : value;
sheet.addCSSAttribute(charAttr, CSS.Attribute.VERTICAL_ALIGN, value);
} else if (t == HTML.Tag.FONT) {
String color = (String) attr.getAttribute(HTML.Attribute.COLOR);
if (color != null) {
sheet.addCSSAttribute(charAttr, CSS.Attribute.COLOR, color);
}
String face = (String) attr.getAttribute(HTML.Attribute.FACE);
if (face != null) {
sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_FAMILY, face);
}
String size = (String) attr.getAttribute(HTML.Attribute.SIZE);
if (size != null) {
sheet.addCSSAttributeFromHTML(charAttr, CSS.Attribute.FONT_SIZE, size);
}
}
}
public void end(HTML.Tag t) {
popCharacterStyle();
}
}
class AnchorAction extends CharacterAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
// set flag to catch empty anchors
emptyAnchor = true;
super.start(t, attr);
}
public void end(HTML.Tag t) {
if (emptyAnchor) {
// if the anchor was empty it was probably a
// named anchor point and we don't want to throw
// it away.
char[] one = new char[1];
one[0] = '\n';
addContent(one, 0, 1);
}
super.end(t);
}
}
class TitleAction extends HiddenAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
inTitle = true;
super.start(t, attr);
}
public void end(HTML.Tag t) {
inTitle = false;
super.end(t);
}
boolean isEmpty(HTML.Tag t) {
return false;
}
}
class BaseAction extends TagAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
String href = (String) attr.getAttribute(HTML.Attribute.HREF);
if (href != null) {
try {
URL newBase = new URL(base, href);
setBase(newBase);
hasBaseTag = true;
} catch (MalformedURLException ex) {
}
}
baseTarget = (String) attr.getAttribute(HTML.Attribute.TARGET);
}
}
class ObjectAction extends SpecialAction {
public void start(HTML.Tag t, MutableAttributeSet a) {
if (t == HTML.Tag.PARAM) {
addParameter(a);
} else {
super.start(t, a);
}
}
public void end(HTML.Tag t) {
if (t != HTML.Tag.PARAM) {
super.end(t);
}
}
void addParameter(AttributeSet a) {
String name = (String) a.getAttribute(HTML.Attribute.NAME);
String value = (String) a.getAttribute(HTML.Attribute.VALUE);
if ((name != null) && (value != null)) {
ElementSpec objSpec = (ElementSpec) parseBuffer.lastElement();
MutableAttributeSet objAttr = (MutableAttributeSet) objSpec.getAttributes();
objAttr.addAttribute(name, value);
}
}
}
/**
* Action to support forms by building all of the elements
* used to represent form controls. This will process
* the <INPUT>, <TEXTAREA>, <SELECT>,
* and <OPTION> tags. The element created by
* this action is expected to have the attribute
* <code>StyleConstants.ModelAttribute</code> set to
* the model that holds the state for the form control.
* This enables multiple views, and allows document to
* be iterated over picking up the data of the form.
* The following are the model assignments for the
* various type of form elements.
* <table summary="model assignments for the various types of form elements">
* <tr>
* <th>Element Type
* <th>Model Type
* <tr>
* <td>input, type button
* <td>{@link DefaultButtonModel}
* <tr>
* <td>input, type checkbox
* <td>{@link javax.swing.JToggleButton.ToggleButtonModel}
* <tr>
* <td>input, type image
* <td>{@link DefaultButtonModel}
* <tr>
* <td>input, type password
* <td>{@link PlainDocument}
* <tr>
* <td>input, type radio
* <td>{@link javax.swing.JToggleButton.ToggleButtonModel}
* <tr>
* <td>input, type reset
* <td>{@link DefaultButtonModel}
* <tr>
* <td>input, type submit
* <td>{@link DefaultButtonModel}
* <tr>
* <td>input, type text or type is null.
* <td>{@link PlainDocument}
* <tr>
* <td>select
* <td>{@link DefaultComboBoxModel} or an {@link DefaultListModel}, with an item type of Option
* <tr>
* <td>textarea
* <td>{@link PlainDocument}
* </table>
*
*/
public class FormAction extends SpecialAction {
public void start(HTML.Tag t, MutableAttributeSet attr) {
if (t == HTML.Tag.INPUT) {
String type = (String)
attr.getAttribute(HTML.Attribute.TYPE);
/*
* if type is not defined teh default is
* assumed to be text.
*/
if (type == null) {
type = "text";
attr.addAttribute(HTML.Attribute.TYPE, "text");
}
setModel(type, attr);
} else if (t == HTML.Tag.TEXTAREA) {
inTextArea = true;
textAreaDocument = new TextAreaDocument();
attr.addAttribute(StyleConstants.ModelAttribute,
textAreaDocument);
} else if (t == HTML.Tag.SELECT) {
int size = HTML.getIntegerAttributeValue(attr,
HTML.Attribute.SIZE,
1);
boolean multiple = ((String)attr.getAttribute(HTML.Attribute.MULTIPLE) != null);
if ((size > 1) || multiple) {
OptionListModel m = new OptionListModel();
if (multiple) {
m.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
}
selectModel = m;
} else {
selectModel = new OptionComboBoxModel();
}
attr.addAttribute(StyleConstants.ModelAttribute,
selectModel);
}
// build the element, unless this is an option.
if (t == HTML.Tag.OPTION) {
option = new Option(attr);
if (selectModel instanceof OptionListModel) {
OptionListModel m = (OptionListModel)selectModel;
m.addElement(option);
if (option.isSelected()) {
m.addSelectionInterval(optionCount, optionCount);
m.setInitialSelection(optionCount);
}
} else if (selectModel instanceof OptionComboBoxModel) {
OptionComboBoxModel m = (OptionComboBoxModel)selectModel;
m.addElement(option);
if (option.isSelected()) {
m.setSelectedItem(option);
m.setInitialSelection(option);
}
}
optionCount++;
} else {
super.start(t, attr);
}
}
public void end(HTML.Tag t) {
if (t == HTML.Tag.OPTION) {
option = null;
} else {
if (t == HTML.Tag.SELECT) {
selectModel = null;
optionCount = 0;
} else if (t == HTML.Tag.TEXTAREA) {
inTextArea = false;
/* Now that the textarea has ended,
* store the entire initial text
* of the text area. This will
* enable us to restore the initial
* state if a reset is requested.
*/
textAreaDocument.storeInitialText();
}
super.end(t);
}
}
void setModel(String type, MutableAttributeSet attr) {
if (type.equals("submit") ||
type.equals("reset") ||
type.equals("image")) {
// button model
attr.addAttribute(StyleConstants.ModelAttribute,
new DefaultButtonModel());
} else if (type.equals("text") ||
type.equals("password")) {
// plain text model
int maxLength = HTML.getIntegerAttributeValue(
attr, HTML.Attribute.MAXLENGTH, -1);
Document doc;
if (maxLength > 0) {
doc = new FixedLengthDocument(maxLength);
}
else {
doc = new PlainDocument();
}
String value = (String)
attr.getAttribute(HTML.Attribute.VALUE);
try {
doc.insertString(0, value, null);
} catch (BadLocationException e) {
}
attr.addAttribute(StyleConstants.ModelAttribute, doc);
} else if (type.equals("file")) {
// plain text model
attr.addAttribute(StyleConstants.ModelAttribute,
new PlainDocument());
} else if (type.equals("checkbox") ||
type.equals("radio")) {
JToggleButton.ToggleButtonModel model = new JToggleButton.ToggleButtonModel();
if (type.equals("radio")) {
String name = (String) attr.getAttribute(HTML.Attribute.NAME);
if ( radioButtonGroupsMap == null ) { //fix for 4772743
radioButtonGroupsMap = new HashMap();
}
ButtonGroup radioButtonGroup = (ButtonGroup)radioButtonGroupsMap.get(name);
if (radioButtonGroup == null) {
radioButtonGroup = new ButtonGroup();
radioButtonGroupsMap.put(name,radioButtonGroup);
}
model.setGroup(radioButtonGroup);
}
boolean checked = (attr.getAttribute(HTML.Attribute.CHECKED) != null);
model.setSelected(checked);
attr.addAttribute(StyleConstants.ModelAttribute, model);
}
}
/**
* If a <SELECT> tag is being processed, this
* model will be a reference to the model being filled
* with the <OPTION> elements (which produce
* objects of type <code>Option</code>.
*/
Object selectModel;
int optionCount;
}
// --- utility methods used by the reader ------------------
/**
* Pushes the current character style on a stack in preparation
* for forming a new nested character style.
*/
protected void pushCharacterStyle() {
charAttrStack.push(charAttr.copyAttributes());
}
/**
* Pops a previously pushed character style off the stack
* to return to a previous style.
*/
protected void popCharacterStyle() {
if (!charAttrStack.empty()) {
charAttr = (MutableAttributeSet) charAttrStack.peek();
charAttrStack.pop();
}
}
/**
* Adds the given content to the textarea document.
* This method gets called when we are in a textarea
* context. Therefore all text that is seen belongs
* to the text area and is hence added to the
* TextAreaDocument associated with the text area.
*/
protected void textAreaContent(char[] data) {
try {
textAreaDocument.insertString(textAreaDocument.getLength(), new String(data), null);
} catch (BadLocationException e) {
// Should do something reasonable
}
}
/**
* Adds the given content that was encountered in a
* PRE element. This synthesizes lines to hold the
* runs of text, and makes calls to addContent to
* actually add the text.
*/
protected void preContent(char[] data) {
int last = 0;
for (int i = 0; i < data.length; i++) {
if (data[i] == '\n') {
addContent(data, last, i - last + 1);
blockClose(HTML.Tag.IMPLIED);
MutableAttributeSet a = new SimpleAttributeSet();
a.addAttribute(CSS.Attribute.WHITE_SPACE, "pre");
blockOpen(HTML.Tag.IMPLIED, a);
last = i + 1;
}
}
if (last < data.length) {
addContent(data, last, data.length - last);
}
}
/**
* Adds an instruction to the parse buffer to create a
* block element with the given attributes.
*/
protected void blockOpen(HTML.Tag t, MutableAttributeSet attr) {
if (impliedP) {
blockClose(HTML.Tag.IMPLIED);
}
inBlock++;
if (!canInsertTag(t, attr, true)) {
return;
}
if (attr.isDefined(IMPLIED)) {
attr.removeAttribute(IMPLIED);
}
lastWasNewline = false;
attr.addAttribute(StyleConstants.NameAttribute, t);
ElementSpec es = new ElementSpec(
attr.copyAttributes(), ElementSpec.StartTagType);
parseBuffer.addElement(es);
}
/**
* Adds an instruction to the parse buffer to close out
* a block element of the given type.
*/
protected void blockClose(HTML.Tag t) {
inBlock--;
if (!foundInsertTag) {
return;
}
// Add a new line, if the last character wasn't one. This is
// needed for proper positioning of the cursor. addContent
// with true will force an implied paragraph to be generated if
// there isn't one. This may result in a rather bogus structure
// (perhaps a table with a child pargraph), but the paragraph
// is needed for proper positioning and display.
if(!lastWasNewline) {
pushCharacterStyle();
charAttr.addAttribute(IMPLIED_CR, Boolean.TRUE);
addContent(NEWLINE, 0, 1, true);
popCharacterStyle();
lastWasNewline = true;
}
if (impliedP) {
impliedP = false;
inParagraph = false;
if (t != HTML.Tag.IMPLIED) {
blockClose(HTML.Tag.IMPLIED);
}
}
// an open/close with no content will be removed, so we
// add a space of content to keep the element being formed.
ElementSpec prev = (parseBuffer.size() > 0) ?
(ElementSpec) parseBuffer.lastElement() : null;
if (prev != null && prev.getType() == ElementSpec.StartTagType) {
char[] one = new char[1];
one[0] = ' ';
addContent(one, 0, 1);
}
ElementSpec es = new ElementSpec(
null, ElementSpec.EndTagType);
parseBuffer.addElement(es);
}
/**
* Adds some text with the current character attributes.
*
* @param data the content to add
* @param offs the initial offset
* @param length the length
*/
protected void addContent(char[] data, int offs, int length) {
addContent(data, offs, length, true);
}
/**
* Adds some text with the current character attributes.
*
* @param data the content to add
* @param offs the initial offset
* @param length the length
* @param generateImpliedPIfNecessary whether to generate implied
* paragraphs
*/
protected void addContent(char[] data, int offs, int length,
boolean generateImpliedPIfNecessary) {
if (!foundInsertTag) {
return;
}
if (generateImpliedPIfNecessary && (! inParagraph) && (! inPre)) {
blockOpen(HTML.Tag.IMPLIED, new SimpleAttributeSet());
inParagraph = true;
impliedP = true;
}
emptyAnchor = false;
charAttr.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT);
AttributeSet a = charAttr.copyAttributes();
ElementSpec es = new ElementSpec(
a, ElementSpec.ContentType, data, offs, length);
parseBuffer.addElement(es);
if (parseBuffer.size() > threshold) {
if ( threshold <= MaxThreshold ) {
threshold *= StepThreshold;
}
try {
flushBuffer(false);
} catch (BadLocationException ble) {
}
}
if(length > 0) {
lastWasNewline = (data[offs + length - 1] == '\n');
}
}
/**
* Adds content that is basically specified entirely
* in the attribute set.
*/
protected void addSpecialElement(HTML.Tag t, MutableAttributeSet a) {
if ((t != HTML.Tag.FRAME) && (! inParagraph) && (! inPre)) {
nextTagAfterPImplied = t;
blockOpen(HTML.Tag.IMPLIED, new SimpleAttributeSet());
nextTagAfterPImplied = null;
inParagraph = true;
impliedP = true;
}
if (!canInsertTag(t, a, t.isBlock())) {
return;
}
if (a.isDefined(IMPLIED)) {
a.removeAttribute(IMPLIED);
}
emptyAnchor = false;
a.addAttributes(charAttr);
a.addAttribute(StyleConstants.NameAttribute, t);
char[] one = new char[1];
one[0] = ' ';
ElementSpec es = new ElementSpec(
a.copyAttributes(), ElementSpec.ContentType, one, 0, 1);
parseBuffer.addElement(es);
// Set this to avoid generating a newline for frames, frames
// shouldn't have any content, and shouldn't need a newline.
if (t == HTML.Tag.FRAME) {
lastWasNewline = true;
}
}
/**
* Flushes the current parse buffer into the document.
* @param endOfStream true if there is no more content to parser
*/
void flushBuffer(boolean endOfStream) throws BadLocationException {
int oldLength = HTMLDocument.this.getLength();
int size = parseBuffer.size();
if (endOfStream && (insertTag != null || insertAfterImplied) &&
size > 0) {
adjustEndSpecsForPartialInsert();
size = parseBuffer.size();
}
ElementSpec[] spec = new ElementSpec[size];
parseBuffer.copyInto(spec);
if (oldLength == 0 && (insertTag == null && !insertAfterImplied)) {
create(spec);
} else {
insert(offset, spec);
}
parseBuffer.removeAllElements();
offset += HTMLDocument.this.getLength() - oldLength;
flushCount++;
}
/**
* This will be invoked for the last flush, if <code>insertTag</code>
* is non null.
*/
private void adjustEndSpecsForPartialInsert() {
int size = parseBuffer.size();
if (insertTagDepthDelta < 0) {
// When inserting via an insertTag, the depths (of the tree
// being read in, and existing hiearchy) may not match up.
// This attemps to clean it up.
int removeCounter = insertTagDepthDelta;
while (removeCounter < 0 && size >= 0 &&
((ElementSpec)parseBuffer.elementAt(size - 1)).
getType() == ElementSpec.EndTagType) {
parseBuffer.removeElementAt(--size);
removeCounter++;
}
}
if (flushCount == 0 && (!insertAfterImplied ||
!wantsTrailingNewline)) {
// If this starts with content (or popDepth > 0 &&
// pushDepth > 0) and ends with EndTagTypes, make sure
// the last content isn't a \n, otherwise will end up with
// an extra \n in the middle of content.
int index = 0;
if (pushDepth > 0) {
if (((ElementSpec)parseBuffer.elementAt(0)).getType() ==
ElementSpec.ContentType) {
index++;
}
}
index += (popDepth + pushDepth);
int cCount = 0;
int cStart = index;
while (index < size && ((ElementSpec)parseBuffer.elementAt
(index)).getType() == ElementSpec.ContentType) {
index++;
cCount++;
}
if (cCount > 1) {
while (index < size && ((ElementSpec)parseBuffer.elementAt
(index)).getType() == ElementSpec.EndTagType) {
index++;
}
if (index == size) {
char[] lastText = ((ElementSpec)parseBuffer.elementAt
(cStart + cCount - 1)).getArray();
if (lastText.length == 1 && lastText[0] == NEWLINE[0]){
index = cStart + cCount - 1;
while (size > index) {
parseBuffer.removeElementAt(--size);
}
}
}
}
}
if (wantsTrailingNewline) {
// Make sure there is in fact a newline
for (int counter = parseBuffer.size() - 1; counter >= 0;
counter--) {
ElementSpec spec = (ElementSpec)parseBuffer.
elementAt(counter);
if (spec.getType() == ElementSpec.ContentType) {
if (spec.getArray()[spec.getLength() - 1] != '\n') {
SimpleAttributeSet attrs =new SimpleAttributeSet();
attrs.addAttribute(StyleConstants.NameAttribute,
HTML.Tag.CONTENT);
parseBuffer.insertElementAt(new ElementSpec(
attrs,
ElementSpec.ContentType, NEWLINE, 0, 1),
counter + 1);
}
break;
}
}
}
}
/**
* Adds the CSS rules in <code>rules</code>.
*/
void addCSSRules(String rules) {
StyleSheet ss = getStyleSheet();
ss.addRule(rules);
}
/**
* Adds the CSS stylesheet at <code>href</code> to the known list
* of stylesheets.
*/
void linkCSSStyleSheet(String href) {
URL url = null;
try {
url = new URL(base, href);
} catch (MalformedURLException mfe) {
try {
url = new URL(href);
} catch (MalformedURLException mfe2) {
url = null;
}
}
if (url != null) {
getStyleSheet().importStyleSheet(url);
}
}
/**
* Returns true if can insert starting at <code>t</code>. This
* will return false if the insert tag is set, and hasn't been found
* yet.
*/
private boolean canInsertTag(HTML.Tag t, AttributeSet attr,
boolean isBlockTag) {
if (!foundInsertTag) {
boolean needPImplied = ((t == HTML.Tag.IMPLIED)
&& (!inParagraph)
&& (!inPre));
if (needPImplied && (nextTagAfterPImplied != null)) {
/*
* If insertTag == null then just proceed to
* foundInsertTag() call below and return true.
*/
if (insertTag != null) {
boolean nextTagIsInsertTag =
isInsertTag(nextTagAfterPImplied);
if ( (! nextTagIsInsertTag) || (! insertInsertTag) ) {
return false;
}
}
/*
* Proceed to foundInsertTag() call...
*/
} else if ((insertTag != null && !isInsertTag(t))
|| (insertAfterImplied
&& (attr == null
|| attr.isDefined(IMPLIED)
|| t == HTML.Tag.IMPLIED
)
)
) {
return false;
}
// Allow the insert if t matches the insert tag, or
// insertAfterImplied is true and the element is implied.
foundInsertTag(isBlockTag);
if (!insertInsertTag) {
return false;
}
}
return true;
}
private boolean isInsertTag(HTML.Tag tag) {
return (insertTag == tag);
}
private void foundInsertTag(boolean isBlockTag) {
foundInsertTag = true;
if (!insertAfterImplied && (popDepth > 0 || pushDepth > 0)) {
try {
if (offset == 0 || !getText(offset - 1, 1).equals("\n")) {
// Need to insert a newline.
AttributeSet newAttrs = null;
boolean joinP = true;
if (offset != 0) {
// Determine if we can use JoinPrevious, we can't
// if the Element has some attributes that are
// not meant to be duplicated.
Element charElement = getCharacterElement
(offset - 1);
AttributeSet attrs = charElement.getAttributes();
if (attrs.isDefined(StyleConstants.
ComposedTextAttribute)) {
joinP = false;
}
else {
Object name = attrs.getAttribute
(StyleConstants.NameAttribute);
if (name instanceof HTML.Tag) {
HTML.Tag tag = (HTML.Tag)name;
if (tag == HTML.Tag.IMG ||
tag == HTML.Tag.HR ||
tag == HTML.Tag.COMMENT ||
(tag instanceof HTML.UnknownTag)) {
joinP = false;
}
}
}
}
if (!joinP) {
// If not joining with the previous element, be
// sure and set the name (otherwise it will be
// inherited).
newAttrs = new SimpleAttributeSet();
((SimpleAttributeSet)newAttrs).addAttribute
(StyleConstants.NameAttribute,
HTML.Tag.CONTENT);
}
ElementSpec es = new ElementSpec(newAttrs,
ElementSpec.ContentType, NEWLINE, 0,
NEWLINE.length);
if (joinP) {
es.setDirection(ElementSpec.
JoinPreviousDirection);
}
parseBuffer.addElement(es);
}
} catch (BadLocationException ble) {}
}
// pops
for (int counter = 0; counter < popDepth; counter++) {
parseBuffer.addElement(new ElementSpec(null, ElementSpec.
EndTagType));
}
// pushes
for (int counter = 0; counter < pushDepth; counter++) {
ElementSpec es = new ElementSpec(null, ElementSpec.
StartTagType);
es.setDirection(ElementSpec.JoinNextDirection);
parseBuffer.addElement(es);
}
insertTagDepthDelta = depthTo(Math.max(0, offset - 1)) -
popDepth + pushDepth - inBlock;
if (isBlockTag) {
// A start spec will be added (for this tag), so we account
// for it here.
insertTagDepthDelta++;
}
else {
// An implied paragraph close (end spec) is going to be added,
// so we account for it here.
insertTagDepthDelta--;
inParagraph = true;
lastWasNewline = false;
}
}
/**
* This is set to true when and end is invoked for <html>.
*/
private boolean receivedEndHTML;
/** Number of times <code>flushBuffer</code> has been invoked. */
private int flushCount;
/** If true, behavior is similiar to insertTag, but instead of
* waiting for insertTag will wait for first Element without
* an 'implied' attribute and begin inserting then. */
private boolean insertAfterImplied;
/** This is only used if insertAfterImplied is true. If false, only
* inserting content, and there is a trailing newline it is removed. */
private boolean wantsTrailingNewline;
int threshold;
int offset;
boolean inParagraph = false;
boolean impliedP = false;
boolean inPre = false;
boolean inTextArea = false;
TextAreaDocument textAreaDocument = null;
boolean inTitle = false;
boolean lastWasNewline = true;
boolean emptyAnchor;
/** True if (!emptyDocument && insertTag == null), this is used so
* much it is cached. */
boolean midInsert;
/** True when the body has been encountered. */
boolean inBody;
/** If non null, gives parent Tag that insert is to happen at. */
HTML.Tag insertTag;
/** If true, the insertTag is inserted, otherwise elements after
* the insertTag is found are inserted. */
boolean insertInsertTag;
/** Set to true when insertTag has been found. */
boolean foundInsertTag;
/** When foundInsertTag is set to true, this will be updated to
* reflect the delta between the two structures. That is, it
* will be the depth the inserts are happening at minus the
* depth of the tags being passed in. A value of 0 (the common
* case) indicates the structures match, a value greater than 0 indicates
* the insert is happening at a deeper depth than the stream is
* parsing, and a value less than 0 indicates the insert is happening earlier
* in the tree that the parser thinks and that we will need to remove
* EndTagType specs in the flushBuffer method.
*/
int insertTagDepthDelta;
/** How many parents to ascend before insert new elements. */
int popDepth;
/** How many parents to descend (relative to popDepth) before
* inserting. */
int pushDepth;
/** Last Map that was encountered. */
Map lastMap;
/** Set to true when a style element is encountered. */
boolean inStyle = false;
/** Name of style to use. Obtained from Meta tag. */
String defaultStyle;
/** Vector describing styles that should be include. Will consist
* of a bunch of HTML.Tags, which will either be:
* <p>LINK: in which case it is followed by an AttributeSet
* <p>STYLE: in which case the following element is a String
* indicating the type (may be null), and the elements following
* it until the next HTML.Tag are the rules as Strings.
*/
Vector styles;
/** True if inside the head tag. */
boolean inHead = false;
/** Set to true if the style language is text/css. Since this is
* used alot, it is cached. */
boolean isStyleCSS;
/** True if inserting into an empty document. */
boolean emptyDocument;
/** Attributes from a style Attribute. */
AttributeSet styleAttributes;
/**
* Current option, if in an option element (needed to
* load the label.
*/
Option option;
protected Vector<ElementSpec> parseBuffer = new Vector(); // Vector<ElementSpec>
protected MutableAttributeSet charAttr = new TaggedAttributeSet();
Stack charAttrStack = new Stack();
Hashtable tagMap;
int inBlock = 0;
/**
* This attribute is sometimes used to refer to next tag
* to be handled after p-implied when the latter is
* the current tag which is being handled.
*/
private HTML.Tag nextTagAfterPImplied = null;
}
/**
* Used by StyleSheet to determine when to avoid removing HTML.Tags
* matching StyleConstants.
*/
static class TaggedAttributeSet extends SimpleAttributeSet {
TaggedAttributeSet() {
super();
}
}
/**
* An element that represents a chunk of text that has
* a set of HTML character level attributes assigned to
* it.
*/
public class RunElement extends LeafElement {
/**
* Constructs an element that represents content within the
* document (has no children).
*
* @param parent the parent element
* @param a the element attributes
* @param offs0 the start offset (must be at least 0)
* @param offs1 the end offset (must be at least offs0)
* @since 1.4
*/
public RunElement(Element parent, AttributeSet a, int offs0, int offs1) {
super(parent, a, offs0, offs1);
}
/**
* Gets the name of the element.
*
* @return the name, null if none
*/
public String getName() {
Object o = getAttribute(StyleConstants.NameAttribute);
if (o != null) {
return o.toString();
}
return super.getName();
}
/**
* Gets the resolving parent. HTML attributes are not inherited
* at the model level so we override this to return null.
*
* @return null, there are none
* @see AttributeSet#getResolveParent
*/
public AttributeSet getResolveParent() {
return null;
}
}
/**
* An element that represents a structural <em>block</em> of
* HTML.
*/
public class BlockElement extends BranchElement {
/**
* Constructs a composite element that initially contains
* no children.
*
* @param parent the parent element
* @param a the attributes for the element
* @since 1.4
*/
public BlockElement(Element parent, AttributeSet a) {
super(parent, a);
}
/**
* Gets the name of the element.
*
* @return the name, null if none
*/
public String getName() {
Object o = getAttribute(StyleConstants.NameAttribute);
if (o != null) {
return o.toString();
}
return super.getName();
}
/**
* Gets the resolving parent. HTML attributes are not inherited
* at the model level so we override this to return null.
*
* @return null, there are none
* @see AttributeSet#getResolveParent
*/
public AttributeSet getResolveParent() {
return null;
}
}
/**
* Document that allows you to set the maximum length of the text.
*/
private static class FixedLengthDocument extends PlainDocument {
private int maxLength;
public FixedLengthDocument(int maxLength) {
this.maxLength = maxLength;
}
public void insertString(int offset, String str, AttributeSet a)
throws BadLocationException {
if (str != null && str.length() + getLength() <= maxLength) {
super.insertString(offset, str, a);
}
}
}
}