This example Java source code file (HTMLDocument.java) is included in the alvinalexander.com
"Java Source Code
Warehouse" project. The intent of this project is to help you "Learn
Java by Example" TM.
/*
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javax.swing.text.html;
import java.awt.font.TextAttribute;
import java.util.*;
import java.net.URL;
import java.net.MalformedURLException;
import java.io.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;
import sun.swing.SwingUtilities2;
import static sun.swing.SwingUtilities2.IMPLIED_CR;
/**
* 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, which implements the
* <code>HTMLEditorKit.ParserCallback protocol that the parser
* expects. To change the structure one can subclass
* <code>HTMLReader, and reimplement the method {@link
* #getReader(int)} to return the new reader implementation. The
* documentation for <code>HTMLReader 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 attribute, which should
* always have a value of type <code>HTML.Tag that identifies
* the kind of element. Some of the elements (such as comments) are
* synthesized. The <code>HTMLFactory uses this attribute to
* determine what kind of view to build.</p>
*
* <p>This document supports incremental loading. The
* <code>TokenThreshold 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 so
* that subclasses can disable it.</p>
*
* <p>The Base property determines the URL against which
* relative URLs are resolved. By default, this will be the
* <code>Document.StreamDescriptionProperty 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>
*
* <p>The default content storage mechanism for this document is a gap
* buffer (<code>GapContent). Alternatives can be supplied by
* using the constructor that takes a <code>Content
* implementation.</p>
*
* <h2>Modifying HTMLDocument
*
* <p>In addition to the methods provided by Document and
* StyledDocument for mutating an HTMLDocument, HTMLDocument provides
* a number of convenience methods. The following methods can be used
* to insert HTML content into an existing document.</p>
*
* <ul>
* <li>{@link #setInnerHTML(Element, String)}
* <li>{@link #setOuterHTML(Element, String)}
* <li>{@link #insertBeforeStart(Element, String)}
* <li>{@link #insertAfterStart(Element, String)}
* <li>{@link #insertBeforeEnd(Element, String)}
* <li>{@link #insertAfterEnd(Element, String)}
* </ul>
*
* <p>The following examples illustrate using these methods. Each
* example assumes the HTML document is initialized in the following
* way:</p>
*
* <pre>
* JEditorPane p = new JEditorPane();
* p.setContentType("text/html");
* p.setText("..."); // Document text is provided below.
* HTMLDocument d = (HTMLDocument) p.getDocument();
* </pre>
*
* <p>With the following HTML content:
*
* <pre>
* <html>
* <head>
* <title>An example HTMLDocument</title>
* <style type="text/css">
* div { background-color: silver; }
* ul { color: red; }
* </style>
* </head>
* <body>
* <div id="BOX">
* <p>Paragraph 1</p>
* <p>Paragraph 2</p>
* </div>
* </body>
* </html>
* </pre>
*
* <p>All the methods for modifying an HTML document require an {@link
* Element}. Elements can be obtained from an HTML document by using
* the method {@link #getElement(Element e, Object attribute, Object
* value)}. It returns the first descendant element that contains the
* specified attribute with the given value, in depth-first order.
* For example, <code>d.getElement(d.getDefaultRootElement(),
* StyleConstants.NameAttribute, HTML.Tag.P)</code> returns the first
* paragraph element.</p>
*
* <p>A convenient shortcut for locating elements is the method {@link
* #getElement(String)}; returns an element whose <code>ID
* attribute matches the specified value. For example,
* <code>d.getElement("BOX") returns the DIV
* element.</p>
*
* <p>The {@link #getIterator(HTML.Tag t)} method can also be used for
* finding all occurrences of the specified HTML tag in the
* document.</p>
*
* <h3>Inserting elements
*
* <p>Elements can be inserted before or after the existing children
* of any non-leaf element by using the methods
* <code>insertAfterStart and insertBeforeEnd.
* For example, if <code>e is the DIV element,
* <code>d.insertAfterStart(e, "<ul><li>List
* Item</li></ul>")</code> inserts the list before the first
* paragraph, and <code>d.insertBeforeEnd(e, "<ul><li>List
* Item</li></ul>")</code> inserts the list after the last
* paragraph. The <code>DIV block becomes the parent of the
* newly inserted elements.</p>
*
* <p>Sibling elements can be inserted before or after any element by
* using the methods <code>insertBeforeStart and
* <code>insertAfterEnd. For example, if e is the
* <code>DIV element, d.insertBeforeStart(e,
* "<ul><li>List Item</li></ul>")</code> inserts the list
* before the <code>DIV element, and d.insertAfterEnd(e,
* "<ul><li>List Item</li></ul>")</code> inserts the list
* after the <code>DIV element. The newly inserted elements
* become siblings of the <code>DIV element.
*
* <h3>Replacing elements
*
* <p>Elements and all their descendants can be replaced by using the
* methods <code>setInnerHTML and setOuterHTML.
* For example, if <code>e is the DIV element,
* <code>d.setInnerHTML(e, "<ul><li>List
* Item</li></ul>")</code> replaces all children paragraphs with
* the list, and <code>d.setOuterHTML(e, "<ul><li>List
* Item</li></ul>")</code> replaces the DIV element
* itself. In latter case the parent of the list is the
* <code>BODY element.
*
* <h3>Summary
*
* <p>The following table shows the example document and the results
* of various methods described above.</p>
*
* <table border=1 cellspacing=0>
* <tr>
* <th>Example
* <th>insertAfterStart
* <th>insertBeforeEnd
* <th>insertBeforeStart
* <th>insertAfterEnd
* <th>setInnerHTML
* <th>setOuterHTML
* </tr>
* <tr valign="top">
* <td style="white-space:nowrap">
* <div style="background-color: silver;">
* <p>Paragraph 1
* <p>Paragraph 2
* </div>
* </td>
* <!--insertAfterStart-->
* <td style="white-space:nowrap">
* <div style="background-color: silver;">
* <ul style="color: red;">
* <li>List Item
* </ul>
* <p>Paragraph 1
* <p>Paragraph 2
* </div>
* </td>
* <!--insertBeforeEnd-->
* <td style="white-space:nowrap">
* <div style="background-color: silver;">
* <p>Paragraph 1
* <p>Paragraph 2
* <ul style="color: red;">
* <li>List Item
* </ul>
* </div>
* </td>
* <!--insertBeforeStart-->
* <td style="white-space:nowrap">
* <ul style="color: red;">
* <li>List Item
* </ul>
* <div style="background-color: silver;">
* <p>Paragraph 1
* <p>Paragraph 2
* </div>
* </td>
* <!--insertAfterEnd-->
* <td style="white-space:nowrap">
* <div style="background-color: silver;">
* <p>Paragraph 1
* <p>Paragraph 2
* </div>
* <ul style="color: red;">
* <li>List Item
* </ul>
* </td>
* <!--setInnerHTML-->
* <td style="white-space:nowrap">
* <div style="background-color: silver;">
* <ul style="color: red;">
* <li>List Item
* </ul>
* </div>
* </td>
* <!--setOuterHTML-->
* <td style="white-space:nowrap">
* <ul style="color: red;">
* <li>List Item
* </ul>
* </td>
* </tr>
* </table>
*
* <p>Warning: 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™
* has been added to the
* <code>java.beans package. Please see {@link
* java.beans.XMLEncoder}.</p>
*
* @author Timothy Prinzing
* @author Scott Violet
* @author Sunita Mani
*/
public class HTMLDocument extends DefaultStyledDocument {
/**
* Constructs an HTML document using the default buffer size
* and a default <code>StyleSheet. This is a convenience
* method for the constructor
* <code>HTMLDocument(Content, StyleSheet).
*/
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).
*
* @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.
* 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.
* 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).
*
* @param popDepth the number of <code>ElementSpec.EndTagTypes
* to generate before inserting
* @param pushDepth the number of <code>ElementSpec.StartTagTypes
* with a direction of <code>ElementSpec.JoinNextDirection
* 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
* to generate before inserting
* @param pushDepth the number of <code>ElementSpec.StartTagTypes
* with a direction of <code>ElementSpec.JoinNextDirection
* 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 StyleSheet
* to be <code>u 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://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html">Concurrency
* in Swing</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 with the document-specific display
* rules (CSS) that were specified in the HTML document itself.
*
* @return the <code>StyleSheet
*/
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
* @return the <code>Iterator 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.
*
* @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.
*
* @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.
*
* @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 that
* are generated by documents in an HTML frame.
* The <code>HyperlinkEvent type, as the parameter suggests,
* is <code>HTMLFrameHyperlinkEvent.
* In addition to the typical information contained in a
* <code>HyperlinkEvent,
* 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 attribute and fires a
* <code>ChangedUpdate 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 attribute
* to have a value equal to the destination URL and fire a
* <code>RemovedUpdate and InsertUpdate.
*<p>
* If the target is _top, this method does nothing. In the implementation
* of the view for a frame, namely the <code>FrameView,
* 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 attribute is updated and a
* <code>ChangedUpdate 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.
*
* @param frameName
* @return the element whose NAME attribute has a value of
* <code>frameName; returns null
* if not found
*/
private Element findFrame(String frameName) {
ElementIterator it = new ElementIterator(this);
Element next;
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 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
* and fires a <code>ChangedUpdate 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 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 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 name the name of the desired <code>Map
* @return the <code>Map or null if it can't
* be found, or if <code>name is null
*/
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 of the possible Maps.
* @return the enumerated list of maps, or <code>null
* if the maps are not an instance of <code>Hashtable
*/
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,
* and <code>setOuterHTML.
* <p>
* <code>HTMLEditorKit.createDefaultDocument will set the parser
* for you. If you create an <code>HTMLDocument 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>
*
* <p>Consider the following structure (the elem
* parameter is <b>in bold).
*
* <pre>
* <body>
* |
* <b><div>
* / \
* <p> <p>
* </pre>
*
* <p>Invoking setInnerHTML(elem, "<ul><li>")
* results in the following structure (new elements are <font
* color="red">in red</font>).
*
* <pre>
* <body>
* |
* <b><div>
* \
* <font color="red"><ul>
* \
* <font color="red"><li>
* </pre>
*
* <p>Parameter elem must not be a leaf element,
* otherwise an <code>IllegalArgumentException is thrown.
* If either <code>elem or htmlText parameter
* is <code>null, no changes are made to the document.
*
* <p>For this to work correctly, the document must have an
* <code>HTMLEditorKit.Parser set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument method.
*
* @param elem the branch element whose children will be replaced
* @param htmlText the string to be parsed and assigned to <code>elem
* @throws IllegalArgumentException if <code>elem is a leaf
* @throws IllegalStateException if an <code>HTMLEditorKit.Parser
* 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>
*
* <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, and one for the newline.</p>
*
* <p>If you try to replace the element at length you will most
* likely end up with two elements, eg
* <code>setOuterHTML(getCharacterElement (getLength()),
* "blah")</code> will result in two leaf elements at the end, one
* representing 'blah', and the other representing the end
* element.</p>
*
* <p>Consider the following structure (the elem
* parameter is <b>in bold).
*
* <pre>
* <body>
* |
* <b><div>
* / \
* <p> <p>
* </pre>
*
* <p>Invoking setOuterHTML(elem, "<ul><li>")
* results in the following structure (new elements are <font
* color="red">in red</font>).
*
* <pre>
* <body>
* |
* <font color="red"><ul>
* \
* <font color="red"><li>
* </pre>
*
* <p>If either elem or htmlText
* parameter is <code>null, no changes are made to the
* document.</p>
*
* <p>For this to work correctly, 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 method.
*
* @param elem the element to replace
* @param htmlText the string to be parsed and inserted in place of <code>elem
* @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>Consider the following structure (the elem
* parameter is <b>in bold).
*
* <pre>
* <body>
* |
* <b><div>
* / \
* <p> <p>
* </pre>
*
* <p>Invoking insertAfterStart(elem,
* "<ul><li>")</code> results in the following structure
* (new elements are <font color="red">in red).
*
* <pre>
* <body>
* |
* <b><div>
* / | \
* <font color="red"><ul> <p> <p>
* /
* <font color="red"><li>
* </pre>
*
* <p>Unlike the insertBeforeStart method, new
* elements become <em>children of the specified element,
* not siblings.</p>
*
* <p>Parameter elem must not be a leaf element,
* otherwise an <code>IllegalArgumentException is thrown.
* If either <code>elem or htmlText parameter
* is <code>null, no changes are made to the document.
*
* <p>For this to work correctly, the document must have an
* <code>HTMLEditorKit.Parser set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument 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
* @throws IllegalArgumentException if <code>elem is a leaf
* @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 || htmlText == null) {
return;
}
if (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 elem's children are leaves, and the
* character at a <code>elem.getEndOffset() - 1 is a newline,
* this will insert before the newline so that there isn't text after
* the newline.</p>
*
* <p>Consider the following structure (the elem
* parameter is <b>in bold).
*
* <pre>
* <body>
* |
* <b><div>
* / \
* <p> <p>
* </pre>
*
* <p>Invoking insertBeforeEnd(elem, "<ul><li>")
* results in the following structure (new elements are <font
* color="red">in red</font>).
*
* <pre>
* <body>
* |
* <b><div>
* / | \
* <p> <p> <font color="red"><ul>
* \
* <font color="red"><li>
* </pre>
*
* <p>Unlike the insertAfterEnd method, new elements
* become <em>children of the specified element, not
* siblings.</p>
*
* <p>Parameter elem must not be a leaf element,
* otherwise an <code>IllegalArgumentException is thrown.
* If either <code>elem or htmlText parameter
* is <code>null, no changes are made to the document.
*
* <p>For this to work correctly, the document must have an
* <code>HTMLEditorKit.Parser set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument 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
* @throws IllegalArgumentException if <code>elem is a leaf
* @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");
}
if (elem != null) {
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>Consider the following structure (the elem
* parameter is <b>in bold).
*
* <pre>
* <body>
* |
* <b><div>
* / \
* <p> <p>
* </pre>
*
* <p>Invoking insertBeforeStart(elem,
* "<ul><li>")</code> results in the following structure
* (new elements are <font color="red">in red).
*
* <pre>
* <body>
* / \
* <font color="red"><ul> <div>
* / / \
* <font color="red"><li> <p> <p>
* </pre>
*
* <p>Unlike the insertAfterStart method, new
* elements become <em>siblings of the specified element, not
* children.</p>
*
* <p>If either elem or htmlText
* parameter is <code>null, no changes are made to the
* document.</p>
*
* <p>For this to work correctly, the document must have an
* <code>HTMLEditorKit.Parser set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument method.
*
* @param elem the element the content is inserted before
* @param htmlText the string to be parsed and inserted before <code>elem
* @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>Consider the following structure (the elem
* parameter is <b>in bold).
*
* <pre>
* <body>
* |
* <b><div>
* / \
* <p> <p>
* </pre>
*
* <p>Invoking insertAfterEnd(elem, "<ul><li>")
* results in the following structure (new elements are <font
* color="red">in red</font>).
*
* <pre>
* <body>
* / \
* <b><div> <ul>
* / \ \
* <p> <p> <font color="red"><li>
* </pre>
*
* <p>Unlike the insertBeforeEnd method, new elements
* become <em>siblings of the specified element, not
* children.</p>
*
* <p>If either elem or htmlText
* parameter is <code>null, no changes are made to the
* document.</p>
*
* <p>For this to work correctly, the document must have an
* <code>HTMLEditorKit.Parser set. This will be the case
* if the document was created from an HTMLEditorKit via the
* <code>createDefaultDocument method.
*
* @param elem the element the content is inserted after
* @param htmlText the string to be parsed and inserted after <code>elem
* @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.
* If the element can't be found, <code>null is returned.
* Note that this method works on an <code>Attribute,
* <i>not a character tag. In the following HTML snippet:
* <code><a id="HelloThere"> the attribute is
* 'id' and the character tag is 'a'.
* This is a convenience method for
* <code>getElement(RootElement, HTML.Attribute.id, id).
* This is not thread-safe.
*
* @param id the string representing the desired <code>Attribute
* @return the element with the specified <code>Attribute
* or <code>null if it can't be found,
* or <code>null if id is null
* @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 that contains the
* attribute, <code>attribute with value value, or
* <code>null 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
* @param value the values for the specified <code>Attribute
* @return the element with the specified <code>Attribute
* and the specified <code>value, or null
* 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 that contains the
* attribute, <code>attribute with value value, or
* <code>null if one isn't found. This is not thread-safe.
* <p>
* If <code>searchLeafAttributes is true, and e is
* a leaf, any attributes that are instances of <code>HTML.Tag
* with a value that is an <code>AttributeSet will also be checked.
*
* @param e the root element where the search begins
* @param attribute the desired <code>Attribute
* @param value the values for the specified <code>Attribute
* @return the element with the specified <code>Attribute
* and the specified <code>value, or null
* 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 set.
* If <code>getParser returns null, 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 is used to identify the location to insert the
* <code>html. If parent 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. 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 e
* will still have at least one child after the remove, and it assumes
* the character at <code>e.getStartOffset() - 1 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 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 to end.
* The end Element(s) will be removed, and the element representing
* <code>start - 1 to start will be recreated. This
* Element has to be recreated as after the content removal its offsets
* become <code>start - 1 to start - 1.
*/
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, it removes
* <code>count elements starting at start from
* <code>e. If remove is true text of length
* <code>start - 1 to end - 1 is removed. If
* <code>create 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<String, ButtonGroup> 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;
/**
* 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 for this tag, or
* <code>null 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 for this tag,
* or <code>null 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 = 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 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.
* @return true if current position is not <code>null,
* 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.
*/
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)
* 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.
* <table border=1 summary="HTML tags and assigned actions">
* <tr>
Tag
Action
* <tr>
HTML.Tag.A
CharacterAction
* <tr>
HTML.Tag.ADDRESS
CharacterAction
* <tr>
HTML.Tag.APPLET
HiddenAction
* <tr>
HTML.Tag.AREA
AreaAction
* <tr>
HTML.Tag.B
CharacterAction
* <tr>
HTML.Tag.BASE
BaseAction
* <tr>
HTML.Tag.BASEFONT
CharacterAction
* <tr>
HTML.Tag.BIG
CharacterAction
* <tr>
HTML.Tag.BLOCKQUOTE
BlockAction
* <tr>
HTML.Tag.BODY
BlockAction
* <tr>
HTML.Tag.BR
SpecialAction
* <tr>
HTML.Tag.CAPTION
BlockAction
* <tr>
HTML.Tag.CENTER
BlockAction
* <tr>
HTML.Tag.CITE
CharacterAction
* <tr>
HTML.Tag.CODE
CharacterAction
* <tr>
HTML.Tag.DD
BlockAction
* <tr>
HTML.Tag.DFN
CharacterAction
* <tr>
HTML.Tag.DIR
BlockAction
* <tr>
HTML.Tag.DIV
BlockAction
* <tr>
HTML.Tag.DL
BlockAction
* <tr>
HTML.Tag.DT
ParagraphAction
* <tr>
HTML.Tag.EM
CharacterAction
* <tr>
HTML.Tag.FONT
CharacterAction
* <tr>
HTML.Tag.FORM
As of 1.4 a BlockAction
* <tr>
HTML.Tag.FRAME
SpecialAction
* <tr>
HTML.Tag.FRAMESET
BlockAction
* <tr>
HTML.Tag.H1
ParagraphAction
* <tr>
HTML.Tag.H2
ParagraphAction
* <tr>
HTML.Tag.H3
ParagraphAction
* <tr>
HTML.Tag.H4
ParagraphAction
* <tr>
HTML.Tag.H5
ParagraphAction
* <tr>
HTML.Tag.H6
ParagraphAction
* <tr>
HTML.Tag.HEAD
HeadAction
* <tr>
HTML.Tag.HR
SpecialAction
* <tr>
HTML.Tag.HTML
BlockAction
* <tr>
HTML.Tag.I
CharacterAction
* <tr>
HTML.Tag.IMG
SpecialAction
* <tr>
HTML.Tag.INPUT
FormAction
* <tr>
HTML.Tag.ISINDEX
IsndexAction
* <tr>
HTML.Tag.KBD
CharacterAction
* <tr>
HTML.Tag.LI
BlockAction
* <tr>
HTML.Tag.LINK
LinkAction
* <tr>
HTML.Tag.MAP
MapAction
* <tr>
HTML.Tag.MENU
BlockAction
* <tr>
HTML.Tag.META
MetaAction
* <tr>
HTML.Tag.NOFRAMES
BlockAction
* <tr>
HTML.Tag.OBJECT
SpecialAction
* <tr>
HTML.Tag.OL
BlockAction
* <tr>
HTML.Tag.OPTION
FormAction
* <tr>
HTML.Tag.P
ParagraphAction
* <tr>
HTML.Tag.PARAM
HiddenAction
* <tr>
HTML.Tag.PRE
PreAction
* <tr>
HTML.Tag.SAMP
CharacterAction
* <tr>
HTML.Tag.SCRIPT
HiddenAction
* <tr>
HTML.Tag.SELECT
FormAction
* <tr>
HTML.Tag.SMALL
CharacterAction
* <tr>
HTML.Tag.STRIKE
CharacterAction
* <tr>
HTML.Tag.S
CharacterAction
* <tr>
HTML.Tag.STRONG
CharacterAction
* <tr>
HTML.Tag.STYLE
StyleAction
* <tr>
HTML.Tag.SUB
CharacterAction
* <tr>
HTML.Tag.SUP
CharacterAction
* <tr>
HTML.Tag.TABLE
BlockAction
* <tr>
HTML.Tag.TD
BlockAction
* <tr>
HTML.Tag.TEXTAREA
FormAction
* <tr>
HTML.Tag.TH
BlockAction
* <tr>
HTML.Tag.TITLE
TitleAction
* <tr>
HTML.Tag.TR
BlockAction
* <tr>
HTML.Tag.TT
CharacterAction
* <tr>
HTML.Tag.U
CharacterAction
* <tr>
HTML.Tag.UL
BlockAction
* <tr>
HTML.Tag.VAR
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 is
* non-<code>null, and offset 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<HTML.Tag, TagAction>(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 flag.
* It is left in <code>false 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
* 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
* until a parent with name, <code>name has been
* found. -1 indicates no matching parent with
* <code>name.
*/
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;
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<Element> 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 = 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 = 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 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 = 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 = 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. eol 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<String, ButtonGroup>();
}
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<Object>(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<Object>(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 = 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 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 the 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 = attr.getAttribute(HTML.Attribute.MULTIPLE) != null;
if ((size > 1) || multiple) {
OptionListModel<Option> m = new OptionListModel