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