- /*
- * @(#)HTMLDocument.java 1.133 00/02/02
- *
- * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
- *
- * This software is the proprietary information of Sun Microsystems, Inc.
- * Use is subject to license terms.
- *
- */
- package javax.swing.text.html;
- import java.awt.Color;
- import java.awt.Component;
- import java.util.*;
- import java.net.URL;
- import java.net.URLEncoder;
- import java.net.MalformedURLException;
- import java.io.*;
- import javax.swing.*;
- import javax.swing.event.*;
- import javax.swing.text.*;
- import javax.swing.undo.*;
- /**
- * A document that models HTML. The purpose of this model
- * is to support both browsing and editing. As a result,
- * the structure described by an HTML document is not
- * exactly replicated by default. The element structure that
- * is modeled by default, is built by the class
- * <code>HTMLDocument.HTMLReader</code>, which implements
- * the <code>HTMLEditorKit.ParserCallback</code> protocol
- * that the parser expects. To change the structure one
- * can subclass HTMLReader, and reimplement the method
- * <code>getReader</code> to return the new
- * reader implementation. The documentation for
- * 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</code> attribute,
- * which should always have a value of type <code>HTML.Tag</code>
- * that identifies the kind of element. Some of the elements
- * (such as comments) are synthesized. The HTMLFactory
- * uses this attribute to determine what kind of view to build.
- * <p>
- * This document supports incremental loading. The
- * <code>TokenThreshold</code> property controls how
- * much of the parse is buffered before trying to update
- * the element structure of the document. This property
- * is set by the EditorKit so that subclasses can disable
- * it.
- * <p>
- * The <code>Base</code> property determines the URL
- * against which relative URLs are resolved.
- * By default, this will be the
- * <code>Document.StreamDescriptionProperty</code> if
- * the value of the property is a URL. If a <BASE>
- * tag is encountered, the base will become the URL specified
- * by that tag. Because the base URL is a property, it
- * can of course be set directly.
- * <p>
- * The default content storage mechanism for this document
- * is a gap buffer (GapContent). Alternatives can be supplied
- * by using the constructor that takes a Content implementation.
- *
- * @author Timothy Prinzing
- * @author Scott Violet
- * @author Sunita Mani
- * @version 1.133 02/02/00
- */
- public class HTMLDocument extends DefaultStyledDocument {
- /**
- * Constructs an HTML document.
- */
- public HTMLDocument() {
- this(new GapContent(BUFFER_SIZE_DEFAULT), new StyleSheet());
- }
- /**
- * Constructs an HTML document with the default content
- * storage implementation and the given style/attribute
- * storage mechanism.
- *
- * @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 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.).
- */
- 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;
- }
- /**
- * 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 ElementSpec.EndTagTypes to generate before
- * inserting
- * @param pushDepth the number of ElementSpec.StartTagTypes with a direction
- * of 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
- */
- 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 ElementSpec.EndTagTypes to generate before
- * inserting
- * @param pushDepth the number of ElementSpec.StartTagTypes with a direction
- * of 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
- */
- 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;
- }
- /**
- * Gets 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.
- */
- 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</code>
- * as well as the receiver.
- */
- 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);
- }
- 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.
- */
- protected void create(ElementSpec[] data) {
- super.create(data);
- }
- /**
- * Sets attributes for a paragraph.
- * <p>
- * This method is thread safe, although most Swing methods
- * are not. Please see
- * <A HREF="http://java.sun.com/products/jfc/swingdoc-archive/threads.html">Threads
- * and 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 StyleSheet with the document-specific display
- * rules (CSS) that were specified in the HTML document itself.
- */
- public StyleSheet getStyleSheet() {
- return (StyleSheet) getAttributeContext();
- }
- /**
- * Fetches an iterator for the following kind of HTML tag.
- * This can be used for things like iterating over the
- * set of anchors contained, iterating over the input
- * elements, etc.
- */
- public Iterator getIterator(HTML.Tag t) {
- if (t.isBlock()) {
- // TBD
- return null;
- }
- return new LeafIterator(t, this);
- }
- /**
- * Creates a document leaf element that directly represents
- * text (doesn't have any children). This is implemented
- * to return an element of type
- * <code>HTMLDocument.RunElement</code>.
- *
- * @param parent the parent element
- * @param a the attributes for the element
- * @param p0 the beginning of the range (must be at least 0)
- * @param p1 the end of the range (must be at least p0)
- * @return the new element
- */
- protected Element createLeafElement(Element parent, AttributeSet a, int p0, int p1) {
- return new RunElement(parent, a, p0, p1);
- }
- /**
- * Creates a document branch element, that can contain other elements.
- * This is implemented to return an element of type
- * <code>HTMLDocument.BlockElement</code>.
- *
- * @param parent the parent element
- * @param a the attributes
- * @return the element
- */
- protected Element createBranchElement(Element parent, AttributeSet a) {
- return new BlockElement(parent, a);
- }
- /**
- * Creates the root element to be used to represent the
- * default document structure.
- *
- * @return the element base
- */
- protected AbstractElement createDefaultRoot() {
- // grabs a write-lock for this initialization and
- // abandon it during initialization so in normal
- // operation we can detect an illegitimate attempt
- // to mutate attributes.
- writeLock();
- MutableAttributeSet a = new SimpleAttributeSet();
- a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.HTML);
- BlockElement html = new BlockElement(null, a.copyAttributes());
- a.removeAttributes(a);
- a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.BODY);
- BlockElement body = new BlockElement(html, a.copyAttributes());
- a.removeAttributes(a);
- a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.P);
- 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.
- */
- 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. By default, this will
- * be <code>Integer.MAX_VALUE</code>.
- */
- public int getTokenThreshold() {
- Integer i = (Integer) getProperty(TokenThreshold);
- if (i != null) {
- return i.intValue();
- }
- return Integer.MAX_VALUE;
- }
- /**
- * Sets how unknown tags are handled. If set to true, unknown
- * tags are put in the model, otherwise they are dropped.
- */
- public void setPreservesUnknownTags(boolean preservesTags) {
- preservesUnknownTags = preservesTags;
- }
- /**
- * @return true if unknown tags are to be preserved when parsing.
- */
- public boolean getPreservesUnknownTags() {
- return preservesUnknownTags;
- }
- /**
- * Processes HyperlinkEvents that
- * are generated by documents in an HTML frame. The HyperlinkEvent
- * type, as the parameter suggests, is HTMLFrameHyperlinkEvent.
- * In addition to the typical information contained in a 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
- * HTML.Attribute.SRC attribute and fires a ChangedUpdate event.
- *
- * 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 HTML.Attribute.SRC attribute to have a value equal
- * to the destination URL and fire a RemovedUpdate and InsertUpdate.
- *
- * If the target is _top, this method does nothing. In the implementation
- * of the view for a frame, namely the 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.
- *
- * If the target is a named frame, then the element hierarchy is searched
- * for an element with a name equal to the target, its HTML.Attribute.SRC
- * attribute is updated and a 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 frameName
- *
- * @param frameName
- * @return element the element whose NAME attribute has
- * a value of frameName. returns null if not
- * found.
- */
- private Element findFrame(String frameName) {
- ElementIterator it = new ElementIterator(this);
- Element next = null;
- while ((next = it.next()) != null) {
- AttributeSet attr = next.getAttributes();
- if (matchNameAttribute(attr, HTML.Tag.FRAME)) {
- String frameTarget = (String)attr.getAttribute(HTML.Attribute.NAME);
- if (frameTarget != null && frameTarget.equals(frameName)) {
- break;
- }
- }
- }
- return next;
- }
- /**
- * Returns true if the StyleConstants.NameAttribute is
- * equal to the tag that is passed in as a parameter.
- *
- */
- 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 = element.getEndOffset();
- remove(startOffset, endOffset - startOffset);
- SimpleAttributeSet attr = new SimpleAttributeSet();
- attr.addAttribute(HTML.Attribute.SRC, url);
- attr.addAttribute(StyleConstants.NameAttribute, HTML.Tag.FRAME);
- insertString(startOffset, " ", attr);
- } catch (BadLocationException e1) {
- // Should handle this better
- }
- }
- /**
- * Updates the Frame elements HTML.Attribute.SRC attribute and
- * fires a 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.
- */
- boolean isFrameDocument() {
- return frameDocument;
- }
- /**
- * Sets a boolean state about whether the document will be
- * viewed in a frame.
- */
- 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.
- */
- 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.
- */
- 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.
- */
- 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 Enumeration of the possible Maps.
- */
- 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.
- */
- /* public */
- void setDefaultStyleSheetType(String contentType) {
- putProperty(StyleType, contentType);
- }
- /**
- * Returns the content type language used for style sheets. The default
- * is text/css.
- */
- /* 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, eg <code>setInnerHTML</code>,
- * <code>setOuterHTML</code>...
- * <p>
- * <code>HTMLEditorKit.createDefaultDocument</code> will set the parser
- * for you. If you create an HTMLDocument by hand, be sure and set the
- * parser accordingly.
- *
- * @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.
- *
- * @since 1.3
- */
- public HTMLEditorKit.Parser getParser() {
- Object p = getProperty("__PARSER__");
- if (p instanceof HTMLEditorKit.Parser) {
- return (HTMLEditorKit.Parser)p;
- }
- return parser;
- }
- /**
- * Replaces the children of the given element with the contents
- * specified as an HTML string.
- * <p>This will be seen as at least two events, n inserts followed by
- * a remove.
- * <p>For this to work correcty, the receiver must have an
- * HTMLEditorKit.Parser set. This will be the case if the receiver
- * was created from an HTMLEditorKit via the
- * <code>createDefaultDocument</code> method.
- *
- * @throws IllegalArgumentException is <code>elem</code> is a leaf
- * @throws IllegalStateException if an HTMLEditorKit.Parser has not
- * been set on the receiver.
- * @since 1.3
- */
- public void setInnerHTML(Element elem, String htmlText) throws
- BadLocationException, IOException {
- verifyParser();
- if (elem != null && elem.isLeaf()) {
- throw new IllegalArgumentException
- ("Can not set inner HTML of a leaf");
- }
- if (elem != null && htmlText != null) {
- int oldCount = elem.getElementCount();
- int insertPosition = elem.getStartOffset();
- insertHTML(elem, elem.getStartOffset(), htmlText, true);
- if (elem.getElementCount() > oldCount) {
- // Elements were inserted, do the cleanup.
- removeElements(elem, elem.getElementCount() - oldCount,
- oldCount);
- }
- }
- }
- /**
- * Replaces the given element in the parent with the contents
- * specified as an HTML string.
- * <p>This will be seen as at least two events, n inserts followed by
- * a remove.
- * <p>When replacing a leaf this will attempt to make sure there is
- * a newline present if one is needed. This may result in an additional
- * element being inserted. Consider, if you were to replace a character
- * element that contained a newline with <img> this would create
- * two elements, one for the image, ane one for the newline.
- * <p>If you try to replace the element at length you will most likely
- * end up with two elements, eg setOuterHTML(getCharacterElement
- * (getLength()), "blah") will result in two leaf elements at the end,
- * one representing 'blah', and the other representing the end element.
- * <p>For this to work correcty, the receiver must have an
- * HTMLEditorKit.Parser set. This will be the case if the receiver
- * was created from an HTMLEditorKit via the
- * <code>createDefaultDocument</code> method.
- *
- * @throws IllegalStateException if an HTMLEditorKit.Parser has not
- * been set on the receiver.
- * @since 1.3
- */
- public void setOuterHTML(Element elem, String htmlText) throws
- BadLocationException, IOException {
- verifyParser();
- if (elem != null && elem.getParentElement() != null &&
- htmlText != null) {
- int start = elem.getStartOffset();
- int end = elem.getEndOffset();
- int startLength = getLength();
- // We don't want a newline if elem is a leaf, and doesn't contain
- // a newline.
- boolean wantsNewline = !elem.isLeaf();
- if (!wantsNewline && (end > startLength ||
- getText(end - 1, 1).charAt(0) == NEWLINE[0])){
- wantsNewline = true;
- }
- Element parent = elem.getParentElement();
- int oldCount = parent.getElementCount();
- insertHTML(parent, start, htmlText, wantsNewline);
- // Remove old.
- int newLength = getLength();
- if (oldCount != parent.getElementCount()) {
- int removeIndex = parent.getElementIndex(start + newLength -
- startLength);
- removeElements(parent, removeIndex, 1);
- }
- }
- }
- /**
- * Inserts the HTML specified as a string at the start
- * of the element.
- * <p>For this to work correcty, the receiver must have an
- * HTMLEditorKit.Parser set. This will be the case if the receiver
- * was created from an HTMLEditorKit via the
- * <code>createDefaultDocument</code> method.
- *
- * @throws IllegalStateException if an HTMLEditorKit.Parser has not
- * been set on the receiver.
- * @since 1.3
- */
- public void insertAfterStart(Element elem, String htmlText) throws
- BadLocationException, IOException {
- verifyParser();
- if (elem != null && elem.isLeaf()) {
- throw new IllegalArgumentException
- ("Can not insert HTML after start of a leaf");
- }
- insertHTML(elem, elem.getStartOffset(), htmlText, false);
- }
- /**
- * Inserts the HTML specified as a string at the end of
- * the element.
- * <p> If <code>elem</code>'s children are leafs, at the
- * character at a <code>elem.getEndOffset() - 1</code> is a newline,
- * this will insert before the newline so that there isn't text after
- * the newline.
- * <p>For this to work correcty, the receiver must have an
- * HTMLEditorKit.Parser set. This will be the case if the receiver
- * was created from an HTMLEditorKit via the
- * <code>createDefaultDocument</code> method.
- *
- * @throws IllegalStateException if an HTMLEditorKit.Parser has not
- * been set on the receiver.
- * @since 1.3
- */
- public void insertBeforeEnd(Element elem, String htmlText) throws
- BadLocationException, IOException {
- verifyParser();
- if (elem != null && elem.isLeaf()) {
- throw new IllegalArgumentException
- ("Can not set inner HTML before end of leaf");
- }
- int offset = elem.getEndOffset();
- if (elem.getElement(elem.getElementIndex(offset - 1)).isLeaf() &&
- getText(offset - 1, 1).charAt(0) == NEWLINE[0]) {
- offset--;
- }
- insertHTML(elem, offset, htmlText, false);
- }
- /**
- * Inserts the HTML specified as string before the start of
- * the given element.
- * <p>For this to work correcty, the receiver must have an
- * HTMLEditorKit.Parser set. This will be the case if the receiver
- * was created from an HTMLEditorKit via the
- * <code>createDefaultDocument</code> method.
- *
- * @throws IllegalStateException if an HTMLEditorKit.Parser has not
- * been set on the receiver.
- * @since 1.3
- */
- public void insertBeforeStart(Element elem, String htmlText) throws
- BadLocationException, IOException {
- verifyParser();
- if (elem != null) {
- Element parent = elem.getParentElement();
- if (parent != null) {
- insertHTML(parent, elem.getStartOffset(), htmlText, false);
- }
- }
- }
- /**
- * Inserts the HTML specified as a string after the
- * the end of the given element.
- * <p>For this to work correcty, the receiver must have an
- * HTMLEditorKit.Parser set. This will be the case if the receiver
- * was created from an HTMLEditorKit via the
- * <code>createDefaultDocument</code> method.
- *
- * @throws IllegalStateException if an HTMLEditorKit.Parser has not
- * been set on the receiver.
- * @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 (elem.isLeaf() && getText(offset - 1, 1).
- charAt(0) == NEWLINE[0]) {
- offset--;
- }
- insertHTML(parent, offset, htmlText, false);
- }
- }
- }
- /**
- * Fetches the element that has the given id attribute.
- * If the element can't be found, null is returned. This is not
- * thread-safe.
- *
- * @since 1.3
- */
- public Element getElement(String id) {
- if (id == null) {
- return null;
- }
- return getElement(getDefaultRootElement(), HTML.Attribute.ID, id,
- true);
- }
- /**
- * Returns the child element of <code>e</code> that contains the
- * attribute, <code>attribute</code> with value <code>value</code>, or
- * null if one isn't found. This is not thread-safe.
- *
- * @since 1.3
- */
- public Element getElement(Element e, Object attribute, Object value) {
- return getElement(e, attribute, value, false);
- }
- /**
- * Returns the child element of <code>e</code> that contains the
- * attribute, <code>attribute</code> with value <code>value</code>, or
- * null if one isn't found. This is not thread-safe.
- * If <code>searchLeafAttributes</code> is true, and <code>e</code> is
- * a leaf, any attributes that are instances of HTML.Tag with a
- * value that is an AttributeSet will also be checked.
- */
- 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 receiver has an HTMLEditorKit.Parser set. If
- * <code>getParser</code> returns null, this will throw an
- * IllegalStateException.
- *
- * @throws IllegalStateException if the receiver does not have a Parser.
- */
- private void verifyParser() {
- if (getParser() == null) {
- throw new IllegalStateException("No HTMLEditorKit.Parser");
- }
- }
- /**
- * Inserts a string of HTML into the document at the given position.
- * <code>parent</code> is used to identify the location to insert the
- * <code>html</code>. If <code>parent</code> is a leaf this can have
- * unexpected results.
- */
- private void insertHTML(Element parent, int offset, String html,
- boolean wantsTrailingNewline)
- throws BadLocationException, IOException {
- if (parent != null && html != null) {
- HTMLEditorKit.Parser parser = getParser();
- if (parser != null) {
- int lastOffset = Math.max(0, offset - 1);
- Element charElement = getCharacterElement(lastOffset);
- Element commonParent = parent;
- int pop = 0;
- int push = 0;
- if (parent.getStartOffset() > lastOffset) {
- while (commonParent != null &&
- commonParent.getStartOffset() > lastOffset) {
- commonParent = commonParent.getParentElement();
- push++;
- }
- if (commonParent == null) {
- throw new BadLocationException("No common parent",
- offset);
- }
- }
- while (charElement != null && charElement != commonParent) {
- pop++;
- charElement = charElement.getParentElement();
- }
- if (charElement != null) {
- // Found it, do the insert.
- HTMLReader reader = new HTMLReader(offset, pop - 1, push,
- null, false, true,
- wantsTrailingNewline);
- parser.parse(new StringReader(html), reader, true);
- reader.flush();
- }
- }
- }
- }
- /**
- * Removes child Elements of the passed in Element <code>e</code>. This
- * will do the necessary cleanup to ensure the element representing the
- * end character is correctly created.
- * <p>This is not a general purpose method, it assumes that <code>e</code>
- * will still have at least one child after the remove, and it assumes
- * the character at <code>e.getStartOffset() - 1</code> is a newline and
- * is of length 1.
- */
- private void removeElements(Element e, int index, int count) throws BadLocationException {
- writeLock();
- try {
- int start = e.getElement(index).getStartOffset();
- int end = e.getElement(index + count - 1).getEndOffset();
- if (end > getLength()) {
- removeElementsAtEnd(e, index, count, start, end);
- }
- else {
- removeElements(e, index, count, start, end);
- }
- } finally {
- writeUnlock();
- }
- }
- /**
- * Called to remove child elements of <code>e</code> when one of the
- * elements to remove is representing the end character.
- * <p>Since the Content will not allow a removal to the end character
- * this will do a remove from <code>start - 1</code> to <code>end</code>.
- * The end Element(s) will be removed, and the element representing
- * <code>start - 1</code> to <code>start</code> will be recreated. This
- * Element has to be recreated as after the content removal its offsets
- * become <code>start - 1</code> to <code>start - 1</code>.
- */
- private void removeElementsAtEnd(Element e, int index, int count,
- int start, int end) throws BadLocationException {
- Element[] added = new Element[0];
- // index must be > 0 otherwise no insert would have happened.
- boolean isLeaf = (e.getElement(index - 1).isLeaf());
- if (isLeaf) {
- index--;
- }
- Element[] removed = new Element[count];
- for (int counter = 0; counter < count; counter++) {
- removed[counter] = e.getElement(counter + index);
- }
- DefaultDocumentEvent dde = new DefaultDocumentEvent
- (start - 1, end - start + 1, DocumentEvent.EventType.REMOVE);
- dde.addEdit(new ElementEdit(e, index, removed, added));
- ((AbstractDocument.BranchElement)e).replace(index, removed.length,
- added);
- UndoableEdit u;
- if (isLeaf) {
- // start - 1 was a leaf, simply remove it.
- u = getContent().remove(start - 1, end - start);
- }
- 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);
- }
- Element[] oRemoved = new Element[1];
- Element[] oAdded = new Element[1];
- oRemoved[0] = newLineE;
- AttributeSet attrs = newLineE.getAttributes();
- newLineE = newLineE.getParentElement();
- u = getContent().remove(start - 1, end - start);
- // Now recreate the Element for start - 1.
- oAdded[0] = createLeafElement(newLineE, attrs, start - 1, start);
- ((AbstractDocument.BranchElement)newLineE).
- replace(newLineE.getElementCount() - 1, 1, oAdded);
- dde.addEdit(new ElementEdit(newLineE, newLineE.getElementCount() -
- 1, oRemoved, oAdded));
- }
- if (u != null) {
- dde.addEdit(u);
- }
- postRemoveUpdate(dde);
- dde.end();
- fireRemoveUpdate(dde);
- if (u != null) {
- fireUndoableEditUpdate(new UndoableEditEvent(this, dde));
- }
- }
- /**
- * 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);
- }
- /*
- * state defines whether the document is a frame document
- * or not.
- */
- private boolean frameDocument = false;
- private boolean preservesUnknownTags = true;
- /*
- * Used to store a button group for radio buttons in
- * a form.
- */
- private ButtonGroup radioButtonGroup;
- /**
- * Document property for the number of tokens to buffer
- * before building an element subtree to represent them.
- */
- static final String TokenThreshold = "token threshold";
- /**
- * 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;
- /**
- * 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;
- 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 {
- /**
- * Fetch the attributes for this tag.
- */
- public abstract AttributeSet getAttributes();
- /**
- * Start of the range for which the current occurence of
- * the tag is defined and has the same attributes.
- */
- public abstract int getStartOffset();
- /**
- * End of the range for which the current occurence of
- * the tag is defined and has the same attributes.
- */
- public abstract int getEndOffset();
- /**
- * Move the iterator forward to the next occurence
- * of the tag it represents.
- */
- public abstract void next();
- /**
- * Indicates if the iterator is currently
- * representing an occurence of a tag. If
- * false there are no more tags for this iterator.
- */
- 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();
- }
- /**
- * Fetches the attributes for this tag.
- */
- public AttributeSet getAttributes() {
- Element elem = pos.current();
- if (elem != null) {
- AttributeSet a = (AttributeSet)
- elem.getAttributes().getAttribute(tag);
- return a;
- }
- return null;
- }
- /**
- * Returns the start of the range for which the current occurence of
- * the tag is defined and has the same attributes.
- */
- 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 occurence of
- * the tag is defined and has the same attributes.
- */
- public int getEndOffset() {
- return endOffset;
- }
- /**
- * Moves the iterator forward to the next occurence
- * 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)) {
- // we found the next one
- setEndOffset();
- break;
- }
- }
- }
- }
- /**
- * Returns the type of tag this iterator represents.
- */
- public HTML.Tag getTag() {
- return tag;
- }
- public boolean isValid() {
- return (pos.current() != null);
- }
- /**
- * Moves the given iterator to the next leaf element.
- */
- 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 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</code>)
- * that describe how to handle the action. The idea behind
- * the actions provided is that the most natural text editing
- * operations can be provided if the element structure boils
- * down to paragraphs with runs of some kind of style
- * in them. Some things are more naturally specified
- * structurally, so arbitrary structure should be allowed
- * above the paragraphs, but will need to be edited with structural
- * actions. The implication of this is that some of the
- * HTML elements specified in the stream being parsed will
- * be collapsed into attributes, and in some cases paragraphs
- * will be synthesized. When HTML elements have been
- * converted to attributes, the attribute key will be of
- * type HTML.Tag, and the value will be of type AttributeSet
- * so that no information is lost. This enables many of the
- * existing actions to work so that the user can type input,
- * hit the return key, backspace, delete, etc and have a
- * reasonable result. Selections can be created, and attributes
- * applied or removed, etc. With this in mind, the work done
- * by the reader can be categorized into the following kinds
- * of tasks:
- * <dl>
- * <dt>Block
- * <dd>Build the structure like it's specified in the stream.
- * This produces elements that contain other elements.
- * <dt>Paragraph
- * <dd>Like block except that it's expected that the element
- * will be used with a paragraph view so a paragraph element
- * won't need to be synthesized.
- * <dt>Character
- * <dd>Contribute the element as an attribute that will start
- * and stop at arbitrary text locations. This will ultimately
- * be mixed into a run of text, with all of the currently
- * flattened HTML character elements.
- * <dt>Special
- * <dd>Produce an embedded graphical element.
- * <dt>Form
- * <dd>Produce an element that is like the embedded graphical
- * element, except that it also has a component model associated
- * with it.
- * <dt>Hidden
- * <dd>Create an element that is hidden from view when the
- * document is being viewed read-only, and visible when the
- * document is being edited. This is useful to keep the
- * model from losing information, and used to store things
- * like comments and unrecognized tags.
- *
- * </dl>
- * <p>
- * Currently, <APPLET>, <PARAM>, <MAP>, <AREA>, <LINK>,
- * <SCRIPT> and <STYLE> are unsupported.
- *
- * <p>
- * The assignment of the actions described is shown in the
- * following table for the tags defined in <code>HTML.Tag</code>.
- * <table>
- * <tr><td><code>HTML.Tag.A</code> <td>CharacterAction
- * <tr><td><code>HTML.Tag.ADDRESS</code> <td>CharacterAction
- * <tr><td><code>HTML.Tag.APPLET</code> <td>HiddenAction
- * <tr><td><code>HTML.Tag.AREA</code> <td>AreaAction
- * <tr><td><code>HTML.Tag.B</code> <td>CharacterAction
- * <tr><td><code>HTML.Tag.BASE</code> <td>BaseAction
- * <tr><td><code>HTML.Tag.BASEFONT</code> <td>CharacterAction
- * <tr><td&g