1 /*
   2  * Copyright (c) 1997, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text.html;
  26 
  27 import sun.awt.AppContext;
  28 
  29 import java.awt.*;
  30 import java.awt.event.*;
  31 import java.io.*;
  32 import java.net.MalformedURLException;
  33 import java.net.URL;
  34 import javax.swing.text.*;
  35 import javax.swing.*;
  36 import javax.swing.event.*;
  37 import javax.swing.plaf.TextUI;
  38 import java.util.*;
  39 import javax.accessibility.*;
  40 import java.lang.ref.*;
  41 import java.security.AccessController;
  42 import java.security.PrivilegedAction;
  43 import javax.swing.text.html.parser.ParserDelegator;
  44 
  45 /**
  46  * The Swing JEditorPane text component supports different kinds
  47  * of content via a plug-in mechanism called an EditorKit.  Because
  48  * HTML is a very popular format of content, some support is provided
  49  * by default.  The default support is provided by this class, which
  50  * supports HTML version 3.2 (with some extensions), and is migrating
  51  * toward version 4.0.
  52  * The <applet> tag is not supported, but some support is provided
  53  * for the <object> tag.
  54  * <p>
  55  * There are several goals of the HTML EditorKit provided, that have
  56  * an effect upon the way that HTML is modeled.  These
  57  * have influenced its design in a substantial way.
  58  * <dl>
  59  * <dt>
  60  * Support editing
  61  * <dd>
  62  * It might seem fairly obvious that a plug-in for JEditorPane
  63  * should provide editing support, but that fact has several
  64  * design considerations.  There are a substantial number of HTML
  65  * documents that don't properly conform to an HTML specification.
  66  * These must be normalized somewhat into a correct form if one
  67  * is to edit them.  Additionally, users don't like to be presented
  68  * with an excessive amount of structure editing, so using traditional
  69  * text editing gestures is preferred over using the HTML structure
  70  * exactly as defined in the HTML document.
  71  * <p>
  72  * The modeling of HTML is provided by the class <code>HTMLDocument</code>.
  73  * Its documentation describes the details of how the HTML is modeled.
  74  * The editing support leverages heavily off of the text package.
  75  *
  76  * <dt>
  77  * Extendable/Scalable
  78  * <dd>
  79  * To maximize the usefulness of this kit, a great deal of effort
  80  * has gone into making it extendable.  These are some of the
  81  * features.
  82  * <ol>
  83  *   <li>
  84  *   The parser is replaceable.  The default parser is the Hot Java
  85  *   parser which is DTD based.  A different DTD can be used, or an
  86  *   entirely different parser can be used.  To change the parser,
  87  *   reimplement the getParser method.  The default parser is
  88  *   dynamically loaded when first asked for, so the class files
  89  *   will never be loaded if an alternative parser is used.  The
  90  *   default parser is in a separate package called parser below
  91  *   this package.
  92  *   <li>
  93  *   The parser drives the ParserCallback, which is provided by
  94  *   HTMLDocument.  To change the callback, subclass HTMLDocument
  95  *   and reimplement the createDefaultDocument method to return
  96  *   document that produces a different reader.  The reader controls
  97  *   how the document is structured.  Although the Document provides
  98  *   HTML support by default, there is nothing preventing support of
  99  *   non-HTML tags that result in alternative element structures.
 100  *   <li>
 101  *   The default view of the models are provided as a hierarchy of
 102  *   View implementations, so one can easily customize how a particular
 103  *   element is displayed or add capabilities for new kinds of elements
 104  *   by providing new View implementations.  The default set of views
 105  *   are provided by the <code>HTMLFactory</code> class.  This can
 106  *   be easily changed by subclassing or replacing the HTMLFactory
 107  *   and reimplementing the getViewFactory method to return the alternative
 108  *   factory.
 109  *   <li>
 110  *   The View implementations work primarily off of CSS attributes,
 111  *   which are kept in the views.  This makes it possible to have
 112  *   multiple views mapped over the same model that appear substantially
 113  *   different.  This can be especially useful for printing.  For
 114  *   most HTML attributes, the HTML attributes are converted to CSS
 115  *   attributes for display.  This helps make the View implementations
 116  *   more general purpose
 117  * </ol>
 118  *
 119  * <dt>
 120  * Asynchronous Loading
 121  * <dd>
 122  * Larger documents involve a lot of parsing and take some time
 123  * to load.  By default, this kit produces documents that will be
 124  * loaded asynchronously if loaded using <code>JEditorPane.setPage</code>.
 125  * This is controlled by a property on the document.  The method
 126  * {@link #createDefaultDocument createDefaultDocument} can
 127  * be overriden to change this.  The batching of work is done
 128  * by the <code>HTMLDocument.HTMLReader</code> class.  The actual
 129  * work is done by the <code>DefaultStyledDocument</code> and
 130  * <code>AbstractDocument</code> classes in the text package.
 131  *
 132  * <dt>
 133  * Customization from current LAF
 134  * <dd>
 135  * HTML provides a well known set of features without exactly
 136  * specifying the display characteristics.  Swing has a theme
 137  * mechanism for its look-and-feel implementations.  It is desirable
 138  * for the look-and-feel to feed display characteristics into the
 139  * HTML views.  An user with poor vision for example would want
 140  * high contrast and larger than typical fonts.
 141  * <p>
 142  * The support for this is provided by the <code>StyleSheet</code>
 143  * class.  The presentation of the HTML can be heavily influenced
 144  * by the setting of the StyleSheet property on the EditorKit.
 145  *
 146  * <dt>
 147  * Not lossy
 148  * <dd>
 149  * An EditorKit has the ability to be read and save documents.
 150  * It is generally the most pleasing to users if there is no loss
 151  * of data between the two operation.  The policy of the HTMLEditorKit
 152  * will be to store things not recognized or not necessarily visible
 153  * so they can be subsequently written out.  The model of the HTML document
 154  * should therefore contain all information discovered while reading the
 155  * document.  This is constrained in some ways by the need to support
 156  * editing (i.e. incorrect documents sometimes must be normalized).
 157  * The guiding principle is that information shouldn't be lost, but
 158  * some might be synthesized to produce a more correct model or it might
 159  * be rearranged.
 160  * </dl>
 161  *
 162  * @author  Timothy Prinzing
 163  */
 164 @SuppressWarnings("serial") // Same-version serialization only
 165 public class HTMLEditorKit extends StyledEditorKit implements Accessible {
 166 
 167     private JEditorPane theEditor;
 168 
 169     /**
 170      * Constructs an HTMLEditorKit, creates a StyleContext,
 171      * and loads the style sheet.
 172      */
 173     public HTMLEditorKit() {
 174 
 175     }
 176 
 177     /**
 178      * Get the MIME type of the data that this
 179      * kit represents support for.  This kit supports
 180      * the type <code>text/html</code>.
 181      *
 182      * @return the type
 183      */
 184     public String getContentType() {
 185         return "text/html";
 186     }
 187 
 188     /**
 189      * Fetch a factory that is suitable for producing
 190      * views of any models that are produced by this
 191      * kit.
 192      *
 193      * @return the factory
 194      */
 195     public ViewFactory getViewFactory() {
 196         return defaultFactory;
 197     }
 198 
 199     /**
 200      * Create an uninitialized text storage model
 201      * that is appropriate for this type of editor.
 202      *
 203      * @return the model
 204      */
 205     public Document createDefaultDocument() {
 206         StyleSheet styles = getStyleSheet();
 207         StyleSheet ss = new StyleSheet();
 208 
 209         ss.addStyleSheet(styles);
 210 
 211         HTMLDocument doc = new HTMLDocument(ss);
 212         doc.setParser(getParser());
 213         doc.setAsynchronousLoadPriority(4);
 214         doc.setTokenThreshold(100);
 215         return doc;
 216     }
 217 
 218     /**
 219      * Try to get an HTML parser from the document.  If no parser is set for
 220      * the document, return the editor kit's default parser.  It is an error
 221      * if no parser could be obtained from the editor kit.
 222      */
 223     private Parser ensureParser(HTMLDocument doc) throws IOException {
 224         Parser p = doc.getParser();
 225         if (p == null) {
 226             p = getParser();
 227         }
 228         if (p == null) {
 229             throw new IOException("Can't load parser");
 230         }
 231         return p;
 232     }
 233 
 234     /**
 235      * Inserts content from the given stream. If <code>doc</code> is
 236      * an instance of HTMLDocument, this will read
 237      * HTML 3.2 text. Inserting HTML into a non-empty document must be inside
 238      * the body Element, if you do not insert into the body an exception will
 239      * be thrown. When inserting into a non-empty document all tags outside
 240      * of the body (head, title) will be dropped.
 241      *
 242      * @param in the stream to read from
 243      * @param doc the destination for the insertion
 244      * @param pos the location in the document to place the
 245      *   content
 246      * @exception IOException on any I/O error
 247      * @exception BadLocationException if pos represents an invalid
 248      *   location within the document
 249      * @exception RuntimeException (will eventually be a BadLocationException)
 250      *            if pos is invalid
 251      */
 252     public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {
 253 
 254         if (doc instanceof HTMLDocument) {
 255             HTMLDocument hdoc = (HTMLDocument) doc;
 256             if (pos > doc.getLength()) {
 257                 throw new BadLocationException("Invalid location", pos);
 258             }
 259 
 260             Parser p = ensureParser(hdoc);
 261             ParserCallback receiver = hdoc.getReader(pos);
 262             Boolean ignoreCharset = (Boolean)doc.getProperty("IgnoreCharsetDirective");
 263             p.parse(in, receiver, (ignoreCharset == null) ? false : ignoreCharset.booleanValue());
 264             receiver.flush();
 265         } else {
 266             super.read(in, doc, pos);
 267         }
 268     }
 269 
 270     /**
 271      * Inserts HTML into an existing document.
 272      *
 273      * @param doc the document to insert into
 274      * @param offset the offset to insert HTML at
 275      * @param popDepth the number of ElementSpec.EndTagTypes to generate
 276      *                  before inserting
 277      * @param html the HTML string
 278      * @param pushDepth the number of ElementSpec.StartTagTypes with a direction
 279      *                  of ElementSpec.JoinNextDirection that should be generated
 280      *                  before inserting, but after the end tags have been generated
 281      * @param insertTag the first tag to start inserting into document
 282      *
 283      * @throws BadLocationException if {@code offset} is invalid
 284      * @throws IOException on I/O error
 285      * @exception RuntimeException (will eventually be a BadLocationException)
 286      *            if pos is invalid
 287      */
 288     public void insertHTML(HTMLDocument doc, int offset, String html,
 289                            int popDepth, int pushDepth,
 290                            HTML.Tag insertTag) throws
 291                        BadLocationException, IOException {
 292         if (offset > doc.getLength()) {
 293             throw new BadLocationException("Invalid location", offset);
 294         }
 295 
 296         Parser p = ensureParser(doc);
 297         ParserCallback receiver = doc.getReader(offset, popDepth, pushDepth,
 298                                                 insertTag);
 299         Boolean ignoreCharset = (Boolean)doc.getProperty
 300                                 ("IgnoreCharsetDirective");
 301         p.parse(new StringReader(html), receiver, (ignoreCharset == null) ?
 302                 false : ignoreCharset.booleanValue());
 303         receiver.flush();
 304     }
 305 
 306     /**
 307      * Write content from a document to the given stream
 308      * in a format appropriate for this kind of content handler.
 309      *
 310      * @param out the stream to write to
 311      * @param doc the source for the write
 312      * @param pos the location in the document to fetch the
 313      *   content
 314      * @param len the amount to write out
 315      * @exception IOException on any I/O error
 316      * @exception BadLocationException if {@code pos} represents an invalid
 317      *   location within the document
 318      */
 319     public void write(Writer out, Document doc, int pos, int len)
 320         throws IOException, BadLocationException {
 321 
 322         if (doc instanceof HTMLDocument) {
 323             HTMLWriter w = new HTMLWriter(out, (HTMLDocument)doc, pos, len);
 324             w.write();
 325         } else if (doc instanceof StyledDocument) {
 326             MinimalHTMLWriter w = new MinimalHTMLWriter(out, (StyledDocument)doc, pos, len);
 327             w.write();
 328         } else {
 329             super.write(out, doc, pos, len);
 330         }
 331     }
 332 
 333     /**
 334      * Called when the kit is being installed into the
 335      * a JEditorPane.
 336      *
 337      * @param c the JEditorPane
 338      */
 339     public void install(JEditorPane c) {
 340         c.addMouseListener(linkHandler);
 341         c.addMouseMotionListener(linkHandler);
 342         c.addCaretListener(nextLinkAction);
 343         super.install(c);
 344         theEditor = c;
 345     }
 346 
 347     /**
 348      * Called when the kit is being removed from the
 349      * JEditorPane.  This is used to unregister any
 350      * listeners that were attached.
 351      *
 352      * @param c the JEditorPane
 353      */
 354     public void deinstall(JEditorPane c) {
 355         c.removeMouseListener(linkHandler);
 356         c.removeMouseMotionListener(linkHandler);
 357         c.removeCaretListener(nextLinkAction);
 358         super.deinstall(c);
 359         theEditor = null;
 360     }
 361 
 362     /**
 363      * Default Cascading Style Sheet file that sets
 364      * up the tag views.
 365      */
 366     public static final String DEFAULT_CSS = "default.css";
 367 
 368     /**
 369      * Set the set of styles to be used to render the various
 370      * HTML elements.  These styles are specified in terms of
 371      * CSS specifications.  Each document produced by the kit
 372      * will have a copy of the sheet which it can add the
 373      * document specific styles to.  By default, the StyleSheet
 374      * specified is shared by all HTMLEditorKit instances.
 375      * This should be reimplemented to provide a finer granularity
 376      * if desired.
 377      *
 378      * @param s a StyleSheet
 379      */
 380     public void setStyleSheet(StyleSheet s) {
 381         if (s == null) {
 382             AppContext.getAppContext().remove(DEFAULT_STYLES_KEY);
 383         } else {
 384             AppContext.getAppContext().put(DEFAULT_STYLES_KEY, s);
 385         }
 386     }
 387 
 388     /**
 389      * Get the set of styles currently being used to render the
 390      * HTML elements.  By default the resource specified by
 391      * DEFAULT_CSS gets loaded, and is shared by all HTMLEditorKit
 392      * instances.
 393      *
 394      * @return the StyleSheet
 395      */
 396     public StyleSheet getStyleSheet() {
 397         AppContext appContext = AppContext.getAppContext();
 398         StyleSheet defaultStyles = (StyleSheet) appContext.get(DEFAULT_STYLES_KEY);
 399 
 400         if (defaultStyles == null) {
 401             defaultStyles = new StyleSheet();
 402             appContext.put(DEFAULT_STYLES_KEY, defaultStyles);
 403             try {
 404                 InputStream is = HTMLEditorKit.getResourceAsStream(DEFAULT_CSS);
 405                 Reader r = new BufferedReader(
 406                         new InputStreamReader(is, "ISO-8859-1"));
 407                 defaultStyles.loadRules(r, null);
 408                 r.close();
 409             } catch (Throwable e) {
 410                 // on error we simply have no styles... the html
 411                 // will look mighty wrong but still function.
 412             }
 413         }
 414         return defaultStyles;
 415     }
 416 
 417     /**
 418      * Fetch a resource relative to the HTMLEditorKit classfile.
 419      * If this is called on 1.2 the loading will occur under the
 420      * protection of a doPrivileged call to allow the HTMLEditorKit
 421      * to function when used in an applet.
 422      *
 423      * @param name the name of the resource, relative to the
 424      *             HTMLEditorKit class
 425      * @return a stream representing the resource
 426      */
 427     static InputStream getResourceAsStream(final String name) {
 428         return AccessController.doPrivileged(
 429                 new PrivilegedAction<InputStream>() {
 430                     public InputStream run() {
 431                         return HTMLEditorKit.class.getResourceAsStream(name);
 432                     }
 433                 });
 434     }
 435 
 436     /**
 437      * Fetches the command list for the editor.  This is
 438      * the list of commands supported by the superclass
 439      * augmented by the collection of commands defined
 440      * locally for style operations.
 441      *
 442      * @return the command list
 443      */
 444     public Action[] getActions() {
 445         return TextAction.augmentList(super.getActions(), defaultActions);
 446     }
 447 
 448     /**
 449      * Copies the key/values in <code>element</code>s AttributeSet into
 450      * <code>set</code>. This does not copy component, icon, or element
 451      * names attributes. Subclasses may wish to refine what is and what
 452      * isn't copied here. But be sure to first remove all the attributes that
 453      * are in <code>set</code>.<p>
 454      * This is called anytime the caret moves over a different location.
 455      *
 456      */
 457     protected void createInputAttributes(Element element,
 458                                          MutableAttributeSet set) {
 459         set.removeAttributes(set);
 460         set.addAttributes(element.getAttributes());
 461         set.removeAttribute(StyleConstants.ComposedTextAttribute);
 462 
 463         Object o = set.getAttribute(StyleConstants.NameAttribute);
 464         if (o instanceof HTML.Tag) {
 465             HTML.Tag tag = (HTML.Tag)o;
 466             // PENDING: we need a better way to express what shouldn't be
 467             // copied when editing...
 468             if(tag == HTML.Tag.IMG) {
 469                 // Remove the related image attributes, src, width, height
 470                 set.removeAttribute(HTML.Attribute.SRC);
 471                 set.removeAttribute(HTML.Attribute.HEIGHT);
 472                 set.removeAttribute(HTML.Attribute.WIDTH);
 473                 set.addAttribute(StyleConstants.NameAttribute,
 474                                  HTML.Tag.CONTENT);
 475             }
 476             else if (tag == HTML.Tag.HR || tag == HTML.Tag.BR) {
 477                 // Don't copy HRs or BRs either.
 478                 set.addAttribute(StyleConstants.NameAttribute,
 479                                  HTML.Tag.CONTENT);
 480             }
 481             else if (tag == HTML.Tag.COMMENT) {
 482                 // Don't copy COMMENTs either
 483                 set.addAttribute(StyleConstants.NameAttribute,
 484                                  HTML.Tag.CONTENT);
 485                 set.removeAttribute(HTML.Attribute.COMMENT);
 486             }
 487             else if (tag == HTML.Tag.INPUT) {
 488                 // or INPUT either
 489                 set.addAttribute(StyleConstants.NameAttribute,
 490                                  HTML.Tag.CONTENT);
 491                 set.removeAttribute(HTML.Tag.INPUT);
 492             }
 493             else if (tag instanceof HTML.UnknownTag) {
 494                 // Don't copy unknowns either:(
 495                 set.addAttribute(StyleConstants.NameAttribute,
 496                                  HTML.Tag.CONTENT);
 497                 set.removeAttribute(HTML.Attribute.ENDTAG);
 498             }
 499         }
 500     }
 501 
 502     /**
 503      * Gets the input attributes used for the styled
 504      * editing actions.
 505      *
 506      * @return the attribute set
 507      */
 508     public MutableAttributeSet getInputAttributes() {
 509         if (input == null) {
 510             input = getStyleSheet().addStyle(null, null);
 511         }
 512         return input;
 513     }
 514 
 515     /**
 516      * Sets the default cursor.
 517      *
 518      * @param cursor a cursor
 519      *
 520      * @since 1.3
 521      */
 522     public void setDefaultCursor(Cursor cursor) {
 523         defaultCursor = cursor;
 524     }
 525 
 526     /**
 527      * Returns the default cursor.
 528      *
 529      * @return the cursor
 530      *
 531      * @since 1.3
 532      */
 533     public Cursor getDefaultCursor() {
 534         return defaultCursor;
 535     }
 536 
 537     /**
 538      * Sets the cursor to use over links.
 539      *
 540      * @param cursor a cursor
 541      *
 542      * @since 1.3
 543      */
 544     public void setLinkCursor(Cursor cursor) {
 545         linkCursor = cursor;
 546     }
 547 
 548     /**
 549      * Returns the cursor to use over hyper links.
 550      *
 551      * @return the cursor
 552      *
 553      * @since 1.3
 554      */
 555     public Cursor getLinkCursor() {
 556         return linkCursor;
 557     }
 558 
 559     /**
 560      * Indicates whether an html form submission is processed automatically
 561      * or only <code>FormSubmitEvent</code> is fired.
 562      *
 563      * @return true  if html form submission is processed automatically,
 564      *         false otherwise.
 565      *
 566      * @see #setAutoFormSubmission
 567      * @since 1.5
 568      */
 569     public boolean isAutoFormSubmission() {
 570         return isAutoFormSubmission;
 571     }
 572 
 573     /**
 574      * Specifies if an html form submission is processed
 575      * automatically or only <code>FormSubmitEvent</code> is fired.
 576      * By default it is set to true.
 577      *
 578      * @param isAuto if {@code true}, html form submission is processed automatically.
 579      *
 580      * @see #isAutoFormSubmission()
 581      * @see FormSubmitEvent
 582      * @since 1.5
 583      */
 584     public void setAutoFormSubmission(boolean isAuto) {
 585         isAutoFormSubmission = isAuto;
 586     }
 587 
 588     /**
 589      * Creates a copy of the editor kit.
 590      *
 591      * @return the copy
 592      */
 593     public Object clone() {
 594         HTMLEditorKit o = (HTMLEditorKit)super.clone();
 595         if (o != null) {
 596             o.input = null;
 597             o.linkHandler = new LinkController();
 598         }
 599         return o;
 600     }
 601 
 602     /**
 603      * Fetch the parser to use for reading HTML streams.
 604      * This can be reimplemented to provide a different
 605      * parser.  The default implementation is loaded dynamically
 606      * to avoid the overhead of loading the default parser if
 607      * it's not used.  The default parser is the HotJava parser
 608      * using an HTML 3.2 DTD.
 609      *
 610      * @return the parser
 611      */
 612     protected Parser getParser() {
 613         if (defaultParser == null) {
 614             defaultParser = new ParserDelegator();
 615         }
 616         return defaultParser;
 617     }
 618 
 619     // ----- Accessibility support -----
 620     private AccessibleContext accessibleContext;
 621 
 622     /**
 623      * returns the AccessibleContext associated with this editor kit
 624      *
 625      * @return the AccessibleContext associated with this editor kit
 626      * @since 1.4
 627      */
 628     public AccessibleContext getAccessibleContext() {
 629         if (theEditor == null) {
 630             return null;
 631         }
 632         if (accessibleContext == null) {
 633             AccessibleHTML a = new AccessibleHTML(theEditor);
 634             accessibleContext = a.getAccessibleContext();
 635         }
 636         return accessibleContext;
 637     }
 638 
 639     // --- variables ------------------------------------------
 640 
 641     private static final Cursor MoveCursor = Cursor.getPredefinedCursor
 642                                     (Cursor.HAND_CURSOR);
 643     private static final Cursor DefaultCursor = Cursor.getPredefinedCursor
 644                                     (Cursor.DEFAULT_CURSOR);
 645 
 646     /** Shared factory for creating HTML Views. */
 647     private static final ViewFactory defaultFactory = new HTMLFactory();
 648 
 649     MutableAttributeSet input;
 650     private static final Object DEFAULT_STYLES_KEY = new Object();
 651     private LinkController linkHandler = new LinkController();
 652     private static Parser defaultParser = null;
 653     private Cursor defaultCursor = DefaultCursor;
 654     private Cursor linkCursor = MoveCursor;
 655     private boolean isAutoFormSubmission = true;
 656 
 657     /**
 658      * Class to watch the associated component and fire
 659      * hyperlink events on it when appropriate.
 660      */
 661     @SuppressWarnings("serial") // Same-version serialization only
 662     public static class LinkController extends MouseAdapter implements MouseMotionListener, Serializable {
 663         private Element curElem = null;
 664         /**
 665          * If true, the current element (curElem) represents an image.
 666          */
 667         private boolean curElemImage = false;
 668         private String href = null;
 669         /** This is used by viewToModel to avoid allocing a new array each
 670          * time. */
 671         private transient Position.Bias[] bias = new Position.Bias[1];
 672         /**
 673          * Current offset.
 674          */
 675         private int curOffset;
 676 
 677         /**
 678          * Called for a mouse click event.
 679          * If the component is read-only (ie a browser) then
 680          * the clicked event is used to drive an attempt to
 681          * follow the reference specified by a link.
 682          *
 683          * @param e the mouse event
 684          * @see MouseListener#mouseClicked
 685          */
 686         @SuppressWarnings("deprecation")
 687         public void mouseClicked(MouseEvent e) {
 688             JEditorPane editor = (JEditorPane) e.getSource();
 689 
 690             if (! editor.isEditable() && editor.isEnabled() &&
 691                     SwingUtilities.isLeftMouseButton(e)) {
 692                 Point pt = new Point(e.getX(), e.getY());
 693                 int pos = editor.viewToModel(pt);
 694                 if (pos >= 0) {
 695                     activateLink(pos, editor, e);
 696                 }
 697             }
 698         }
 699 
 700         // ignore the drags
 701         public void mouseDragged(MouseEvent e) {
 702         }
 703 
 704         // track the moving of the mouse.
 705         @SuppressWarnings("deprecation")
 706         public void mouseMoved(MouseEvent e) {
 707             JEditorPane editor = (JEditorPane) e.getSource();
 708             if (!editor.isEnabled()) {
 709                 return;
 710             }
 711 
 712             HTMLEditorKit kit = (HTMLEditorKit)editor.getEditorKit();
 713             boolean adjustCursor = true;
 714             Cursor newCursor = kit.getDefaultCursor();
 715             if (!editor.isEditable()) {
 716                 Point pt = new Point(e.getX(), e.getY());
 717                 int pos = editor.getUI().viewToModel(editor, pt, bias);
 718                 if (bias[0] == Position.Bias.Backward && pos > 0) {
 719                     pos--;
 720                 }
 721                 if (pos >= 0 &&(editor.getDocument() instanceof HTMLDocument)){
 722                     HTMLDocument hdoc = (HTMLDocument)editor.getDocument();
 723                     Element elem = hdoc.getCharacterElement(pos);
 724                     if (!doesElementContainLocation(editor, elem, pos,
 725                                                     e.getX(), e.getY())) {
 726                         elem = null;
 727                     }
 728                     if (curElem != elem || curElemImage) {
 729                         Element lastElem = curElem;
 730                         curElem = elem;
 731                         String href = null;
 732                         curElemImage = false;
 733                         if (elem != null) {
 734                             AttributeSet a = elem.getAttributes();
 735                             AttributeSet anchor = (AttributeSet)a.
 736                                                    getAttribute(HTML.Tag.A);
 737                             if (anchor == null) {
 738                                 curElemImage = (a.getAttribute(StyleConstants.
 739                                             NameAttribute) == HTML.Tag.IMG);
 740                                 if (curElemImage) {
 741                                     href = getMapHREF(editor, hdoc, elem, a,
 742                                                       pos, e.getX(), e.getY());
 743                                 }
 744                             }
 745                             else {
 746                                 href = (String)anchor.getAttribute
 747                                     (HTML.Attribute.HREF);
 748                             }
 749                         }
 750 
 751                         if (href != this.href) {
 752                             // reference changed, fire event(s)
 753                             fireEvents(editor, hdoc, href, lastElem, e);
 754                             this.href = href;
 755                             if (href != null) {
 756                                 newCursor = kit.getLinkCursor();
 757                             }
 758                         }
 759                         else {
 760                             adjustCursor = false;
 761                         }
 762                     }
 763                     else {
 764                         adjustCursor = false;
 765                     }
 766                     curOffset = pos;
 767                 }
 768             }
 769             if (adjustCursor && editor.getCursor() != newCursor) {
 770                 editor.setCursor(newCursor);
 771             }
 772         }
 773 
 774         /**
 775          * Returns a string anchor if the passed in element has a
 776          * USEMAP that contains the passed in location.
 777          */
 778         @SuppressWarnings("deprecation")
 779         private String getMapHREF(JEditorPane html, HTMLDocument hdoc,
 780                                   Element elem, AttributeSet attr, int offset,
 781                                   int x, int y) {
 782             Object useMap = attr.getAttribute(HTML.Attribute.USEMAP);
 783             if (useMap != null && (useMap instanceof String)) {
 784                 Map m = hdoc.getMap((String)useMap);
 785                 if (m != null && offset < hdoc.getLength()) {
 786                     Rectangle bounds;
 787                     TextUI ui = html.getUI();
 788                     try {
 789                         Shape lBounds = ui.modelToView(html, offset,
 790                                                    Position.Bias.Forward);
 791                         Shape rBounds = ui.modelToView(html, offset + 1,
 792                                                    Position.Bias.Backward);
 793                         bounds = lBounds.getBounds();
 794                         bounds.add((rBounds instanceof Rectangle) ?
 795                                     (Rectangle)rBounds : rBounds.getBounds());
 796                     } catch (BadLocationException ble) {
 797                         bounds = null;
 798                     }
 799                     if (bounds != null) {
 800                         AttributeSet area = m.getArea(x - bounds.x,
 801                                                       y - bounds.y,
 802                                                       bounds.width,
 803                                                       bounds.height);
 804                         if (area != null) {
 805                             return (String)area.getAttribute(HTML.Attribute.
 806                                                              HREF);
 807                         }
 808                     }
 809                 }
 810             }
 811             return null;
 812         }
 813 
 814         /**
 815          * Returns true if the View representing <code>e</code> contains
 816          * the location <code>x</code>, <code>y</code>. <code>offset</code>
 817          * gives the offset into the Document to check for.
 818          */
 819         @SuppressWarnings("deprecation")
 820         private boolean doesElementContainLocation(JEditorPane editor,
 821                                                    Element e, int offset,
 822                                                    int x, int y) {
 823             if (e != null && offset > 0 && e.getStartOffset() == offset) {
 824                 try {
 825                     TextUI ui = editor.getUI();
 826                     Shape s1 = ui.modelToView(editor, offset,
 827                                               Position.Bias.Forward);
 828                     if (s1 == null) {
 829                         return false;
 830                     }
 831                     Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle)s1 :
 832                                     s1.getBounds();
 833                     Shape s2 = ui.modelToView(editor, e.getEndOffset(),
 834                                               Position.Bias.Backward);
 835                     if (s2 != null) {
 836                         Rectangle r2 = (s2 instanceof Rectangle) ? (Rectangle)s2 :
 837                                     s2.getBounds();
 838                         r1.add(r2);
 839                     }
 840                     return r1.contains(x, y);
 841                 } catch (BadLocationException ble) {
 842                 }
 843             }
 844             return true;
 845         }
 846 
 847         /**
 848          * Calls linkActivated on the associated JEditorPane
 849          * if the given position represents a link.<p>This is implemented
 850          * to forward to the method with the same name, but with the following
 851          * args both == -1.
 852          *
 853          * @param pos the position
 854          * @param editor the editor pane
 855          */
 856         protected void activateLink(int pos, JEditorPane editor) {
 857             activateLink(pos, editor, null);
 858         }
 859 
 860         /**
 861          * Calls linkActivated on the associated JEditorPane
 862          * if the given position represents a link. If this was the result
 863          * of a mouse click, <code>x</code> and
 864          * <code>y</code> will give the location of the mouse, otherwise
 865          * they will be {@literal <} 0.
 866          *
 867          * @param pos the position
 868          * @param html the editor pane
 869          */
 870         void activateLink(int pos, JEditorPane html, MouseEvent mouseEvent) {
 871             Document doc = html.getDocument();
 872             if (doc instanceof HTMLDocument) {
 873                 HTMLDocument hdoc = (HTMLDocument) doc;
 874                 Element e = hdoc.getCharacterElement(pos);
 875                 AttributeSet a = e.getAttributes();
 876                 AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A);
 877                 HyperlinkEvent linkEvent = null;
 878                 String description;
 879                 int x = -1;
 880                 int y = -1;
 881 
 882                 if (mouseEvent != null) {
 883                     x = mouseEvent.getX();
 884                     y = mouseEvent.getY();
 885                 }
 886 
 887                 if (anchor == null) {
 888                     href = getMapHREF(html, hdoc, e, a, pos, x, y);
 889                 }
 890                 else {
 891                     href = (String)anchor.getAttribute(HTML.Attribute.HREF);
 892                 }
 893 
 894                 if (href != null) {
 895                     linkEvent = createHyperlinkEvent(html, hdoc, href, anchor,
 896                                                      e, mouseEvent);
 897                 }
 898                 if (linkEvent != null) {
 899                     html.fireHyperlinkUpdate(linkEvent);
 900                 }
 901             }
 902         }
 903 
 904         /**
 905          * Creates and returns a new instance of HyperlinkEvent. If
 906          * <code>hdoc</code> is a frame document a HTMLFrameHyperlinkEvent
 907          * will be created.
 908          */
 909         HyperlinkEvent createHyperlinkEvent(JEditorPane html,
 910                                             HTMLDocument hdoc, String href,
 911                                             AttributeSet anchor,
 912                                             Element element,
 913                                             MouseEvent mouseEvent) {
 914             URL u;
 915             try {
 916                 URL base = hdoc.getBase();
 917                 u = new URL(base, href);
 918                 // Following is a workaround for 1.2, in which
 919                 // new URL("file://...", "#...") causes the filename to
 920                 // be lost.
 921                 if (href != null && "file".equals(u.getProtocol()) &&
 922                     href.startsWith("#")) {
 923                     String baseFile = base.getFile();
 924                     String newFile = u.getFile();
 925                     if (baseFile != null && newFile != null &&
 926                         !newFile.startsWith(baseFile)) {
 927                         u = new URL(base, baseFile + href);
 928                     }
 929                 }
 930             } catch (MalformedURLException m) {
 931                 u = null;
 932             }
 933             HyperlinkEvent linkEvent;
 934 
 935             if (!hdoc.isFrameDocument()) {
 936                 linkEvent = new HyperlinkEvent(
 937                         html, HyperlinkEvent.EventType.ACTIVATED, u, href,
 938                         element, mouseEvent);
 939             } else {
 940                 String target = (anchor != null) ?
 941                     (String)anchor.getAttribute(HTML.Attribute.TARGET) : null;
 942                 if (target == null || target.isEmpty()) {
 943                     target = hdoc.getBaseTarget();
 944                 }
 945                 if (target == null || target.isEmpty()) {
 946                     target = "_self";
 947                 }
 948                     linkEvent = new HTMLFrameHyperlinkEvent(
 949                         html, HyperlinkEvent.EventType.ACTIVATED, u, href,
 950                         element, mouseEvent, target);
 951             }
 952             return linkEvent;
 953         }
 954 
 955         void fireEvents(JEditorPane editor, HTMLDocument doc, String href,
 956                         Element lastElem, MouseEvent mouseEvent) {
 957             if (this.href != null) {
 958                 // fire an exited event on the old link
 959                 URL u;
 960                 try {
 961                     u = new URL(doc.getBase(), this.href);
 962                 } catch (MalformedURLException m) {
 963                     u = null;
 964                 }
 965                 HyperlinkEvent exit = new HyperlinkEvent(editor,
 966                                  HyperlinkEvent.EventType.EXITED, u, this.href,
 967                                  lastElem, mouseEvent);
 968                 editor.fireHyperlinkUpdate(exit);
 969             }
 970             if (href != null) {
 971                 // fire an entered event on the new link
 972                 URL u;
 973                 try {
 974                     u = new URL(doc.getBase(), href);
 975                 } catch (MalformedURLException m) {
 976                     u = null;
 977                 }
 978                 HyperlinkEvent entered = new HyperlinkEvent(editor,
 979                                             HyperlinkEvent.EventType.ENTERED,
 980                                             u, href, curElem, mouseEvent);
 981                 editor.fireHyperlinkUpdate(entered);
 982             }
 983         }
 984     }
 985 
 986     /**
 987      * Interface to be supported by the parser.  This enables
 988      * providing a different parser while reusing some of the
 989      * implementation provided by this editor kit.
 990      */
 991     public abstract static class Parser {
 992         /**
 993          * Parse the given stream and drive the given callback
 994          * with the results of the parse.  This method should
 995          * be implemented to be thread-safe.
 996          *
 997          * @param r a reader
 998          * @param cb a parser callback
 999          * @param ignoreCharSet if {@code true} charset is ignoring
1000          * @throws IOException if an I/O exception occurs
1001          */
1002         public abstract void parse(Reader r, ParserCallback cb, boolean ignoreCharSet) throws IOException;
1003 
1004     }
1005 
1006     /**
1007      * The result of parsing drives these callback methods.
1008      * The open and close actions should be balanced.  The
1009      * <code>flush</code> method will be the last method
1010      * called, to give the receiver a chance to flush any
1011      * pending data into the document.
1012      * <p>Refer to DocumentParser, the default parser used, for further
1013      * information on the contents of the AttributeSets, the positions, and
1014      * other info.
1015      *
1016      * @see javax.swing.text.html.parser.DocumentParser
1017      */
1018     public static class ParserCallback {
1019         /**
1020          * This is passed as an attribute in the attributeset to indicate
1021          * the element is implied eg, the string '&lt;&gt;foo&lt;\t&gt;'
1022          * contains an implied html element and an implied body element.
1023          *
1024          * @since 1.3
1025          */
1026         public static final Object IMPLIED = "_implied_";
1027 
1028         /**
1029          * The last method called on the reader. It allows
1030          * any pending changes to be flushed into the document.
1031          * Since this is currently loading synchronously, the entire
1032          * set of changes are pushed in at this point.
1033          *
1034          * @throws BadLocationException if the given position does not
1035          *   represent a valid location in the associated document.
1036          */
1037         public void flush() throws BadLocationException {
1038         }
1039 
1040         /**
1041          * Called by the parser to indicate a block of text was
1042          * encountered.
1043          *
1044          * @param data a data
1045          * @param pos a position
1046          */
1047         public void handleText(char[] data, int pos) {
1048         }
1049 
1050         /**
1051          * Called by the parser to indicate a block of comment was
1052          * encountered.
1053          *
1054          * @param data a data
1055          * @param pos a position
1056          */
1057         public void handleComment(char[] data, int pos) {
1058         }
1059 
1060         /**
1061          * Callback from the parser. Route to the appropriate
1062          * handler for the tag.
1063          *
1064          * @param t an HTML tag
1065          * @param a a set of attributes
1066          * @param pos a position
1067          */
1068         public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
1069         }
1070 
1071         /**
1072          * Callback from the parser. Route to the appropriate
1073          * handler for the tag.
1074          *
1075          * @param t an HTML tag
1076          * @param pos a position
1077          */
1078         public void handleEndTag(HTML.Tag t, int pos) {
1079         }
1080 
1081         /**
1082          * Callback from the parser. Route to the appropriate
1083          * handler for the tag.
1084          *
1085          * @param t an HTML tag
1086          * @param a a set of attributes
1087          * @param pos a position
1088          */
1089         public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
1090         }
1091 
1092         /**
1093          * Callback from the parser. Route to the appropriate
1094          * handler for the error.
1095          *
1096          * @param errorMsg a error message
1097          * @param pos a position
1098          */
1099         public void handleError(String errorMsg, int pos) {
1100         }
1101 
1102         /**
1103          * This is invoked after the stream has been parsed, but before
1104          * <code>flush</code>. <code>eol</code> will be one of \n, \r
1105          * or \r\n, which ever is encountered the most in parsing the
1106          * stream.
1107          *
1108          * @param eol value of eol
1109          *
1110          * @since 1.3
1111          */
1112         public void handleEndOfLineString(String eol) {
1113         }
1114     }
1115 
1116     /**
1117      * A factory to build views for HTML.  The following
1118      * table describes what this factory will build by
1119      * default.
1120      *
1121      * <table class="striped">
1122      * <caption>Describes the tag and view created by this factory by default
1123      * </caption>
1124      * <thead>
1125      *   <tr>
1126      *     <th scope="col">Tag
1127      *     <th scope="col">View created
1128      * </thead>
1129      * <tbody>
1130      *   <tr>
1131      *     <th scope="row">HTML.Tag.CONTENT
1132      *     <td>InlineView
1133      *   <tr>
1134      *     <th scope="row">HTML.Tag.IMPLIED
1135      *     <td>javax.swing.text.html.ParagraphView
1136      *   <tr>
1137      *     <th scope="row">HTML.Tag.P
1138      *     <td>javax.swing.text.html.ParagraphView
1139      *   <tr>
1140      *     <th scope="row">HTML.Tag.H1
1141      *     <td>javax.swing.text.html.ParagraphView
1142      *   <tr>
1143      *     <th scope="row">HTML.Tag.H2
1144      *     <td>javax.swing.text.html.ParagraphView
1145      *   <tr>
1146      *     <th scope="row">HTML.Tag.H3
1147      *     <td>javax.swing.text.html.ParagraphView
1148      *   <tr>
1149      *     <th scope="row">HTML.Tag.H4
1150      *     <td>javax.swing.text.html.ParagraphView
1151      *   <tr>
1152      *     <th scope="row">HTML.Tag.H5
1153      *     <td>javax.swing.text.html.ParagraphView
1154      *   <tr>
1155      *     <th scope="row">HTML.Tag.H6
1156      *     <td>javax.swing.text.html.ParagraphView
1157      *   <tr>
1158      *     <th scope="row">HTML.Tag.DT
1159      *     <td>javax.swing.text.html.ParagraphView
1160      *   <tr>
1161      *     <th scope="row">HTML.Tag.MENU
1162      *     <td>ListView
1163      *   <tr>
1164      *     <th scope="row">HTML.Tag.DIR
1165      *     <td>ListView
1166      *   <tr>
1167      *     <th scope="row">HTML.Tag.UL
1168      *     <td>ListView
1169      *   <tr>
1170      *     <th scope="row">HTML.Tag.OL
1171      *     <td>ListView
1172      *   <tr>
1173      *     <th scope="row">HTML.Tag.LI
1174      *     <td>BlockView
1175      *   <tr>
1176      *     <th scope="row">HTML.Tag.DL
1177      *     <td>BlockView
1178      *   <tr>
1179      *     <th scope="row">HTML.Tag.DD
1180      *     <td>BlockView
1181      *   <tr>
1182      *     <th scope="row">HTML.Tag.BODY
1183      *     <td>BlockView
1184      *   <tr>
1185      *     <th scope="row">HTML.Tag.HTML
1186      *     <td>BlockView
1187      *   <tr>
1188      *     <th scope="row">HTML.Tag.CENTER
1189      *     <td>BlockView
1190      *   <tr>
1191      *     <th scope="row">HTML.Tag.DIV
1192      *     <td>BlockView
1193      *   <tr>
1194      *     <th scope="row">HTML.Tag.BLOCKQUOTE
1195      *     <td>BlockView
1196      *   <tr>
1197      *     <th scope="row">HTML.Tag.PRE
1198      *     <td>BlockView
1199      *   <tr>
1200      *     <th scope="row">HTML.Tag.BLOCKQUOTE
1201      *     <td>BlockView
1202      *   <tr>
1203      *     <th scope="row">HTML.Tag.PRE
1204      *     <td>BlockView
1205      *   <tr>
1206      *     <th scope="row">HTML.Tag.IMG
1207      *     <td>ImageView
1208      *   <tr>
1209      *     <th scope="row">HTML.Tag.HR
1210      *     <td>HRuleView
1211      *   <tr>
1212      *     <th scope="row">HTML.Tag.BR
1213      *     <td>BRView
1214      *   <tr>
1215      *     <th scope="row">HTML.Tag.TABLE
1216      *     <td>javax.swing.text.html.TableView
1217      *   <tr>
1218      *     <th scope="row">HTML.Tag.INPUT
1219      *     <td>FormView
1220      *   <tr>
1221      *     <th scope="row">HTML.Tag.SELECT
1222      *     <td>FormView
1223      *   <tr>
1224      *     <th scope="row">HTML.Tag.TEXTAREA
1225      *     <td>FormView
1226      *   <tr>
1227      *     <th scope="row">HTML.Tag.OBJECT
1228      *     <td>ObjectView
1229      *   <tr>
1230      *     <th scope="row">HTML.Tag.FRAMESET
1231      *     <td>FrameSetView
1232      *   <tr>
1233      *     <th scope="row">HTML.Tag.FRAME
1234      *     <td>FrameView
1235      * </tbody>
1236      * </table>
1237      */
1238     public static class HTMLFactory implements ViewFactory {
1239 
1240         /**
1241          * Creates a view from an element.
1242          *
1243          * @param elem the element
1244          * @return the view
1245          */
1246         public View create(Element elem) {
1247             AttributeSet attrs = elem.getAttributes();
1248             Object elementName =
1249                 attrs.getAttribute(AbstractDocument.ElementNameAttribute);
1250             Object o = (elementName != null) ?
1251                 null : attrs.getAttribute(StyleConstants.NameAttribute);
1252             if (o instanceof HTML.Tag) {
1253                 HTML.Tag kind = (HTML.Tag) o;
1254                 if (kind == HTML.Tag.CONTENT) {
1255                     return new InlineView(elem);
1256                 } else if (kind == HTML.Tag.IMPLIED) {
1257                     String ws = (String) elem.getAttributes().getAttribute(
1258                         CSS.Attribute.WHITE_SPACE);
1259                     if ((ws != null) && ws.equals("pre")) {
1260                         return new LineView(elem);
1261                     }
1262                     return new javax.swing.text.html.ParagraphView(elem);
1263                 } else if ((kind == HTML.Tag.P) ||
1264                            (kind == HTML.Tag.H1) ||
1265                            (kind == HTML.Tag.H2) ||
1266                            (kind == HTML.Tag.H3) ||
1267                            (kind == HTML.Tag.H4) ||
1268                            (kind == HTML.Tag.H5) ||
1269                            (kind == HTML.Tag.H6) ||
1270                            (kind == HTML.Tag.DT)) {
1271                     // paragraph
1272                     return new javax.swing.text.html.ParagraphView(elem);
1273                 } else if ((kind == HTML.Tag.MENU) ||
1274                            (kind == HTML.Tag.DIR) ||
1275                            (kind == HTML.Tag.UL)   ||
1276                            (kind == HTML.Tag.OL)) {
1277                     return new ListView(elem);
1278                 } else if (kind == HTML.Tag.BODY) {
1279                     return new BodyBlockView(elem);
1280                 } else if (kind == HTML.Tag.HTML) {
1281                     return new BlockView(elem, View.Y_AXIS);
1282                 } else if ((kind == HTML.Tag.LI) ||
1283                            (kind == HTML.Tag.CENTER) ||
1284                            (kind == HTML.Tag.DL) ||
1285                            (kind == HTML.Tag.DD) ||
1286                            (kind == HTML.Tag.DIV) ||
1287                            (kind == HTML.Tag.BLOCKQUOTE) ||
1288                            (kind == HTML.Tag.PRE) ||
1289                            (kind == HTML.Tag.FORM)) {
1290                     // vertical box
1291                     return new BlockView(elem, View.Y_AXIS);
1292                 } else if (kind == HTML.Tag.NOFRAMES) {
1293                     return new NoFramesView(elem, View.Y_AXIS);
1294                 } else if (kind==HTML.Tag.IMG) {
1295                     return new ImageView(elem);
1296                 } else if (kind == HTML.Tag.ISINDEX) {
1297                     return new IsindexView(elem);
1298                 } else if (kind == HTML.Tag.HR) {
1299                     return new HRuleView(elem);
1300                 } else if (kind == HTML.Tag.BR) {
1301                     return new BRView(elem);
1302                 } else if (kind == HTML.Tag.TABLE) {
1303                     return new javax.swing.text.html.TableView(elem);
1304                 } else if ((kind == HTML.Tag.INPUT) ||
1305                            (kind == HTML.Tag.SELECT) ||
1306                            (kind == HTML.Tag.TEXTAREA)) {
1307                     return new FormView(elem);
1308                 } else if (kind == HTML.Tag.OBJECT) {
1309                     return new ObjectView(elem);
1310                 } else if (kind == HTML.Tag.FRAMESET) {
1311                      if (elem.getAttributes().isDefined(HTML.Attribute.ROWS)) {
1312                          return new FrameSetView(elem, View.Y_AXIS);
1313                      } else if (elem.getAttributes().isDefined(HTML.Attribute.COLS)) {
1314                          return new FrameSetView(elem, View.X_AXIS);
1315                      }
1316                      throw new RuntimeException("Can't build a"  + kind + ", " + elem + ":" +
1317                                      "no ROWS or COLS defined.");
1318                 } else if (kind == HTML.Tag.FRAME) {
1319                     return new FrameView(elem);
1320                 } else if (kind instanceof HTML.UnknownTag) {
1321                     return new HiddenTagView(elem);
1322                 } else if (kind == HTML.Tag.COMMENT) {
1323                     return new CommentView(elem);
1324                 } else if (kind == HTML.Tag.HEAD) {
1325                     // Make the head never visible, and never load its
1326                     // children. For Cursor positioning,
1327                     // getNextVisualPositionFrom is overriden to always return
1328                     // the end offset of the element.
1329                     return new BlockView(elem, View.X_AXIS) {
1330                         public float getPreferredSpan(int axis) {
1331                             return 0;
1332                         }
1333                         public float getMinimumSpan(int axis) {
1334                             return 0;
1335                         }
1336                         public float getMaximumSpan(int axis) {
1337                             return 0;
1338                         }
1339                         protected void loadChildren(ViewFactory f) {
1340                         }
1341                         public Shape modelToView(int pos, Shape a,
1342                                Position.Bias b) throws BadLocationException {
1343                             return a;
1344                         }
1345                         public int getNextVisualPositionFrom(int pos,
1346                                      Position.Bias b, Shape a,
1347                                      int direction, Position.Bias[] biasRet) {
1348                             return getElement().getEndOffset();
1349                         }
1350                     };
1351                 } else if ((kind == HTML.Tag.TITLE) ||
1352                            (kind == HTML.Tag.META) ||
1353                            (kind == HTML.Tag.LINK) ||
1354                            (kind == HTML.Tag.STYLE) ||
1355                            (kind == HTML.Tag.SCRIPT) ||
1356                            (kind == HTML.Tag.AREA) ||
1357                            (kind == HTML.Tag.MAP) ||
1358                            (kind == HTML.Tag.PARAM) ||
1359                            (kind == HTML.Tag.APPLET)) {
1360                     return new HiddenTagView(elem);
1361                 }
1362             }
1363             // If we get here, it's either an element we don't know about
1364             // or something from StyledDocument that doesn't have a mapping to HTML.
1365             String nm = (elementName != null) ? (String)elementName :
1366                                                 elem.getName();
1367             if (nm != null) {
1368                 if (nm.equals(AbstractDocument.ContentElementName)) {
1369                     return new LabelView(elem);
1370                 } else if (nm.equals(AbstractDocument.ParagraphElementName)) {
1371                     return new ParagraphView(elem);
1372                 } else if (nm.equals(AbstractDocument.SectionElementName)) {
1373                     return new BoxView(elem, View.Y_AXIS);
1374                 } else if (nm.equals(StyleConstants.ComponentElementName)) {
1375                     return new ComponentView(elem);
1376                 } else if (nm.equals(StyleConstants.IconElementName)) {
1377                     return new IconView(elem);
1378                 }
1379             }
1380 
1381             // default to text display
1382             return new LabelView(elem);
1383         }
1384 
1385         static class BodyBlockView extends BlockView implements ComponentListener {
1386             public BodyBlockView(Element elem) {
1387                 super(elem,View.Y_AXIS);
1388             }
1389             // reimplement major axis requirements to indicate that the
1390             // block is flexible for the body element... so that it can
1391             // be stretched to fill the background properly.
1392             protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) {
1393                 r = super.calculateMajorAxisRequirements(axis, r);
1394                 r.maximum = Integer.MAX_VALUE;
1395                 return r;
1396             }
1397 
1398             protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
1399                 Container container = getContainer();
1400                 Container parentContainer;
1401                 if (container != null
1402                     && (container instanceof javax.swing.JEditorPane)
1403                     && (parentContainer = container.getParent()) != null
1404                     && (parentContainer instanceof javax.swing.JViewport)) {
1405                     JViewport viewPort = (JViewport)parentContainer;
1406                     if (cachedViewPort != null) {
1407                         JViewport cachedObject = cachedViewPort.get();
1408                         if (cachedObject != null) {
1409                             if (cachedObject != viewPort) {
1410                                 cachedObject.removeComponentListener(this);
1411                             }
1412                         } else {
1413                             cachedViewPort = null;
1414                         }
1415                     }
1416                     if (cachedViewPort == null) {
1417                         viewPort.addComponentListener(this);
1418                         cachedViewPort = new WeakReference<JViewport>(viewPort);
1419                     }
1420 
1421                     componentVisibleWidth = viewPort.getExtentSize().width;
1422                     if (componentVisibleWidth > 0) {
1423                     Insets insets = container.getInsets();
1424                     viewVisibleWidth = componentVisibleWidth - insets.left - getLeftInset();
1425                     //try to use viewVisibleWidth if it is smaller than targetSpan
1426                     targetSpan = Math.min(targetSpan, viewVisibleWidth);
1427                     }
1428                 } else {
1429                     if (cachedViewPort != null) {
1430                         JViewport cachedObject = cachedViewPort.get();
1431                         if (cachedObject != null) {
1432                             cachedObject.removeComponentListener(this);
1433                         }
1434                         cachedViewPort = null;
1435                     }
1436                 }
1437                 super.layoutMinorAxis(targetSpan, axis, offsets, spans);
1438             }
1439 
1440             public void setParent(View parent) {
1441                 //if parent == null unregister component listener
1442                 if (parent == null) {
1443                     if (cachedViewPort != null) {
1444                         Object cachedObject;
1445                         if ((cachedObject = cachedViewPort.get()) != null) {
1446                             ((JComponent)cachedObject).removeComponentListener(this);
1447                         }
1448                         cachedViewPort = null;
1449                     }
1450                 }
1451                 super.setParent(parent);
1452             }
1453 
1454             public void componentResized(ComponentEvent e) {
1455                 if ( !(e.getSource() instanceof JViewport) ) {
1456                     return;
1457                 }
1458                 JViewport viewPort = (JViewport)e.getSource();
1459                 if (componentVisibleWidth != viewPort.getExtentSize().width) {
1460                     Document doc = getDocument();
1461                     if (doc instanceof AbstractDocument) {
1462                         AbstractDocument document = (AbstractDocument)getDocument();
1463                         document.readLock();
1464                         try {
1465                             layoutChanged(X_AXIS);
1466                             preferenceChanged(null, true, true);
1467                         } finally {
1468                             document.readUnlock();
1469                         }
1470 
1471                     }
1472                 }
1473             }
1474             public void componentHidden(ComponentEvent e) {
1475             }
1476             public void componentMoved(ComponentEvent e) {
1477             }
1478             public void componentShown(ComponentEvent e) {
1479             }
1480             /*
1481              * we keep weak reference to viewPort if and only if BodyBoxView is listening for ComponentEvents
1482              * only in that case cachedViewPort is not equal to null.
1483              * we need to keep this reference in order to remove BodyBoxView from viewPort listeners.
1484              *
1485              */
1486             private Reference<JViewport> cachedViewPort = null;
1487             private boolean isListening = false;
1488             private int viewVisibleWidth = Integer.MAX_VALUE;
1489             private int componentVisibleWidth = Integer.MAX_VALUE;
1490         }
1491 
1492     }
1493 
1494     // --- Action implementations ------------------------------
1495 
1496 /** The bold action identifier
1497 */
1498     public static final String  BOLD_ACTION = "html-bold-action";
1499 /** The italic action identifier
1500 */
1501     public static final String  ITALIC_ACTION = "html-italic-action";
1502 /** The paragraph left indent action identifier
1503 */
1504     public static final String  PARA_INDENT_LEFT = "html-para-indent-left";
1505 /** The paragraph right indent action identifier
1506 */
1507     public static final String  PARA_INDENT_RIGHT = "html-para-indent-right";
1508 /** The  font size increase to next value action identifier
1509 */
1510     public static final String  FONT_CHANGE_BIGGER = "html-font-bigger";
1511 /** The font size decrease to next value action identifier
1512 */
1513     public static final String  FONT_CHANGE_SMALLER = "html-font-smaller";
1514 /** The Color choice action identifier
1515      The color is passed as an argument
1516 */
1517     public static final String  COLOR_ACTION = "html-color-action";
1518 /** The logical style choice action identifier
1519      The logical style is passed in as an argument
1520 */
1521     public static final String  LOGICAL_STYLE_ACTION = "html-logical-style-action";
1522     /**
1523      * Align images at the top.
1524      */
1525     public static final String  IMG_ALIGN_TOP = "html-image-align-top";
1526 
1527     /**
1528      * Align images in the middle.
1529      */
1530     public static final String  IMG_ALIGN_MIDDLE = "html-image-align-middle";
1531 
1532     /**
1533      * Align images at the bottom.
1534      */
1535     public static final String  IMG_ALIGN_BOTTOM = "html-image-align-bottom";
1536 
1537     /**
1538      * Align images at the border.
1539      */
1540     public static final String  IMG_BORDER = "html-image-border";
1541 
1542 
1543     /** HTML used when inserting tables. */
1544     private static final String INSERT_TABLE_HTML = "<table border=1><tr><td></td></tr></table>";
1545 
1546     /** HTML used when inserting unordered lists. */
1547     private static final String INSERT_UL_HTML = "<ul><li></li></ul>";
1548 
1549     /** HTML used when inserting ordered lists. */
1550     private static final String INSERT_OL_HTML = "<ol><li></li></ol>";
1551 
1552     /** HTML used when inserting hr. */
1553     private static final String INSERT_HR_HTML = "<hr>";
1554 
1555     /** HTML used when inserting pre. */
1556     private static final String INSERT_PRE_HTML = "<pre></pre>";
1557 
1558     private static final NavigateLinkAction nextLinkAction =
1559         new NavigateLinkAction("next-link-action");
1560 
1561     private static final NavigateLinkAction previousLinkAction =
1562         new NavigateLinkAction("previous-link-action");
1563 
1564     private static final ActivateLinkAction activateLinkAction =
1565         new ActivateLinkAction("activate-link-action");
1566 
1567     private static final Action[] defaultActions = {
1568         new InsertHTMLTextAction("InsertTable", INSERT_TABLE_HTML,
1569                                  HTML.Tag.BODY, HTML.Tag.TABLE),
1570         new InsertHTMLTextAction("InsertTableRow", INSERT_TABLE_HTML,
1571                                  HTML.Tag.TABLE, HTML.Tag.TR,
1572                                  HTML.Tag.BODY, HTML.Tag.TABLE),
1573         new InsertHTMLTextAction("InsertTableDataCell", INSERT_TABLE_HTML,
1574                                  HTML.Tag.TR, HTML.Tag.TD,
1575                                  HTML.Tag.BODY, HTML.Tag.TABLE),
1576         new InsertHTMLTextAction("InsertUnorderedList", INSERT_UL_HTML,
1577                                  HTML.Tag.BODY, HTML.Tag.UL),
1578         new InsertHTMLTextAction("InsertUnorderedListItem", INSERT_UL_HTML,
1579                                  HTML.Tag.UL, HTML.Tag.LI,
1580                                  HTML.Tag.BODY, HTML.Tag.UL),
1581         new InsertHTMLTextAction("InsertOrderedList", INSERT_OL_HTML,
1582                                  HTML.Tag.BODY, HTML.Tag.OL),
1583         new InsertHTMLTextAction("InsertOrderedListItem", INSERT_OL_HTML,
1584                                  HTML.Tag.OL, HTML.Tag.LI,
1585                                  HTML.Tag.BODY, HTML.Tag.OL),
1586         new InsertHRAction(),
1587         new InsertHTMLTextAction("InsertPre", INSERT_PRE_HTML,
1588                                  HTML.Tag.BODY, HTML.Tag.PRE),
1589         nextLinkAction, previousLinkAction, activateLinkAction,
1590 
1591         new BeginAction(beginAction, false),
1592         new BeginAction(selectionBeginAction, true)
1593     };
1594 
1595     // link navigation support
1596     private boolean foundLink = false;
1597     private int prevHypertextOffset = -1;
1598     private Object linkNavigationTag;
1599 
1600 
1601     /**
1602      * An abstract Action providing some convenience methods that may
1603      * be useful in inserting HTML into an existing document.
1604      * <p>NOTE: None of the convenience methods obtain a lock on the
1605      * document. If you have another thread modifying the text these
1606      * methods may have inconsistent behavior, or return the wrong thing.
1607      */
1608     @SuppressWarnings("serial") // Superclass is not serializable across versions
1609     public abstract static class HTMLTextAction extends StyledTextAction {
1610 
1611         /**
1612          * Creates a new HTMLTextAction from a string action name.
1613          *
1614          * @param name the name of the action
1615          */
1616         public HTMLTextAction(String name) {
1617             super(name);
1618         }
1619 
1620         /**
1621          * @param e the JEditorPane
1622          * @return HTMLDocument of <code>e</code>.
1623          */
1624         protected HTMLDocument getHTMLDocument(JEditorPane e) {
1625             Document d = e.getDocument();
1626             if (d instanceof HTMLDocument) {
1627                 return (HTMLDocument) d;
1628             }
1629             throw new IllegalArgumentException("document must be HTMLDocument");
1630         }
1631 
1632         /**
1633          * @param e the JEditorPane
1634          * @return HTMLEditorKit for <code>e</code>.
1635          */
1636         protected HTMLEditorKit getHTMLEditorKit(JEditorPane e) {
1637             EditorKit k = e.getEditorKit();
1638             if (k instanceof HTMLEditorKit) {
1639                 return (HTMLEditorKit) k;
1640             }
1641             throw new IllegalArgumentException("EditorKit must be HTMLEditorKit");
1642         }
1643 
1644         /**
1645          * Returns an array of the Elements that contain <code>offset</code>.
1646          * The first elements corresponds to the root.
1647          *
1648          * @param doc an instance of HTMLDocument
1649          * @param offset value of offset
1650          * @return an array of the Elements that contain <code>offset</code>
1651          */
1652         protected Element[] getElementsAt(HTMLDocument doc, int offset) {
1653             return getElementsAt(doc.getDefaultRootElement(), offset, 0);
1654         }
1655 
1656         /**
1657          * Recursive method used by getElementsAt.
1658          */
1659         private Element[] getElementsAt(Element parent, int offset,
1660                                         int depth) {
1661             if (parent.isLeaf()) {
1662                 Element[] retValue = new Element[depth + 1];
1663                 retValue[depth] = parent;
1664                 return retValue;
1665             }
1666             Element[] retValue = getElementsAt(parent.getElement
1667                           (parent.getElementIndex(offset)), offset, depth + 1);
1668             retValue[depth] = parent;
1669             return retValue;
1670         }
1671 
1672         /**
1673          * Returns number of elements, starting at the deepest leaf, needed
1674          * to get to an element representing <code>tag</code>. This will
1675          * return -1 if no elements is found representing <code>tag</code>,
1676          * or 0 if the parent of the leaf at <code>offset</code> represents
1677          * <code>tag</code>.
1678          *
1679          * @param doc an instance of HTMLDocument
1680          * @param offset an offset to start from
1681          * @param tag tag to represent
1682          * @return number of elements
1683          */
1684         protected int elementCountToTag(HTMLDocument doc, int offset,
1685                                         HTML.Tag tag) {
1686             int depth = -1;
1687             Element e = doc.getCharacterElement(offset);
1688             while (e != null && e.getAttributes().getAttribute
1689                    (StyleConstants.NameAttribute) != tag) {
1690                 e = e.getParentElement();
1691                 depth++;
1692             }
1693             if (e == null) {
1694                 return -1;
1695             }
1696             return depth;
1697         }
1698 
1699         /**
1700          * Returns the deepest element at <code>offset</code> matching
1701          * <code>tag</code>.
1702          *
1703          * @param doc an instance of HTMLDocument
1704          * @param offset the specified offset &gt;= 0
1705          * @param tag an instance of HTML.Tag
1706          *
1707          * @return the deepest element
1708          */
1709         protected Element findElementMatchingTag(HTMLDocument doc, int offset,
1710                                                  HTML.Tag tag) {
1711             Element e = doc.getDefaultRootElement();
1712             Element lastMatch = null;
1713             while (e != null) {
1714                 if (e.getAttributes().getAttribute
1715                    (StyleConstants.NameAttribute) == tag) {
1716                     lastMatch = e;
1717                 }
1718                 e = e.getElement(e.getElementIndex(offset));
1719             }
1720             return lastMatch;
1721         }
1722     }
1723 
1724 
1725     /**
1726      * InsertHTMLTextAction can be used to insert an arbitrary string of HTML
1727      * into an existing HTML document. At least two HTML.Tags need to be
1728      * supplied. The first Tag, parentTag, identifies the parent in
1729      * the document to add the elements to. The second tag, addTag,
1730      * identifies the first tag that should be added to the document as
1731      * seen in the HTML string. One important thing to remember, is that
1732      * the parser is going to generate all the appropriate tags, even if
1733      * they aren't in the HTML string passed in.<p>
1734      * For example, lets say you wanted to create an action to insert
1735      * a table into the body. The parentTag would be HTML.Tag.BODY,
1736      * addTag would be HTML.Tag.TABLE, and the string could be something
1737      * like &lt;table&gt;&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;.
1738      * <p>There is also an option to supply an alternate parentTag and
1739      * addTag. These will be checked for if there is no parentTag at
1740      * offset.
1741      */
1742     @SuppressWarnings("serial") // Superclass is not serializable across versions
1743     public static class InsertHTMLTextAction extends HTMLTextAction {
1744 
1745         /**
1746          * Creates a new InsertHTMLTextAction.
1747          *
1748          * @param name a name of the action
1749          * @param html an HTML string
1750          * @param parentTag a parent tag
1751          * @param addTag the first tag to start inserting into document
1752          */
1753         public InsertHTMLTextAction(String name, String html,
1754                                     HTML.Tag parentTag, HTML.Tag addTag) {
1755             this(name, html, parentTag, addTag, null, null);
1756         }
1757 
1758         /**
1759          * Creates a new InsertHTMLTextAction.
1760          *
1761          * @param name a name of the action
1762          * @param html an HTML string
1763          * @param parentTag a parent tag
1764          * @param addTag the first tag to start inserting into document
1765          * @param alternateParentTag an alternative parent tag
1766          * @param alternateAddTag an alternative tag
1767          */
1768         public InsertHTMLTextAction(String name, String html,
1769                                     HTML.Tag parentTag,
1770                                     HTML.Tag addTag,
1771                                     HTML.Tag alternateParentTag,
1772                                     HTML.Tag alternateAddTag) {
1773             this(name, html, parentTag, addTag, alternateParentTag,
1774                  alternateAddTag, true);
1775         }
1776 
1777         /* public */
1778         InsertHTMLTextAction(String name, String html,
1779                                     HTML.Tag parentTag,
1780                                     HTML.Tag addTag,
1781                                     HTML.Tag alternateParentTag,
1782                                     HTML.Tag alternateAddTag,
1783                                     boolean adjustSelection) {
1784             super(name);
1785             this.html = html;
1786             this.parentTag = parentTag;
1787             this.addTag = addTag;
1788             this.alternateParentTag = alternateParentTag;
1789             this.alternateAddTag = alternateAddTag;
1790             this.adjustSelection = adjustSelection;
1791         }
1792 
1793         /**
1794          * A cover for HTMLEditorKit.insertHTML. If an exception it
1795          * thrown it is wrapped in a RuntimeException and thrown.
1796          *
1797          * @param editor an instance of JEditorPane
1798          * @param doc the document to insert into
1799          * @param offset the offset to insert HTML at
1800          * @param html an HTML string
1801          * @param popDepth the number of ElementSpec.EndTagTypes to generate
1802          *                  before inserting
1803          * @param pushDepth the number of ElementSpec.StartTagTypes with a direction
1804          *                  of ElementSpec.JoinNextDirection that should be generated
1805          *                  before inserting, but after the end tags have been generated
1806          * @param addTag the first tag to start inserting into document
1807          */
1808         protected void insertHTML(JEditorPane editor, HTMLDocument doc,
1809                                   int offset, String html, int popDepth,
1810                                   int pushDepth, HTML.Tag addTag) {
1811             try {
1812                 getHTMLEditorKit(editor).insertHTML(doc, offset, html,
1813                                                     popDepth, pushDepth,
1814                                                     addTag);
1815             } catch (IOException ioe) {
1816                 throw new RuntimeException("Unable to insert: " + ioe);
1817             } catch (BadLocationException ble) {
1818                 throw new RuntimeException("Unable to insert: " + ble);
1819             }
1820         }
1821 
1822         /**
1823          * This is invoked when inserting at a boundary. It determines
1824          * the number of pops, and then the number of pushes that need
1825          * to be performed, and then invokes insertHTML.
1826          *
1827          * @param editor an instance of JEditorPane
1828          * @param doc an instance of HTMLDocument
1829          * @param offset an offset to start from
1830          * @param insertElement an instance of Element
1831          * @param html an HTML string
1832          * @param parentTag a parent tag
1833          * @param addTag the first tag to start inserting into document
1834          *
1835          * @since 1.3
1836          *
1837          */
1838         protected void insertAtBoundary(JEditorPane editor, HTMLDocument doc,
1839                                         int offset, Element insertElement,
1840                                         String html, HTML.Tag parentTag,
1841                                         HTML.Tag addTag) {
1842             insertAtBoundry(editor, doc, offset, insertElement, html,
1843                             parentTag, addTag);
1844         }
1845 
1846         /**
1847          * This is invoked when inserting at a boundary. It determines
1848          * the number of pops, and then the number of pushes that need
1849          * to be performed, and then invokes insertHTML.
1850          * @deprecated As of Java 2 platform v1.3, use insertAtBoundary
1851          *
1852          * @param editor an instance of JEditorPane
1853          * @param doc an instance of HTMLDocument
1854          * @param offset an offset to start from
1855          * @param insertElement an instance of Element
1856          * @param html an HTML string
1857          * @param parentTag a parent tag
1858          * @param addTag the first tag to start inserting into document
1859          */
1860         @Deprecated
1861         protected void insertAtBoundry(JEditorPane editor, HTMLDocument doc,
1862                                        int offset, Element insertElement,
1863                                        String html, HTML.Tag parentTag,
1864                                        HTML.Tag addTag) {
1865             // Find the common parent.
1866             Element e;
1867             Element commonParent;
1868             boolean isFirst = (offset == 0);
1869 
1870             if (offset > 0 || insertElement == null) {
1871                 e = doc.getDefaultRootElement();
1872                 while (e != null && e.getStartOffset() != offset &&
1873                        !e.isLeaf()) {
1874                     e = e.getElement(e.getElementIndex(offset));
1875                 }
1876                 commonParent = (e != null) ? e.getParentElement() : null;
1877             }
1878             else {
1879                 // If inserting at the origin, the common parent is the
1880                 // insertElement.
1881                 commonParent = insertElement;
1882             }
1883             if (commonParent != null) {
1884                 // Determine how many pops to do.
1885                 int pops = 0;
1886                 int pushes = 0;
1887                 if (isFirst && insertElement != null) {
1888                     e = commonParent;
1889                     while (e != null && !e.isLeaf()) {
1890                         e = e.getElement(e.getElementIndex(offset));
1891                         pops++;
1892                     }
1893                 }
1894                 else {
1895                     e = commonParent;
1896                     offset--;
1897                     while (e != null && !e.isLeaf()) {
1898                         e = e.getElement(e.getElementIndex(offset));
1899                         pops++;
1900                     }
1901 
1902                     // And how many pushes
1903                     e = commonParent;
1904                     offset++;
1905                     while (e != null && e != insertElement) {
1906                         e = e.getElement(e.getElementIndex(offset));
1907                         pushes++;
1908                     }
1909                 }
1910                 pops = Math.max(0, pops - 1);
1911 
1912                 // And insert!
1913                 insertHTML(editor, doc, offset, html, pops, pushes, addTag);
1914             }
1915         }
1916 
1917         /**
1918          * If there is an Element with name <code>tag</code> at
1919          * <code>offset</code>, this will invoke either insertAtBoundary
1920          * or <code>insertHTML</code>. This returns true if there is
1921          * a match, and one of the inserts is invoked.
1922          */
1923         /*protected*/
1924         boolean insertIntoTag(JEditorPane editor, HTMLDocument doc,
1925                               int offset, HTML.Tag tag, HTML.Tag addTag) {
1926             Element e = findElementMatchingTag(doc, offset, tag);
1927             if (e != null && e.getStartOffset() == offset) {
1928                 insertAtBoundary(editor, doc, offset, e, html,
1929                                  tag, addTag);
1930                 return true;
1931             }
1932             else if (offset > 0) {
1933                 int depth = elementCountToTag(doc, offset - 1, tag);
1934                 if (depth != -1) {
1935                     insertHTML(editor, doc, offset, html, depth, 0, addTag);
1936                     return true;
1937                 }
1938             }
1939             return false;
1940         }
1941 
1942         /**
1943          * Called after an insertion to adjust the selection.
1944          */
1945         /* protected */
1946         void adjustSelection(JEditorPane pane, HTMLDocument doc,
1947                              int startOffset, int oldLength) {
1948             int newLength = doc.getLength();
1949             if (newLength != oldLength && startOffset < newLength) {
1950                 if (startOffset > 0) {
1951                     String text;
1952                     try {
1953                         text = doc.getText(startOffset - 1, 1);
1954                     } catch (BadLocationException ble) {
1955                         text = null;
1956                     }
1957                     if (text != null && text.length() > 0 &&
1958                         text.charAt(0) == '\n') {
1959                         pane.select(startOffset, startOffset);
1960                     }
1961                     else {
1962                         pane.select(startOffset + 1, startOffset + 1);
1963                     }
1964                 }
1965                 else {
1966                     pane.select(1, 1);
1967                 }
1968             }
1969         }
1970 
1971         /**
1972          * Inserts the HTML into the document.
1973          *
1974          * @param ae the event
1975          */
1976         public void actionPerformed(ActionEvent ae) {
1977             JEditorPane editor = getEditor(ae);
1978             if (editor != null) {
1979                 HTMLDocument doc = getHTMLDocument(editor);
1980                 int offset = editor.getSelectionStart();
1981                 int length = doc.getLength();
1982                 boolean inserted;
1983                 // Try first choice
1984                 if (!insertIntoTag(editor, doc, offset, parentTag, addTag) &&
1985                     alternateParentTag != null) {
1986                     // Then alternate.
1987                     inserted = insertIntoTag(editor, doc, offset,
1988                                              alternateParentTag,
1989                                              alternateAddTag);
1990                 }
1991                 else {
1992                     inserted = true;
1993                 }
1994                 if (adjustSelection && inserted) {
1995                     adjustSelection(editor, doc, offset, length);
1996                 }
1997             }
1998         }
1999 
2000         /** HTML to insert. */
2001         protected String html;
2002         /** Tag to check for in the document. */
2003         protected HTML.Tag parentTag;
2004         /** Tag in HTML to start adding tags from. */
2005         protected HTML.Tag addTag;
2006         /** Alternate Tag to check for in the document if parentTag is
2007          * not found. */
2008         protected HTML.Tag alternateParentTag;
2009         /** Alternate tag in HTML to start adding tags from if parentTag
2010          * is not found and alternateParentTag is found. */
2011         protected HTML.Tag alternateAddTag;
2012         /** True indicates the selection should be adjusted after an insert. */
2013         boolean adjustSelection;
2014     }
2015 
2016 
2017     /**
2018      * InsertHRAction is special, at actionPerformed time it will determine
2019      * the parent HTML.Tag based on the paragraph element at the selection
2020      * start.
2021      */
2022     @SuppressWarnings("serial") // Superclass is not serializable across versions
2023     static class InsertHRAction extends InsertHTMLTextAction {
2024         InsertHRAction() {
2025             super("InsertHR", "<hr>", null, HTML.Tag.IMPLIED, null, null,
2026                   false);
2027         }
2028 
2029         /**
2030          * Inserts the HTML into the document.
2031          *
2032          * @param ae the event
2033          */
2034         public void actionPerformed(ActionEvent ae) {
2035             JEditorPane editor = getEditor(ae);
2036             if (editor != null) {
2037                 HTMLDocument doc = getHTMLDocument(editor);
2038                 int offset = editor.getSelectionStart();
2039                 Element paragraph = doc.getParagraphElement(offset);
2040                 if (paragraph.getParentElement() != null) {
2041                     parentTag = (HTML.Tag)paragraph.getParentElement().
2042                                   getAttributes().getAttribute
2043                                   (StyleConstants.NameAttribute);
2044                     super.actionPerformed(ae);
2045                 }
2046             }
2047         }
2048 
2049     }
2050 
2051     /*
2052      * Returns the object in an AttributeSet matching a key
2053      */
2054     private static Object getAttrValue(AttributeSet attr, HTML.Attribute key) {
2055         Enumeration<?> names = attr.getAttributeNames();
2056         while (names.hasMoreElements()) {
2057             Object nextKey = names.nextElement();
2058             Object nextVal = attr.getAttribute(nextKey);
2059             if (nextVal instanceof AttributeSet) {
2060                 Object value = getAttrValue((AttributeSet)nextVal, key);
2061                 if (value != null) {
2062                     return value;
2063                 }
2064             } else if (nextKey == key) {
2065                 return nextVal;
2066             }
2067         }
2068         return null;
2069     }
2070 
2071     /*
2072      * Action to move the focus on the next or previous hypertext link
2073      * or object. TODO: This method relies on support from the
2074      * javax.accessibility package.  The text package should support
2075      * keyboard navigation of text elements directly.
2076      */
2077     @SuppressWarnings("serial") // Superclass is not serializable across versions
2078     static class NavigateLinkAction extends TextAction implements CaretListener {
2079 
2080         private static final FocusHighlightPainter focusPainter =
2081             new FocusHighlightPainter(null);
2082         private final boolean focusBack;
2083 
2084         /*
2085          * Create this action with the appropriate identifier.
2086          */
2087         public NavigateLinkAction(String actionName) {
2088             super(actionName);
2089             focusBack = "previous-link-action".equals(actionName);
2090         }
2091 
2092         /**
2093          * Called when the caret position is updated.
2094          *
2095          * @param e the caret event
2096          */
2097         public void caretUpdate(CaretEvent e) {
2098             Object src = e.getSource();
2099             if (src instanceof JTextComponent) {
2100                 JTextComponent comp = (JTextComponent) src;
2101                 HTMLEditorKit kit = getHTMLEditorKit(comp);
2102                 if (kit != null && kit.foundLink) {
2103                     kit.foundLink = false;
2104                     // TODO: The AccessibleContext for the editor should register
2105                     // as a listener for CaretEvents and forward the events to
2106                     // assistive technologies listening for such events.
2107                     comp.getAccessibleContext().firePropertyChange(
2108                         AccessibleContext.ACCESSIBLE_HYPERTEXT_OFFSET,
2109                         Integer.valueOf(kit.prevHypertextOffset),
2110                         Integer.valueOf(e.getDot()));
2111                 }
2112             }
2113         }
2114 
2115         /*
2116          * The operation to perform when this action is triggered.
2117          */
2118         public void actionPerformed(ActionEvent e) {
2119             JTextComponent comp = getTextComponent(e);
2120             if (comp == null || comp.isEditable()) {
2121                 return;
2122             }
2123 
2124             Document doc = comp.getDocument();
2125             HTMLEditorKit kit = getHTMLEditorKit(comp);
2126             if (doc == null || kit == null) {
2127                 return;
2128             }
2129 
2130             // TODO: Should start successive iterations from the
2131             // current caret position.
2132             ElementIterator ei = new ElementIterator(doc);
2133             int currentOffset = comp.getCaretPosition();
2134             int prevStartOffset = -1;
2135             int prevEndOffset = -1;
2136 
2137             // highlight the next link or object after the current caret position
2138             Element nextElement;
2139             while ((nextElement = ei.next()) != null) {
2140                 String name = nextElement.getName();
2141                 AttributeSet attr = nextElement.getAttributes();
2142 
2143                 Object href = getAttrValue(attr, HTML.Attribute.HREF);
2144                 if (!(name.equals(HTML.Tag.OBJECT.toString())) && href == null) {
2145                     continue;
2146                 }
2147 
2148                 int elementOffset = nextElement.getStartOffset();
2149                 if (focusBack) {
2150                     if (elementOffset >= currentOffset &&
2151                         prevStartOffset >= 0) {
2152 
2153                         kit.foundLink = true;
2154                         comp.setCaretPosition(prevStartOffset);
2155                         moveCaretPosition(comp, kit, prevStartOffset,
2156                                           prevEndOffset);
2157                         kit.prevHypertextOffset = prevStartOffset;
2158                         return;
2159                     }
2160                 } else { // focus forward
2161                     if (elementOffset > currentOffset) {
2162 
2163                         kit.foundLink = true;
2164                         comp.setCaretPosition(elementOffset);
2165                         moveCaretPosition(comp, kit, elementOffset,
2166                                           nextElement.getEndOffset());
2167                         kit.prevHypertextOffset = elementOffset;
2168                         return;
2169                     }
2170                 }
2171                 prevStartOffset = nextElement.getStartOffset();
2172                 prevEndOffset = nextElement.getEndOffset();
2173             }
2174             if (focusBack && prevStartOffset >= 0) {
2175                 kit.foundLink = true;
2176                 comp.setCaretPosition(prevStartOffset);
2177                 moveCaretPosition(comp, kit, prevStartOffset, prevEndOffset);
2178                 kit.prevHypertextOffset = prevStartOffset;
2179             }
2180         }
2181 
2182         /*
2183          * Moves the caret from mark to dot
2184          */
2185         private void moveCaretPosition(JTextComponent comp, HTMLEditorKit kit,
2186                                        int mark, int dot) {
2187             Highlighter h = comp.getHighlighter();
2188             if (h != null) {
2189                 int p0 = Math.min(dot, mark);
2190                 int p1 = Math.max(dot, mark);
2191                 try {
2192                     if (kit.linkNavigationTag != null) {
2193                         h.changeHighlight(kit.linkNavigationTag, p0, p1);
2194                     } else {
2195                         kit.linkNavigationTag =
2196                                 h.addHighlight(p0, p1, focusPainter);
2197                     }
2198                 } catch (BadLocationException e) {
2199                 }
2200             }
2201         }
2202 
2203         private HTMLEditorKit getHTMLEditorKit(JTextComponent comp) {
2204             if (comp instanceof JEditorPane) {
2205                 EditorKit kit = ((JEditorPane) comp).getEditorKit();
2206                 if (kit instanceof HTMLEditorKit) {
2207                     return (HTMLEditorKit) kit;
2208                 }
2209             }
2210             return null;
2211         }
2212 
2213         /**
2214          * A highlight painter that draws a one-pixel border around
2215          * the highlighted area.
2216          */
2217         static class FocusHighlightPainter extends
2218             DefaultHighlighter.DefaultHighlightPainter {
2219 
2220             FocusHighlightPainter(Color color) {
2221                 super(color);
2222             }
2223 
2224             /**
2225              * Paints a portion of a highlight.
2226              *
2227              * @param g the graphics context
2228              * @param offs0 the starting model offset &ge; 0
2229              * @param offs1 the ending model offset &ge; offs1
2230              * @param bounds the bounding box of the view, which is not
2231              *               necessarily the region to paint.
2232              * @param c the editor
2233              * @param view View painting for
2234              * @return region in which drawing occurred
2235              */
2236             public Shape paintLayer(Graphics g, int offs0, int offs1,
2237                                     Shape bounds, JTextComponent c, View view) {
2238 
2239                 Color color = getColor();
2240 
2241                 if (color == null) {
2242                     g.setColor(c.getSelectionColor());
2243                 }
2244                 else {
2245                     g.setColor(color);
2246                 }
2247                 if (offs0 == view.getStartOffset() &&
2248                     offs1 == view.getEndOffset()) {
2249                     // Contained in view, can just use bounds.
2250                     Rectangle alloc;
2251                     if (bounds instanceof Rectangle) {
2252                         alloc = (Rectangle)bounds;
2253                     }
2254                     else {
2255                         alloc = bounds.getBounds();
2256                     }
2257                     g.drawRect(alloc.x, alloc.y, alloc.width - 1, alloc.height);
2258                     return alloc;
2259                 }
2260                 else {
2261                     // Should only render part of View.
2262                     try {
2263                         // --- determine locations ---
2264                         Shape shape = view.modelToView(offs0, Position.Bias.Forward,
2265                                                        offs1,Position.Bias.Backward,
2266                                                        bounds);
2267                         Rectangle r = (shape instanceof Rectangle) ?
2268                             (Rectangle)shape : shape.getBounds();
2269                         g.drawRect(r.x, r.y, r.width - 1, r.height);
2270                         return r;
2271                     } catch (BadLocationException e) {
2272                         // can't render
2273                     }
2274                 }
2275                 // Only if exception
2276                 return null;
2277             }
2278         }
2279     }
2280 
2281     /*
2282      * Action to activate the hypertext link that has focus.
2283      * TODO: This method relies on support from the
2284      * javax.accessibility package.  The text package should support
2285      * keyboard navigation of text elements directly.
2286      */
2287     @SuppressWarnings("serial") // Superclass is not serializable across versions
2288     static class ActivateLinkAction extends TextAction {
2289 
2290         /**
2291          * Create this action with the appropriate identifier.
2292          */
2293         public ActivateLinkAction(String actionName) {
2294             super(actionName);
2295         }
2296 
2297         /*
2298          * activates the hyperlink at offset
2299          */
2300         private void activateLink(String href, HTMLDocument doc,
2301                                   JEditorPane editor, int offset) {
2302             try {
2303                 URL page =
2304                     (URL)doc.getProperty(Document.StreamDescriptionProperty);
2305                 URL url = new URL(page, href);
2306                 HyperlinkEvent linkEvent = new HyperlinkEvent
2307                     (editor, HyperlinkEvent.EventType.
2308                      ACTIVATED, url, url.toExternalForm(),
2309                      doc.getCharacterElement(offset));
2310                 editor.fireHyperlinkUpdate(linkEvent);
2311             } catch (MalformedURLException m) {
2312             }
2313         }
2314 
2315         /*
2316          * Invokes default action on the object in an element
2317          */
2318         private void doObjectAction(JEditorPane editor, Element elem) {
2319             View view = getView(editor, elem);
2320             if (view != null && view instanceof ObjectView) {
2321                 Component comp = ((ObjectView)view).getComponent();
2322                 if (comp != null && comp instanceof Accessible) {
2323                     AccessibleContext ac = comp.getAccessibleContext();
2324                     if (ac != null) {
2325                         AccessibleAction aa = ac.getAccessibleAction();
2326                         if (aa != null) {
2327                             aa.doAccessibleAction(0);
2328                         }
2329                     }
2330                 }
2331             }
2332         }
2333 
2334         /*
2335          * Returns the root view for a document
2336          */
2337         private View getRootView(JEditorPane editor) {
2338             return editor.getUI().getRootView(editor);
2339         }
2340 
2341         /*
2342          * Returns a view associated with an element
2343          */
2344         private View getView(JEditorPane editor, Element elem) {
2345             Object lock = lock(editor);
2346             try {
2347                 View rootView = getRootView(editor);
2348                 int start = elem.getStartOffset();
2349                 if (rootView != null) {
2350                     return getView(rootView, elem, start);
2351                 }
2352                 return null;
2353             } finally {
2354                 unlock(lock);
2355             }
2356         }
2357 
2358         private View getView(View parent, Element elem, int start) {
2359             if (parent.getElement() == elem) {
2360                 return parent;
2361             }
2362             int index = parent.getViewIndex(start, Position.Bias.Forward);
2363 
2364             if (index != -1 && index < parent.getViewCount()) {
2365                 return getView(parent.getView(index), elem, start);
2366             }
2367             return null;
2368         }
2369 
2370         /*
2371          * If possible acquires a lock on the Document.  If a lock has been
2372          * obtained a key will be retured that should be passed to
2373          * <code>unlock</code>.
2374          */
2375         private Object lock(JEditorPane editor) {
2376             Document document = editor.getDocument();
2377 
2378             if (document instanceof AbstractDocument) {
2379                 ((AbstractDocument)document).readLock();
2380                 return document;
2381             }
2382             return null;
2383         }
2384 
2385         /*
2386          * Releases a lock previously obtained via <code>lock</code>.
2387          */
2388         private void unlock(Object key) {
2389             if (key != null) {
2390                 ((AbstractDocument)key).readUnlock();
2391             }
2392         }
2393 
2394         /*
2395          * The operation to perform when this action is triggered.
2396          */
2397         public void actionPerformed(ActionEvent e) {
2398 
2399             JTextComponent c = getTextComponent(e);
2400             if (c.isEditable() || !(c instanceof JEditorPane)) {
2401                 return;
2402             }
2403             JEditorPane editor = (JEditorPane)c;
2404 
2405             Document d = editor.getDocument();
2406             if (d == null || !(d instanceof HTMLDocument)) {
2407                 return;
2408             }
2409             HTMLDocument doc = (HTMLDocument)d;
2410 
2411             ElementIterator ei = new ElementIterator(doc);
2412             int currentOffset = editor.getCaretPosition();
2413 
2414             // invoke the next link or object action
2415             String urlString = null;
2416             String objString = null;
2417             Element currentElement;
2418             while ((currentElement = ei.next()) != null) {
2419                 String name = currentElement.getName();
2420                 AttributeSet attr = currentElement.getAttributes();
2421 
2422                 Object href = getAttrValue(attr, HTML.Attribute.HREF);
2423                 if (href != null) {
2424                     if (currentOffset >= currentElement.getStartOffset() &&
2425                         currentOffset <= currentElement.getEndOffset()) {
2426 
2427                         activateLink((String)href, doc, editor, currentOffset);
2428                         return;
2429                     }
2430                 } else if (name.equals(HTML.Tag.OBJECT.toString())) {
2431                     Object obj = getAttrValue(attr, HTML.Attribute.CLASSID);
2432                     if (obj != null) {
2433                         if (currentOffset >= currentElement.getStartOffset() &&
2434                             currentOffset <= currentElement.getEndOffset()) {
2435 
2436                             doObjectAction(editor, currentElement);
2437                             return;
2438                         }
2439                     }
2440                 }
2441             }
2442         }
2443     }
2444 
2445     private static int getBodyElementStart(JTextComponent comp) {
2446         Element rootElement = comp.getDocument().getRootElements()[0];
2447         for (int i = 0; i < rootElement.getElementCount(); i++) {
2448             Element currElement = rootElement.getElement(i);
2449             if("body".equals(currElement.getName())) {
2450                 return currElement.getStartOffset();
2451             }
2452         }
2453         return 0;
2454     }
2455 
2456     /*
2457      * Move the caret to the beginning of the document.
2458      * @see DefaultEditorKit#beginAction
2459      * @see HTMLEditorKit#getActions
2460      */
2461     @SuppressWarnings("serial") // Superclass is not serializable across versions
2462     static class BeginAction extends TextAction {
2463 
2464         /* Create this object with the appropriate identifier. */
2465         BeginAction(String nm, boolean select) {
2466             super(nm);
2467             this.select = select;
2468         }
2469 
2470         /** The operation to perform when this action is triggered. */
2471         public void actionPerformed(ActionEvent e) {
2472             JTextComponent target = getTextComponent(e);
2473             int bodyStart = getBodyElementStart(target);
2474 
2475             if (target != null) {
2476                 if (select) {
2477                     target.moveCaretPosition(bodyStart);
2478                 } else {
2479                     target.setCaretPosition(bodyStart);
2480                 }
2481             }
2482         }
2483 
2484         private boolean select;
2485     }
2486 }