| /* |
| * Copyright 1997-2008 Sun Microsystems, Inc. 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. Sun designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, |
| * CA 95054 USA or visit www.sun.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; |
| |
| 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 name 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<String, ButtonGroup> radioButtonGroupsMap; |
| |
| /** |
| * Document property for the number of tokens to buffer |
| * before building an element subtree to represent them. |
| */ |
| static final String TokenThreshold = "token threshold"; |
| |
| private static final int MaxThreshold = 10000; |
| |
| private static final int StepThreshold = 5; |
| |
| |
| /** |
| * Document property key value. The value for the key will be a Vector |
| * of Strings that are comments not found in the body. |
| */ |
| public static final String AdditionalComments = "AdditionalComments"; |
| |
| /** |
| * Document property key value. The value for the key will be a |
| * String indicating the default type of stylesheet links. |
| */ |
| /* public */ static final String StyleType = "StyleType"; |
| |
| /** |
| * The location to resolve relative URLs against. By |
| * default this will be the document's URL if the document |
| * was loaded from a URL. If a base tag is found and |
| * can be parsed, it will be used as the base location. |
| */ |
| URL base; |
| |
| /** |
| * does the document have base tag |
| */ |
| boolean hasBaseTag = false; |
| |
| /** |
| * BASE tag's TARGET attribute value |
| */ |
| private String baseTarget = null; |
| |
| /** |
| * The parser that is used when inserting html into the existing |
| * document. |
| */ |
| private HTMLEditorKit.Parser parser; |
| |
| /** |
| * Used for inserts when a null AttributeSet is supplied. |
| */ |
| private static AttributeSet contentAttributeSet; |
| |
| /** |
| * Property Maps are registered under, will be a Hashtable. |
| */ |
| static String MAP_PROPERTY = "__MAP__"; |
| |
| private static char[] NEWLINE; |
| 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 = 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<HTML.Tag, TagAction>(57); |
| TagAction na = new TagAction(); |
| TagAction ba = new BlockAction(); |
| TagAction pa = new ParagraphAction(); |
| TagAction ca = new CharacterAction(); |
| TagAction sa = new SpecialAction(); |
| TagAction fa = new FormAction(); |
| TagAction ha = new HiddenAction(); |
| TagAction conv = new ConvertAction(); |
| |
| // register handlers for the well known tags |
| tagMap.put(HTML.Tag.A, new AnchorAction()); |
| tagMap.put(HTML.Tag.ADDRESS, ca); |
| tagMap.put(HTML.Tag.APPLET, ha); |
| tagMap.put(HTML.Tag.AREA, new AreaAction()); |
| tagMap.put(HTML.Tag.B, conv); |
| tagMap.put(HTML.Tag.BASE, new BaseAction()); |
| tagMap.put(HTML.Tag.BASEFONT, ca); |
| tagMap.put(HTML.Tag.BIG, ca); |
| tagMap.put(HTML.Tag.BLOCKQUOTE, ba); |
| tagMap.put(HTML.Tag.BODY, ba); |
| tagMap.put(HTML.Tag.BR, sa); |
| tagMap.put(HTML.Tag.CAPTION, ba); |
| tagMap.put(HTML.Tag.CENTER, ba); |
| tagMap.put(HTML.Tag.CITE, ca); |
| tagMap.put(HTML.Tag.CODE, ca); |
| tagMap.put(HTML.Tag.DD, ba); |
| tagMap.put(HTML.Tag.DFN, ca); |
| tagMap.put(HTML.Tag.DIR, ba); |
| tagMap.put(HTML.Tag.DIV, ba); |
| tagMap.put(HTML.Tag.DL, ba); |
| tagMap.put(HTML.Tag.DT, pa); |
| tagMap.put(HTML.Tag.EM, ca); |
| tagMap.put(HTML.Tag.FONT, conv); |
| tagMap.put(HTML.Tag.FORM, new FormTagAction()); |
| tagMap.put(HTML.Tag.FRAME, sa); |
| tagMap.put(HTML.Tag.FRAMESET, ba); |
| tagMap.put(HTML.Tag.H1, pa); |
| tagMap.put(HTML.Tag.H2, pa); |
| tagMap.put(HTML.Tag.H3, pa); |
| tagMap.put(HTML.Tag.H4, pa); |
| tagMap.put(HTML.Tag.H5, pa); |
| tagMap.put(HTML.Tag.H6, pa); |
| tagMap.put(HTML.Tag.HEAD, new HeadAction()); |
| tagMap.put(HTML.Tag.HR, sa); |
| tagMap.put(HTML.Tag.HTML, ba); |
| tagMap.put(HTML.Tag.I, conv); |
| tagMap.put(HTML.Tag.IMG, sa); |
| tagMap.put(HTML.Tag.INPUT, fa); |
| tagMap.put(HTML.Tag.ISINDEX, new IsindexAction()); |
| tagMap.put(HTML.Tag.KBD, ca); |
| tagMap.put(HTML.Tag.LI, ba); |
| tagMap.put(HTML.Tag.LINK, new LinkAction()); |
| tagMap.put(HTML.Tag.MAP, new MapAction()); |
| tagMap.put(HTML.Tag.MENU, ba); |
| tagMap.put(HTML.Tag.META, new MetaAction()); |
| tagMap.put(HTML.Tag.NOBR, ca); |
| tagMap.put(HTML.Tag.NOFRAMES, ba); |
| tagMap.put(HTML.Tag.OBJECT, sa); |
| tagMap.put(HTML.Tag.OL, ba); |
| tagMap.put(HTML.Tag.OPTION, fa); |
| tagMap.put(HTML.Tag.P, pa); |
| tagMap.put(HTML.Tag.PARAM, new ObjectAction()); |
| tagMap.put(HTML.Tag.PRE, new PreAction()); |
| tagMap.put(HTML.Tag.SAMP, ca); |
| tagMap.put(HTML.Tag.SCRIPT, ha); |
| tagMap.put(HTML.Tag.SELECT, fa); |
| tagMap.put(HTML.Tag.SMALL, ca); |
| tagMap.put(HTML.Tag.SPAN, ca); |
| tagMap.put(HTML.Tag.STRIKE, conv); |
| tagMap.put(HTML.Tag.S, ca); |
| tagMap.put(HTML.Tag.STRONG, ca); |
| tagMap.put(HTML.Tag.STYLE, new StyleAction()); |
| tagMap.put(HTML.Tag.SUB, conv); |
| tagMap.put(HTML.Tag.SUP, conv); |
| tagMap.put(HTML.Tag.TABLE, ba); |
| tagMap.put(HTML.Tag.TD, ba); |
| tagMap.put(HTML.Tag.TEXTAREA, fa); |
| tagMap.put(HTML.Tag.TH, ba); |
| tagMap.put(HTML.Tag.TITLE, new TitleAction()); |
| tagMap.put(HTML.Tag.TR, ba); |
| tagMap.put(HTML.Tag.TT, ca); |
| tagMap.put(HTML.Tag.U, conv); |
| tagMap.put(HTML.Tag.UL, ba); |
| tagMap.put(HTML.Tag.VAR, ca); |
| |
| if (insertTag != null) { |
| this.insertTag = insertTag; |
| this.popDepth = popDepth; |
| this.pushDepth = pushDepth; |
| this.insertInsertTag = insertInsertTag; |
| foundInsertTag = false; |
| } |
| else { |
| foundInsertTag = true; |
| } |
| if (insertAfterImplied) { |
| this.popDepth = popDepth; |
| this.pushDepth = pushDepth; |
| this.insertAfterImplied = true; |
| foundInsertTag = false; |
| midInsert = false; |
| this.insertInsertTag = true; |
| this.wantsTrailingNewline = wantsTrailingNewline; |
| } |
| else { |
| midInsert = (!emptyDocument && insertTag == null); |
| if (midInsert) { |
| generateEndsSpecsForMidInsert(); |
| } |
| } |
| |
| /** |
| * This block initializes the <code>inParagraph</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; |
| Element[] added; |
| Element[] removed; |
| int index; |
| // Remove the fake second body. |
| added = new Element[0]; |
| removed = new Element[1]; |
| index = pPath[0].getElementIndex(length); |
| removed[0] = pPath[0].getElement(index); |
| ((BranchElement)pPath[0]).replace(index, 1, added); |
| ElementEdit firstEdit = new ElementEdit(pPath[0], index, |
| removed, added); |
| |
| // Insert a new element to represent the end that the |
| // second body was representing. |
| SimpleAttributeSet sas = new SimpleAttributeSet(); |
| sas.addAttribute(StyleConstants.NameAttribute, |
| HTML.Tag.CONTENT); |
| sas.addAttribute(IMPLIED_CR, Boolean.TRUE); |
| added = new Element[1]; |
| added[0] = createLeafElement(pPath[pLength - 1], |
| sas, length, length + 1); |
| index = pPath[pLength - 1].getElementCount(); |
| ((BranchElement)pPath[pLength - 1]).replace(index, 0, |
| added); |
| event = new DefaultDocumentEvent(length, 1, |
| DocumentEvent.EventType.CHANGE); |
| event.addEdit(new ElementEdit(pPath[pLength - 1], |
| index, new Element[0], added)); |
| event.addEdit(firstEdit); |
| event.end(); |
| fireChangedUpdate(event); |
| fireUndoableEditUpdate(new UndoableEditEvent(this, event)); |
| |
| if (lastText.equals("\n")) { |
| // We now have two \n's, one part of the Document. |
| // We need to remove one |
| event = new DefaultDocumentEvent(length - 1, 1, |
| DocumentEvent.EventType.REMOVE); |
| removeUpdate(event); |
| UndoableEdit u = getContent().remove(length - 1, 1); |
| if (u != null) { |
| event.addEdit(u); |
| } |
| postRemoveUpdate(event); |
| // Mark the edit as done. |
| event.end(); |
| fireRemoveUpdate(event); |
| fireUndoableEditUpdate(new UndoableEditEvent( |
| this, event)); |
| } |
| } |
| } |
| catch (BadLocationException ble) { |
| } |
| finally { |
| releaseLock(); |
| } |
| } |
| |
| private Element[] getPathTo(int offset) { |
| Stack<Element> elements = new Stack<Element>(); |
| Element e = getDefaultRootElement(); |
| int index; |
| while (!e.isLeaf()) { |
| elements.push(e); |
| e = e.getElement(e.getElementIndex(offset)); |
| } |
| Element[] retValue = new Element[elements.size()]; |
| elements.copyInto(retValue); |
| return retValue; |
| } |
| |
| // -- HTMLEditorKit.ParserCallback methods -------------------- |
| |
| /** |
| * The last method called on the reader. It allows |
| * any pending changes to be flushed into the document. |
| * Since this is currently loading synchronously, the entire |
| * set of changes are pushed in at this point. |
| */ |
| public void flush() throws BadLocationException { |
| if (emptyDocument && !insertAfterImplied) { |
| if (HTMLDocument.this.getLength() > 0 || |
| parseBuffer.size() > 0) { |
| flushBuffer(true); |
| adjustEndElement(); |
| } |
| // We won't insert when |
| } |
| else { |
| flushBuffer(true); |
| } |
| } |
| |
| /** |
| * Called by the parser to indicate a block of text was |
| * encountered. |
| */ |
| public void handleText(char[] data, int pos) { |
| if (receivedEndHTML || (midInsert && !inBody)) { |
| return; |
| } |
| |
| // see if complex glyph layout support is needed |
| if(HTMLDocument.this.getProperty(I18NProperty).equals( Boolean.FALSE ) ) { |
| // if a default direction of right-to-left has been specified, |
| // we want complex layout even if the text is all left to right. |
| Object d = getProperty(TextAttribute.RUN_DIRECTION); |
| if ((d != null) && (d.equals(TextAttribute.RUN_DIRECTION_RTL))) { |
| HTMLDocument.this.putProperty( I18NProperty, Boolean.TRUE); |
| } else { |
| if (SwingUtilities2.isComplexLayout(data, 0, data.length)) { |
| HTMLDocument.this.putProperty( I18NProperty, Boolean.TRUE); |
| } |
| } |
| } |
| |
| if (inTextArea) { |
| textAreaContent(data); |
| } else if (inPre) { |
| preContent(data); |
| } else if (inTitle) { |
| putProperty(Document.TitleProperty, new String(data)); |
| } else if (option != null) { |
| option.setLabel(new String(data)); |
| } else if (inStyle) { |
| if (styles != null) { |
| styles.addElement(new String(data)); |
| } |
| } else if (inBlock > 0) { |
| if (!foundInsertTag && insertAfterImplied) { |
| // Assume content should be added. |
| foundInsertTag(false); |
| foundInsertTag = true; |
| inParagraph = impliedP = true; |
| } |
| if (data.length >= 1) { |
| addContent(data, 0, data.length); |
| } |
| } |
| } |
| |
| /** |
| * Callback from the parser. Route to the appropriate |
| * handler for the tag. |
| */ |
| public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) { |
| if (receivedEndHTML) { |
| return; |
| } |
| if (midInsert && !inBody) { |
| if (t == HTML.Tag.BODY) { |
| inBody = true; |
| // Increment inBlock since we know we are in the body, |
| // this is needed incase an implied-p is needed. If |
| // inBlock isn't incremented, and an implied-p is |
| // encountered, addContent won't be called! |
| inBlock++; |
| } |
| return; |
| } |
| if (!inBody && t == HTML.Tag.BODY) { |
| inBody = true; |
| } |
| if (isStyleCSS && a.isDefined(HTML.Attribute.STYLE)) { |
| // Map the style attributes. |
| String decl = (String)a.getAttribute(HTML.Attribute.STYLE); |
| a.removeAttribute(HTML.Attribute.STYLE); |
| styleAttributes = getStyleSheet().getDeclaration(decl); |
| a.addAttributes(styleAttributes); |
| } |
| else { |
| styleAttributes = null; |
| } |
| TagAction action = tagMap.get(t); |
| |
| if (action != null) { |
| action.start(t, a); |
| } |
| } |
| |
| public void handleComment(char[] data, int pos) { |
| if (receivedEndHTML) { |
| addExternalComment(new String(data)); |
| return; |
| } |
| if (inStyle) { |
| if (styles != null) { |
| styles.addElement(new String(data)); |
| } |
| } |
| else if (getPreservesUnknownTags()) { |
| if (inBlock == 0 && (foundInsertTag || |
| insertTag != HTML.Tag.COMMENT)) { |
| // Comment outside of body, will not be able to show it, |
| // but can add it as a property on the Document. |
| addExternalComment(new String(data)); |
| return; |
| } |
| SimpleAttributeSet sas = new SimpleAttributeSet(); |
| sas.addAttribute(HTML.Attribute.COMMENT, new String(data)); |
| addSpecialElement(HTML.Tag.COMMENT, sas); |
| } |
| |
| TagAction action = tagMap.get(HTML.Tag.COMMENT); |
| if (action != null) { |
| action.start(HTML.Tag.COMMENT, new SimpleAttributeSet()); |
| action.end(HTML.Tag.COMMENT); |
| } |
| } |
| |
| /** |
| * Adds the comment <code>comment</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 = tagMap.get(t); |
| if (action != null) { |
| action.end(t); |
| } |
| } |
| |
| /** |
| * Callback from the parser. Route to the appropriate |
| * handler for the tag. |
| */ |
| public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) { |
| if (receivedEndHTML || (midInsert && !inBody)) { |
| return; |
| } |
| |
| if (isStyleCSS && a.isDefined(HTML.Attribute.STYLE)) { |
| // Map the style attributes. |
| String decl = (String)a.getAttribute(HTML.Attribute.STYLE); |
| a.removeAttribute(HTML.Attribute.STYLE); |
| styleAttributes = getStyleSheet().getDeclaration(decl); |
| a.addAttributes(styleAttributes); |
| } |
| else { |
| styleAttributes = null; |
| } |
| |
| TagAction action = tagMap.get(t); |
| if (action != null) { |
| action.start(t, a); |
| action.end(t); |
| } |
| else if (getPreservesUnknownTags()) { |
| // unknown tag, only add if should preserve it. |
| addSpecialElement(t, a); |
| } |
| } |
| |
| /** |
| * This is invoked after the stream has been parsed, but before |
| * <code>flush</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<String, ButtonGroup>(); |
| } |
| |
| public void end(HTML.Tag t) { |
| super.end(t); |
| // reset the button group to null since |
| // the form has ended. |
| radioButtonGroupsMap = null; |
| } |
| } |
| |
| |
| public class ParagraphAction extends BlockAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| super.start(t, a); |
| inParagraph = true; |
| } |
| |
| public void end(HTML.Tag t) { |
| super.end(t); |
| inParagraph = false; |
| } |
| } |
| |
| public class SpecialAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| addSpecialElement(t, a); |
| } |
| |
| } |
| |
| public class IsindexAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| blockOpen(HTML.Tag.IMPLIED, new SimpleAttributeSet()); |
| addSpecialElement(t, a); |
| blockClose(HTML.Tag.IMPLIED); |
| } |
| |
| } |
| |
| |
| public class HiddenAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| addSpecialElement(t, a); |
| } |
| |
| public void end(HTML.Tag t) { |
| if (!isEmpty(t)) { |
| MutableAttributeSet a = new SimpleAttributeSet(); |
| a.addAttribute(HTML.Attribute.ENDTAG, "true"); |
| addSpecialElement(t, a); |
| } |
| } |
| |
| boolean isEmpty(HTML.Tag t) { |
| if (t == HTML.Tag.APPLET || |
| t == HTML.Tag.SCRIPT) { |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| |
| /** |
| * Subclass of HiddenAction to set the content type for style sheets, |
| * and to set the name of the default style sheet. |
| */ |
| class MetaAction extends HiddenAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| Object equiv = a.getAttribute(HTML.Attribute.HTTPEQUIV); |
| if (equiv != null) { |
| equiv = ((String)equiv).toLowerCase(); |
| if (equiv.equals("content-style-type")) { |
| String value = (String)a.getAttribute |
| (HTML.Attribute.CONTENT); |
| setDefaultStyleSheetType(value); |
| isStyleCSS = "text/css".equals |
| (getDefaultStyleSheetType()); |
| } |
| else if (equiv.equals("default-style")) { |
| defaultStyle = (String)a.getAttribute |
| (HTML.Attribute.CONTENT); |
| } |
| } |
| super.start(t, a); |
| } |
| |
| boolean isEmpty(HTML.Tag t) { |
| return true; |
| } |
| } |
| |
| |
| /** |
| * End if overridden to create the necessary stylesheets that |
| * are referenced via the link tag. It is done in this manner |
| * as the meta tag can be used to specify an alternate style sheet, |
| * and is not guaranteed to come before the link tags. |
| */ |
| class HeadAction extends BlockAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| inHead = true; |
| // This check of the insertTag is put in to avoid considering |
| // the implied-p that is generated for the head. This allows |
| // inserts for HR to work correctly. |
| if ((insertTag == null && !insertAfterImplied) || |
| (insertTag == HTML.Tag.HEAD) || |
| (insertAfterImplied && |
| (foundInsertTag || !a.isDefined(IMPLIED)))) { |
| super.start(t, a); |
| } |
| } |
| |
| public void end(HTML.Tag t) { |
| inHead = inStyle = false; |
| // See if there is a StyleSheet to link to. |
| if (styles != null) { |
| boolean isDefaultCSS = isStyleCSS; |
| for (int counter = 0, maxCounter = styles.size(); |
| counter < maxCounter;) { |
| Object value = styles.elementAt(counter); |
| if (value == HTML.Tag.LINK) { |
| handleLink((AttributeSet)styles. |
| elementAt(++counter)); |
| counter++; |
| } |
| else { |
| // Rule. |
| // First element gives type. |
| String type = (String)styles.elementAt(++counter); |
| boolean isCSS = (type == null) ? isDefaultCSS : |
| type.equals("text/css"); |
| while (++counter < maxCounter && |
| (styles.elementAt(counter) |
| instanceof String)) { |
| if (isCSS) { |
| addCSSRules((String)styles.elementAt |
| (counter)); |
| } |
| } |
| } |
| } |
| } |
| if ((insertTag == null && !insertAfterImplied) || |
| insertTag == HTML.Tag.HEAD || |
| (insertAfterImplied && foundInsertTag)) { |
| super.end(t); |
| } |
| } |
| |
| boolean isEmpty(HTML.Tag t) { |
| return false; |
| } |
| |
| private void handleLink(AttributeSet attr) { |
| // Link. |
| String type = (String)attr.getAttribute(HTML.Attribute.TYPE); |
| if (type == null) { |
| type = getDefaultStyleSheetType(); |
| } |
| // Only choose if type==text/css |
| // Select link if rel==stylesheet. |
| // Otherwise if rel==alternate stylesheet and |
| // title matches default style. |
| if (type.equals("text/css")) { |
| String rel = (String)attr.getAttribute(HTML.Attribute.REL); |
| String title = (String)attr.getAttribute |
| (HTML.Attribute.TITLE); |
| String media = (String)attr.getAttribute |
| (HTML.Attribute.MEDIA); |
| if (media == null) { |
| media = "all"; |
| } |
| else { |
| media = media.toLowerCase(); |
| } |
| if (rel != null) { |
| rel = rel.toLowerCase(); |
| if ((media.indexOf("all") != -1 || |
| media.indexOf("screen") != -1) && |
| (rel.equals("stylesheet") || |
| (rel.equals("alternate stylesheet") && |
| title.equals(defaultStyle)))) { |
| linkCSSStyleSheet((String)attr.getAttribute |
| (HTML.Attribute.HREF)); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * A subclass to add the AttributeSet to styles if the |
| * attributes contains an attribute for 'rel' with value |
| * 'stylesheet' or 'alternate stylesheet'. |
| */ |
| class LinkAction extends HiddenAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| String rel = (String)a.getAttribute(HTML.Attribute.REL); |
| if (rel != null) { |
| rel = rel.toLowerCase(); |
| if (rel.equals("stylesheet") || |
| rel.equals("alternate stylesheet")) { |
| if (styles == null) { |
| styles = new Vector<Object>(3); |
| } |
| styles.addElement(t); |
| styles.addElement(a.copyAttributes()); |
| } |
| } |
| super.start(t, a); |
| } |
| } |
| |
| class MapAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| lastMap = new Map((String)a.getAttribute(HTML.Attribute.NAME)); |
| addMap(lastMap); |
| } |
| |
| public void end(HTML.Tag t) { |
| } |
| } |
| |
| |
| class AreaAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| if (lastMap != null) { |
| lastMap.addArea(a.copyAttributes()); |
| } |
| } |
| |
| public void end(HTML.Tag t) { |
| } |
| } |
| |
| |
| class StyleAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| if (inHead) { |
| if (styles == null) { |
| styles = new Vector<Object>(3); |
| } |
| styles.addElement(t); |
| styles.addElement(a.getAttribute(HTML.Attribute.TYPE)); |
| inStyle = true; |
| } |
| } |
| |
| public void end(HTML.Tag t) { |
| inStyle = false; |
| } |
| |
| boolean isEmpty(HTML.Tag t) { |
| return false; |
| } |
| } |
| |
| |
| public class PreAction extends BlockAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet attr) { |
| inPre = true; |
| blockOpen(t, attr); |
| attr.addAttribute(CSS.Attribute.WHITE_SPACE, "pre"); |
| blockOpen(HTML.Tag.IMPLIED, attr); |
| } |
| |
| public void end(HTML.Tag t) { |
| blockClose(HTML.Tag.IMPLIED); |
| // set inPre to false after closing, so that if a newline |
| // is added it won't generate a blockOpen. |
| inPre = false; |
| blockClose(t); |
| } |
| } |
| |
| public class CharacterAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet attr) { |
| pushCharacterStyle(); |
| if (!foundInsertTag) { |
| // Note that the third argument should really be based off |
| // inParagraph and impliedP. If we're wrong (that is |
| // insertTagDepthDelta shouldn't be changed), we'll end up |
| // removing an extra EndSpec, which won't matter anyway. |
| boolean insert = canInsertTag(t, attr, false); |
| if (foundInsertTag) { |
| if (!inParagraph) { |
| inParagraph = impliedP = true; |
| } |
| } |
| if (!insert) { |
| return; |
| } |
| } |
| if (attr.isDefined(IMPLIED)) { |
| attr.removeAttribute(IMPLIED); |
| } |
| charAttr.addAttribute(t, attr.copyAttributes()); |
| if (styleAttributes != null) { |
| charAttr.addAttributes(styleAttributes); |
| } |
| } |
| |
| public void end(HTML.Tag t) { |
| popCharacterStyle(); |
| } |
| } |
| |
| /** |
| * Provides conversion of HTML tag/attribute |
| * mappings that have a corresponding StyleConstants |
| * and CSS mapping. The conversion is to CSS attributes. |
| */ |
| class ConvertAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet attr) { |
| pushCharacterStyle(); |
| if (!foundInsertTag) { |
| // Note that the third argument should really be based off |
| // inParagraph and impliedP. If we're wrong (that is |
| // insertTagDepthDelta shouldn't be changed), we'll end up |
| // removing an extra EndSpec, which won't matter anyway. |
| boolean insert = canInsertTag(t, attr, false); |
| if (foundInsertTag) { |
| if (!inParagraph) { |
| inParagraph = impliedP = true; |
| } |
| } |
| if (!insert) { |
| return; |
| } |
| } |
| if (attr.isDefined(IMPLIED)) { |
| attr.removeAttribute(IMPLIED); |
| } |
| if (styleAttributes != null) { |
| charAttr.addAttributes(styleAttributes); |
| } |
| // We also need to add attr, otherwise we lose custom |
| // attributes, including class/id for style lookups, and |
| // further confuse style lookup (doesn't have tag). |
| charAttr.addAttribute(t, attr.copyAttributes()); |
| StyleSheet sheet = getStyleSheet(); |
| if (t == HTML.Tag.B) { |
| sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_WEIGHT, "bold"); |
| } else if (t == HTML.Tag.I) { |
| sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_STYLE, "italic"); |
| } else if (t == HTML.Tag.U) { |
| Object v = charAttr.getAttribute(CSS.Attribute.TEXT_DECORATION); |
| String value = "underline"; |
| value = (v != null) ? value + "," + v.toString() : value; |
| sheet.addCSSAttribute(charAttr, CSS.Attribute.TEXT_DECORATION, value); |
| } else if (t == HTML.Tag.STRIKE) { |
| Object v = charAttr.getAttribute(CSS.Attribute.TEXT_DECORATION); |
| String value = "line-through"; |
| value = (v != null) ? value + "," + v.toString() : value; |
| sheet.addCSSAttribute(charAttr, CSS.Attribute.TEXT_DECORATION, value); |
| } else if (t == HTML.Tag.SUP) { |
| Object v = charAttr.getAttribute(CSS.Attribute.VERTICAL_ALIGN); |
| String value = "sup"; |
| value = (v != null) ? value + "," + v.toString() : value; |
| sheet.addCSSAttribute(charAttr, CSS.Attribute.VERTICAL_ALIGN, value); |
| } else if (t == HTML.Tag.SUB) { |
| Object v = charAttr.getAttribute(CSS.Attribute.VERTICAL_ALIGN); |
| String value = "sub"; |
| value = (v != null) ? value + "," + v.toString() : value; |
| sheet.addCSSAttribute(charAttr, CSS.Attribute.VERTICAL_ALIGN, value); |
| } else if (t == HTML.Tag.FONT) { |
| String color = (String) attr.getAttribute(HTML.Attribute.COLOR); |
| if (color != null) { |
| sheet.addCSSAttribute(charAttr, CSS.Attribute.COLOR, color); |
| } |
| String face = (String) attr.getAttribute(HTML.Attribute.FACE); |
| if (face != null) { |
| sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_FAMILY, face); |
| } |
| String size = (String) attr.getAttribute(HTML.Attribute.SIZE); |
| if (size != null) { |
| sheet.addCSSAttributeFromHTML(charAttr, CSS.Attribute.FONT_SIZE, size); |
| } |
| } |
| } |
| |
| public void end(HTML.Tag t) { |
| popCharacterStyle(); |
| } |
| |
| } |
| |
| class AnchorAction extends CharacterAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet attr) { |
| // set flag to catch empty anchors |
| emptyAnchor = true; |
| super.start(t, attr); |
| } |
| |
| public void end(HTML.Tag t) { |
| if (emptyAnchor) { |
| // if the anchor was empty it was probably a |
| // named anchor point and we don't want to throw |
| // it away. |
| char[] one = new char[1]; |
| one[0] = '\n'; |
| addContent(one, 0, 1); |
| } |
| super.end(t); |
| } |
| } |
| |
| class TitleAction extends HiddenAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet attr) { |
| inTitle = true; |
| super.start(t, attr); |
| } |
| |
| public void end(HTML.Tag t) { |
| inTitle = false; |
| super.end(t); |
| } |
| |
| boolean isEmpty(HTML.Tag t) { |
| return false; |
| } |
| } |
| |
| |
| class BaseAction extends TagAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet attr) { |
| String href = (String) attr.getAttribute(HTML.Attribute.HREF); |
| if (href != null) { |
| try { |
| URL newBase = new URL(base, href); |
| setBase(newBase); |
| hasBaseTag = true; |
| } catch (MalformedURLException ex) { |
| } |
| } |
| baseTarget = (String) attr.getAttribute(HTML.Attribute.TARGET); |
| } |
| } |
| |
| class ObjectAction extends SpecialAction { |
| |
| public void start(HTML.Tag t, MutableAttributeSet a) { |
| if (t == HTML.Tag.PARAM) { |
| addParameter(a); |
| } else { |
| super.start(t, a); |
| } |
| } |
| |
| public void end(HTML.Tag t) { |
| if (t != HTML.Tag.PARAM) { |
| super.end(t); |
| } |
| } |
| |
| void addParameter(AttributeSet a) { |
| String name = (String) a.getAttribute(HTML.Attribute.NAME); |
| String value = (String) a.getAttribute(HTML.Attribute.VALUE); |
| if ((name != null) && (value != null)) { |
| ElementSpec objSpec = parseBuffer.lastElement(); |
| MutableAttributeSet objAttr = (MutableAttributeSet) objSpec.getAttributes(); |
| objAttr.addAttribute(name, value); |
| } |
| } |
| } |
| |
| /** |
| * Action to support forms by building all of the elements |
| * used to represent form controls. This will process |
| * the <INPUT>, <TEXTAREA>, <SELECT>, |
| * and <OPTION> tags. The element created by |
| * this action is expected to have the attribute |
| * <code>StyleConstants.ModelAttribute</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 = 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<String, ButtonGroup>(); |
| } |
| ButtonGroup radioButtonGroup = 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) ? |
| 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 && |
| 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 (parseBuffer.elementAt(0).getType() == |
| ElementSpec.ContentType) { |
| index++; |
| } |
| } |
| index += (popDepth + pushDepth); |
| int cCount = 0; |
| int cStart = index; |
| while (index < size && parseBuffer.elementAt |
| (index).getType() == ElementSpec.ContentType) { |
| index++; |
| cCount++; |
| } |
| if (cCount > 1) { |
| while (index < size && parseBuffer.elementAt |
| (index).getType() == ElementSpec.EndTagType) { |
| index++; |
| } |
| if (index == size) { |
| char[] lastText = 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 = 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; |
| 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<Object> 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<ElementSpec>(); |
| protected MutableAttributeSet charAttr = new TaggedAttributeSet(); |
| Stack<AttributeSet> charAttrStack = new Stack<AttributeSet>(); |
| Hashtable<HTML.Tag, TagAction> 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); |
| } |
| } |
| } |
| } |