/* |
|
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. |
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
* |
|
* This code is free software; you can redistribute it and/or modify it |
|
* under the terms of the GNU General Public License version 2 only, as |
|
* published by the Free Software Foundation. Oracle designates this |
|
* particular file as subject to the "Classpath" exception as provided |
|
* by Oracle in the LICENSE file that accompanied this code. |
|
* |
|
* This code is distributed in the hope that it will be useful, but WITHOUT |
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
* version 2 for more details (a copy is included in the LICENSE file that |
|
* accompanied this code). |
|
* |
|
* You should have received a copy of the GNU General Public License version |
|
* 2 along with this work; if not, write to the Free Software Foundation, |
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
* |
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
* or visit www.oracle.com if you need additional information or have any |
|
* questions. |
|
*/ |
|
package javax.swing.text.html; |
|
import java.awt.font.TextAttribute; |
|
import java.util.*; |
|
import java.net.URL; |
|
import java.net.MalformedURLException; |
|
import java.io.*; |
|
import javax.swing.*; |
|
import javax.swing.event.*; |
|
import javax.swing.text.*; |
|
import javax.swing.undo.*; |
|
import sun.swing.SwingUtilities2; |
|
import static sun.swing.SwingUtilities2.IMPLIED_CR; |
|
/** |
|
* A document that models HTML. The purpose of this model is to |
|
* support both browsing and editing. As a result, the structure |
|
* described by an HTML document is not exactly replicated by default. |
|
* The element structure that is modeled by default, is built by the |
|
* class <code>HTMLDocument.HTMLReader</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 style="white-space:nowrap"> |
|
* <div style="background-color: silver;"> |
|
* <p>Paragraph 1</p> |
|
* <p>Paragraph 2</p> |
|
* </div> |
|
* </td> |
|
* <!--insertAfterStart--> |
|
* <td style="white-space: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 style="white-space: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 style="white-space: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 style="white-space: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 style="white-space:nowrap"> |
|
* <div style="background-color: silver;"> |
|
* <ul style="color: red;"> |
|
* <li>List Item</li> |
|
* </ul> |
|
* </div> |
|
* </td> |
|
* <!--setOuterHTML--> |
|
* <td style="white-space: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™ |
|
* 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="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html">Concurrency |
|
* in Swing</A> for more information. |
|
* |
|
* @param offset the offset into the paragraph (must be at least 0) |
|
* @param length the number of characters affected (must be at least 0) |
|
* @param s the attributes |
|
* @param replace whether to replace existing attributes, or merge them |
|
*/ |
|
public void setParagraphAttributes(int offset, int length, AttributeSet s, |
|
boolean replace) { |
|
try { |
|
writeLock(); |
|
// Make sure we send out a change for the length of the paragraph. |
|
int end = Math.min(offset + length, getLength()); |
|
Element e = getParagraphElement(offset); |
|
offset = e.getStartOffset(); |
|
e = getParagraphElement(end); |
|
length = Math.max(0, e.getEndOffset() - offset); |
|
DefaultDocumentEvent changes = |
|
new DefaultDocumentEvent(offset, length, |
|
DocumentEvent.EventType.CHANGE); |
|
AttributeSet sCopy = s.copyAttributes(); |
|
int lastEnd = Integer.MAX_VALUE; |
|
for (int pos = offset; pos <= end; pos = lastEnd) { |
|
Element paragraph = getParagraphElement(pos); |
|
if (lastEnd == paragraph.getEndOffset()) { |
|
lastEnd++; |
|
} |
|
else { |
|
lastEnd = paragraph.getEndOffset(); |
|
} |
|
MutableAttributeSet attr = |
|
(MutableAttributeSet) paragraph.getAttributes(); |
|
changes.addEdit(new AttributeUndoableEdit(paragraph, sCopy, replace)); |
|
if (replace) { |
|
attr.removeAttributes(attr); |
|
} |
|
attr.addAttributes(s); |
|
} |
|
changes.end(); |
|
fireChangedUpdate(changes); |
|
fireUndoableEditUpdate(new UndoableEditEvent(this, changes)); |
|
} finally { |
|
writeUnlock(); |
|
} |
|
} |
|
/** |
|
* Fetches the <code>StyleSheet</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 correctly, 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, and one for the newline.</p> |
|
* |
|
* <p>If you try to replace the element at length you will most |
|
* likely end up with two elements, eg |
|
* <code>setOuterHTML(getCharacterElement (getLength()), |
|
* "blah")</code> will result in two leaf elements at the end, one |
|
* representing 'blah', and the other representing the end |
|
* element.</p> |
|
* |
|
* <p>Consider the following structure (the <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 correctly, the document must have an |
|
* HTMLEditorKit.Parser set. This will be the case if the document |
|
* was created from an HTMLEditorKit via the |
|
* <code>createDefaultDocument</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 correctly, 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 || htmlText == null) { |
|
return; |
|
} |
|
if (elem.isLeaf()) { |
|
throw new IllegalArgumentException |
|
("Can not insert HTML after start of a leaf"); |
|
} |
|
insertHTML(elem, elem.getStartOffset(), htmlText, false); |
|
} |
|
/** |
|
* Inserts the HTML specified as a string at the end of |
|
* the element. |
|
* |
|
* <p> If <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 correctly, 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 correctly, 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 correctly, 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) { |
|
// If we are going to insert the string into the body |
|
// section, it is necessary to set the corrsponding flag. |
|
if (HTML.Tag.BODY.name.equals(parent.getName())) { |
|
insertInBody = true; |
|
} |
|
int offset = elem.getEndOffset(); |
|
if (offset > (getLength() + 1)) { |
|
offset--; |
|
} |
|
else if (elem.isLeaf() && getText(offset - 1, 1). |
|
charAt(0) == NEWLINE[0]) { |
|
offset--; |
|
} |
|
insertHTML(parent, offset, htmlText, false); |
|
// Cleanup the flag, if any. |
|
if (insertInBody) { |
|
insertInBody = 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; |
|
/** |
|
* Indicates that direct insertion to body section takes place. |
|
*/ |
|
private boolean insertInBody = false; |
|
/** |
|
* 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>. |
|
* <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; |
|
// If content is added directly to the body, it should |
|
// be wrapped by p-implied. |
|
inParagraph = impliedP = !insertInBody; |
|
} |
|
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 the default is |
|
* assumed to be text. |
|
*/ |
|
if (type == null) { |
|
type = "text"; |
|
attr.addAttribute(HTML.Attribute.TYPE, "text"); |
|
} |
|
setModel(type, attr); |
|
} else if (t == HTML.Tag.TEXTAREA) { |
|
inTextArea = true; |
|
textAreaDocument = new TextAreaDocument(); |
|
attr.addAttribute(StyleConstants.ModelAttribute, |
|
textAreaDocument); |
|
} else if (t == HTML.Tag.SELECT) { |
|
int size = HTML.getIntegerAttributeValue(attr, |
|
HTML.Attribute.SIZE, |
|
1); |
|
boolean multiple = attr.getAttribute(HTML.Attribute.MULTIPLE) != null; |
|
if ((size > 1) || multiple) { |
|
OptionListModel<Option> m = new OptionListModel<Option>(); |
|
if (multiple) { |
|
m.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); |
|
} |
|
selectModel = m; |
|
} else { |
|
selectModel = new OptionComboBoxModel<Option>(); |
|
} |
|
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<Option> m = (OptionListModel<Option>) selectModel; |
|
m.addElement(option); |
|
if (option.isSelected()) { |
|
m.addSelectionInterval(optionCount, optionCount); |
|
m.setInitialSelection(optionCount); |
|
} |
|
} else if (selectModel instanceof OptionComboBoxModel) { |
|
OptionComboBoxModel<Option> m = (OptionComboBoxModel<Option>) 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 hierarchy) 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 {@literal <html>}. |
|
*/ |
|
private boolean receivedEndHTML; |
|
/** Number of times <code>flushBuffer</code> has been invoked. */ |
|
private int flushCount; |
|
/** If true, behavior is similar 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); |
|
} |
|
} |
|
} |
|
} |