51 * The <applet> tag is not supported, but some support is provided
52 * for the <object> tag.
53 * <p>
54 * There are several goals of the HTML EditorKit provided, that have
55 * an effect upon the way that HTML is modeled. These
56 * have influenced its design in a substantial way.
57 * <dl>
58 * <dt>
59 * Support editing
60 * <dd>
61 * It might seem fairly obvious that a plug-in for JEditorPane
62 * should provide editing support, but that fact has several
63 * design considerations. There are a substantial number of HTML
64 * documents that don't properly conform to an HTML specification.
65 * These must be normalized somewhat into a correct form if one
66 * is to edit them. Additionally, users don't like to be presented
67 * with an excessive amount of structure editing, so using traditional
68 * text editing gestures is preferred over using the HTML structure
69 * exactly as defined in the HTML document.
70 * <p>
71 * The modeling of HTML is provided by the class <code>HTMLDocument</code>.
72 * Its documentation describes the details of how the HTML is modeled.
73 * The editing support leverages heavily off of the text package.
74 *
75 * <dt>
76 * Extendable/Scalable
77 * <dd>
78 * To maximize the usefulness of this kit, a great deal of effort
79 * has gone into making it extendable. These are some of the
80 * features.
81 * <ol>
82 * <li>
83 * The parser is replaceable. The default parser is the Hot Java
84 * parser which is DTD based. A different DTD can be used, or an
85 * entirely different parser can be used. To change the parser,
86 * reimplement the getParser method. The default parser is
87 * dynamically loaded when first asked for, so the class files
88 * will never be loaded if an alternative parser is used. The
89 * default parser is in a separate package called parser below
90 * this package.
91 * <li>
92 * The parser drives the ParserCallback, which is provided by
93 * HTMLDocument. To change the callback, subclass HTMLDocument
94 * and reimplement the createDefaultDocument method to return
95 * document that produces a different reader. The reader controls
96 * how the document is structured. Although the Document provides
97 * HTML support by default, there is nothing preventing support of
98 * non-HTML tags that result in alternative element structures.
99 * <li>
100 * The default view of the models are provided as a hierarchy of
101 * View implementations, so one can easily customize how a particular
102 * element is displayed or add capabilities for new kinds of elements
103 * by providing new View implementations. The default set of views
104 * are provided by the <code>HTMLFactory</code> class. This can
105 * be easily changed by subclassing or replacing the HTMLFactory
106 * and reimplementing the getViewFactory method to return the alternative
107 * factory.
108 * <li>
109 * The View implementations work primarily off of CSS attributes,
110 * which are kept in the views. This makes it possible to have
111 * multiple views mapped over the same model that appear substantially
112 * different. This can be especially useful for printing. For
113 * most HTML attributes, the HTML attributes are converted to CSS
114 * attributes for display. This helps make the View implementations
115 * more general purpose
116 * </ol>
117 *
118 * <dt>
119 * Asynchronous Loading
120 * <dd>
121 * Larger documents involve a lot of parsing and take some time
122 * to load. By default, this kit produces documents that will be
123 * loaded asynchronously if loaded using <code>JEditorPane.setPage</code>.
124 * This is controlled by a property on the document. The method
125 * {@link #createDefaultDocument createDefaultDocument} can
126 * be overriden to change this. The batching of work is done
127 * by the <code>HTMLDocument.HTMLReader</code> class. The actual
128 * work is done by the <code>DefaultStyledDocument</code> and
129 * <code>AbstractDocument</code> classes in the text package.
130 *
131 * <dt>
132 * Customization from current LAF
133 * <dd>
134 * HTML provides a well known set of features without exactly
135 * specifying the display characteristics. Swing has a theme
136 * mechanism for its look-and-feel implementations. It is desirable
137 * for the look-and-feel to feed display characteristics into the
138 * HTML views. An user with poor vision for example would want
139 * high contrast and larger than typical fonts.
140 * <p>
141 * The support for this is provided by the <code>StyleSheet</code>
142 * class. The presentation of the HTML can be heavily influenced
143 * by the setting of the StyleSheet property on the EditorKit.
144 *
145 * <dt>
146 * Not lossy
147 * <dd>
148 * An EditorKit has the ability to be read and save documents.
149 * It is generally the most pleasing to users if there is no loss
150 * of data between the two operation. The policy of the HTMLEditorKit
151 * will be to store things not recognized or not necessarily visible
152 * so they can be subsequently written out. The model of the HTML document
153 * should therefore contain all information discovered while reading the
154 * document. This is constrained in some ways by the need to support
155 * editing (i.e. incorrect documents sometimes must be normalized).
156 * The guiding principle is that information shouldn't be lost, but
157 * some might be synthesized to produce a more correct model or it might
158 * be rearranged.
159 * </dl>
160 *
161 * @author Timothy Prinzing
162 */
163 @SuppressWarnings("serial") // Same-version serialization only
164 public class HTMLEditorKit extends StyledEditorKit implements Accessible {
165
166 private JEditorPane theEditor;
167
168 /**
169 * Constructs an HTMLEditorKit, creates a StyleContext,
170 * and loads the style sheet.
171 */
172 public HTMLEditorKit() {
173
174 }
175
176 /**
177 * Get the MIME type of the data that this
178 * kit represents support for. This kit supports
179 * the type <code>text/html</code>.
180 *
181 * @return the type
182 */
183 public String getContentType() {
184 return "text/html";
185 }
186
187 /**
188 * Fetch a factory that is suitable for producing
189 * views of any models that are produced by this
190 * kit.
191 *
192 * @return the factory
193 */
194 public ViewFactory getViewFactory() {
195 return defaultFactory;
196 }
197
198 /**
199 * Create an uninitialized text storage model
214 return doc;
215 }
216
217 /**
218 * Try to get an HTML parser from the document. If no parser is set for
219 * the document, return the editor kit's default parser. It is an error
220 * if no parser could be obtained from the editor kit.
221 */
222 private Parser ensureParser(HTMLDocument doc) throws IOException {
223 Parser p = doc.getParser();
224 if (p == null) {
225 p = getParser();
226 }
227 if (p == null) {
228 throw new IOException("Can't load parser");
229 }
230 return p;
231 }
232
233 /**
234 * Inserts content from the given stream. If <code>doc</code> is
235 * an instance of HTMLDocument, this will read
236 * HTML 3.2 text. Inserting HTML into a non-empty document must be inside
237 * the body Element, if you do not insert into the body an exception will
238 * be thrown. When inserting into a non-empty document all tags outside
239 * of the body (head, title) will be dropped.
240 *
241 * @param in the stream to read from
242 * @param doc the destination for the insertion
243 * @param pos the location in the document to place the
244 * content
245 * @exception IOException on any I/O error
246 * @exception BadLocationException if pos represents an invalid
247 * location within the document
248 * @exception RuntimeException (will eventually be a BadLocationException)
249 * if pos is invalid
250 */
251 public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {
252
253 if (doc instanceof HTMLDocument) {
254 HTMLDocument hdoc = (HTMLDocument) doc;
428 new PrivilegedAction<InputStream>() {
429 public InputStream run() {
430 return HTMLEditorKit.class.getResourceAsStream(name);
431 }
432 });
433 }
434
435 /**
436 * Fetches the command list for the editor. This is
437 * the list of commands supported by the superclass
438 * augmented by the collection of commands defined
439 * locally for style operations.
440 *
441 * @return the command list
442 */
443 public Action[] getActions() {
444 return TextAction.augmentList(super.getActions(), defaultActions);
445 }
446
447 /**
448 * Copies the key/values in <code>element</code>s AttributeSet into
449 * <code>set</code>. This does not copy component, icon, or element
450 * names attributes. Subclasses may wish to refine what is and what
451 * isn't copied here. But be sure to first remove all the attributes that
452 * are in <code>set</code>.<p>
453 * This is called anytime the caret moves over a different location.
454 *
455 */
456 protected void createInputAttributes(Element element,
457 MutableAttributeSet set) {
458 set.removeAttributes(set);
459 set.addAttributes(element.getAttributes());
460 set.removeAttribute(StyleConstants.ComposedTextAttribute);
461
462 Object o = set.getAttribute(StyleConstants.NameAttribute);
463 if (o instanceof HTML.Tag) {
464 HTML.Tag tag = (HTML.Tag)o;
465 // PENDING: we need a better way to express what shouldn't be
466 // copied when editing...
467 if(tag == HTML.Tag.IMG) {
468 // Remove the related image attributes, src, width, height
469 set.removeAttribute(HTML.Attribute.SRC);
470 set.removeAttribute(HTML.Attribute.HEIGHT);
471 set.removeAttribute(HTML.Attribute.WIDTH);
472 set.addAttribute(StyleConstants.NameAttribute,
540 *
541 * @since 1.3
542 */
543 public void setLinkCursor(Cursor cursor) {
544 linkCursor = cursor;
545 }
546
547 /**
548 * Returns the cursor to use over hyper links.
549 *
550 * @return the cursor
551 *
552 * @since 1.3
553 */
554 public Cursor getLinkCursor() {
555 return linkCursor;
556 }
557
558 /**
559 * Indicates whether an html form submission is processed automatically
560 * or only <code>FormSubmitEvent</code> is fired.
561 *
562 * @return true if html form submission is processed automatically,
563 * false otherwise.
564 *
565 * @see #setAutoFormSubmission
566 * @since 1.5
567 */
568 public boolean isAutoFormSubmission() {
569 return isAutoFormSubmission;
570 }
571
572 /**
573 * Specifies if an html form submission is processed
574 * automatically or only <code>FormSubmitEvent</code> is fired.
575 * By default it is set to true.
576 *
577 * @param isAuto if {@code true}, html form submission is processed automatically.
578 *
579 * @see #isAutoFormSubmission()
580 * @see FormSubmitEvent
581 * @since 1.5
582 */
583 public void setAutoFormSubmission(boolean isAuto) {
584 isAutoFormSubmission = isAuto;
585 }
586
587 /**
588 * Creates a copy of the editor kit.
589 *
590 * @return the copy
591 */
592 public Object clone() {
593 HTMLEditorKit o = (HTMLEditorKit)super.clone();
594 if (o != null) {
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 private boolean doesElementContainLocation(JEditorPane editor,
820 Element e, int offset,
821 int x, int y) {
822 if (e != null && offset > 0 && e.getStartOffset() == offset) {
823 try {
824 TextUI ui = editor.getUI();
825 Shape s1 = ui.modelToView(editor, offset,
826 Position.Bias.Forward);
827 if (s1 == null) {
828 return false;
829 }
830 Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle)s1 :
831 s1.getBounds();
832 Shape s2 = ui.modelToView(editor, e.getEndOffset(),
833 Position.Bias.Backward);
834 if (s2 != null) {
835 Rectangle r2 = (s2 instanceof Rectangle) ? (Rectangle)s2 :
836 s2.getBounds();
842 }
843 return true;
844 }
845
846 /**
847 * Calls linkActivated on the associated JEditorPane
848 * if the given position represents a link.<p>This is implemented
849 * to forward to the method with the same name, but with the following
850 * args both == -1.
851 *
852 * @param pos the position
853 * @param editor the editor pane
854 */
855 protected void activateLink(int pos, JEditorPane editor) {
856 activateLink(pos, editor, null);
857 }
858
859 /**
860 * Calls linkActivated on the associated JEditorPane
861 * if the given position represents a link. If this was the result
862 * of a mouse click, <code>x</code> and
863 * <code>y</code> will give the location of the mouse, otherwise
864 * they will be {@literal <} 0.
865 *
866 * @param pos the position
867 * @param html the editor pane
868 */
869 void activateLink(int pos, JEditorPane html, MouseEvent mouseEvent) {
870 Document doc = html.getDocument();
871 if (doc instanceof HTMLDocument) {
872 HTMLDocument hdoc = (HTMLDocument) doc;
873 Element e = hdoc.getCharacterElement(pos);
874 AttributeSet a = e.getAttributes();
875 AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A);
876 HyperlinkEvent linkEvent = null;
877 String description;
878 int x = -1;
879 int y = -1;
880
881 if (mouseEvent != null) {
882 x = mouseEvent.getX();
883 y = mouseEvent.getY();
885
886 if (anchor == null) {
887 href = getMapHREF(html, hdoc, e, a, pos, x, y);
888 }
889 else {
890 href = (String)anchor.getAttribute(HTML.Attribute.HREF);
891 }
892
893 if (href != null) {
894 linkEvent = createHyperlinkEvent(html, hdoc, href, anchor,
895 e, mouseEvent);
896 }
897 if (linkEvent != null) {
898 html.fireHyperlinkUpdate(linkEvent);
899 }
900 }
901 }
902
903 /**
904 * Creates and returns a new instance of HyperlinkEvent. If
905 * <code>hdoc</code> is a frame document a HTMLFrameHyperlinkEvent
906 * will be created.
907 */
908 HyperlinkEvent createHyperlinkEvent(JEditorPane html,
909 HTMLDocument hdoc, String href,
910 AttributeSet anchor,
911 Element element,
912 MouseEvent mouseEvent) {
913 URL u;
914 try {
915 URL base = hdoc.getBase();
916 u = new URL(base, href);
917 // Following is a workaround for 1.2, in which
918 // new URL("file://...", "#...") causes the filename to
919 // be lost.
920 if (href != null && "file".equals(u.getProtocol()) &&
921 href.startsWith("#")) {
922 String baseFile = base.getFile();
923 String newFile = u.getFile();
924 if (baseFile != null && newFile != null &&
925 !newFile.startsWith(baseFile)) {
988 * implementation provided by this editor kit.
989 */
990 public abstract static class Parser {
991 /**
992 * Parse the given stream and drive the given callback
993 * with the results of the parse. This method should
994 * be implemented to be thread-safe.
995 *
996 * @param r a reader
997 * @param cb a parser callback
998 * @param ignoreCharSet if {@code true} charset is ignoring
999 * @throws IOException if an I/O exception occurs
1000 */
1001 public abstract void parse(Reader r, ParserCallback cb, boolean ignoreCharSet) throws IOException;
1002
1003 }
1004
1005 /**
1006 * The result of parsing drives these callback methods.
1007 * The open and close actions should be balanced. The
1008 * <code>flush</code> method will be the last method
1009 * called, to give the receiver a chance to flush any
1010 * pending data into the document.
1011 * <p>Refer to DocumentParser, the default parser used, for further
1012 * information on the contents of the AttributeSets, the positions, and
1013 * other info.
1014 *
1015 * @see javax.swing.text.html.parser.DocumentParser
1016 */
1017 public static class ParserCallback {
1018 /**
1019 * This is passed as an attribute in the attributeset to indicate
1020 * the element is implied eg, the string '<>foo<\t>'
1021 * contains an implied html element and an implied body element.
1022 *
1023 * @since 1.3
1024 */
1025 public static final Object IMPLIED = "_implied_";
1026
1027 /**
1028 * The last method called on the reader. It allows
1083 *
1084 * @param t an HTML tag
1085 * @param a a set of attributes
1086 * @param pos a position
1087 */
1088 public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
1089 }
1090
1091 /**
1092 * Callback from the parser. Route to the appropriate
1093 * handler for the error.
1094 *
1095 * @param errorMsg a error message
1096 * @param pos a position
1097 */
1098 public void handleError(String errorMsg, int pos) {
1099 }
1100
1101 /**
1102 * This is invoked after the stream has been parsed, but before
1103 * <code>flush</code>. <code>eol</code> will be one of \n, \r
1104 * or \r\n, which ever is encountered the most in parsing the
1105 * stream.
1106 *
1107 * @param eol value of eol
1108 *
1109 * @since 1.3
1110 */
1111 public void handleEndOfLineString(String eol) {
1112 }
1113 }
1114
1115 /**
1116 * A factory to build views for HTML. The following
1117 * table describes what this factory will build by
1118 * default.
1119 *
1120 * <table summary="Describes the tag and view created by this factory by default">
1121 * <tr>
1122 * <th align=left>Tag<th align=left>View created
1123 * </tr><tr>
1560 * An abstract Action providing some convenience methods that may
1561 * be useful in inserting HTML into an existing document.
1562 * <p>NOTE: None of the convenience methods obtain a lock on the
1563 * document. If you have another thread modifying the text these
1564 * methods may have inconsistent behavior, or return the wrong thing.
1565 */
1566 @SuppressWarnings("serial") // Superclass is not serializable across versions
1567 public abstract static class HTMLTextAction extends StyledTextAction {
1568
1569 /**
1570 * Creates a new HTMLTextAction from a string action name.
1571 *
1572 * @param name the name of the action
1573 */
1574 public HTMLTextAction(String name) {
1575 super(name);
1576 }
1577
1578 /**
1579 * @param e the JEditorPane
1580 * @return HTMLDocument of <code>e</code>.
1581 */
1582 protected HTMLDocument getHTMLDocument(JEditorPane e) {
1583 Document d = e.getDocument();
1584 if (d instanceof HTMLDocument) {
1585 return (HTMLDocument) d;
1586 }
1587 throw new IllegalArgumentException("document must be HTMLDocument");
1588 }
1589
1590 /**
1591 * @param e the JEditorPane
1592 * @return HTMLEditorKit for <code>e</code>.
1593 */
1594 protected HTMLEditorKit getHTMLEditorKit(JEditorPane e) {
1595 EditorKit k = e.getEditorKit();
1596 if (k instanceof HTMLEditorKit) {
1597 return (HTMLEditorKit) k;
1598 }
1599 throw new IllegalArgumentException("EditorKit must be HTMLEditorKit");
1600 }
1601
1602 /**
1603 * Returns an array of the Elements that contain <code>offset</code>.
1604 * The first elements corresponds to the root.
1605 *
1606 * @param doc an instance of HTMLDocument
1607 * @param offset value of offset
1608 * @return an array of the Elements that contain <code>offset</code>
1609 */
1610 protected Element[] getElementsAt(HTMLDocument doc, int offset) {
1611 return getElementsAt(doc.getDefaultRootElement(), offset, 0);
1612 }
1613
1614 /**
1615 * Recursive method used by getElementsAt.
1616 */
1617 private Element[] getElementsAt(Element parent, int offset,
1618 int depth) {
1619 if (parent.isLeaf()) {
1620 Element[] retValue = new Element[depth + 1];
1621 retValue[depth] = parent;
1622 return retValue;
1623 }
1624 Element[] retValue = getElementsAt(parent.getElement
1625 (parent.getElementIndex(offset)), offset, depth + 1);
1626 retValue[depth] = parent;
1627 return retValue;
1628 }
1629
1630 /**
1631 * Returns number of elements, starting at the deepest leaf, needed
1632 * to get to an element representing <code>tag</code>. This will
1633 * return -1 if no elements is found representing <code>tag</code>,
1634 * or 0 if the parent of the leaf at <code>offset</code> represents
1635 * <code>tag</code>.
1636 *
1637 * @param doc an instance of HTMLDocument
1638 * @param offset an offset to start from
1639 * @param tag tag to represent
1640 * @return number of elements
1641 */
1642 protected int elementCountToTag(HTMLDocument doc, int offset,
1643 HTML.Tag tag) {
1644 int depth = -1;
1645 Element e = doc.getCharacterElement(offset);
1646 while (e != null && e.getAttributes().getAttribute
1647 (StyleConstants.NameAttribute) != tag) {
1648 e = e.getParentElement();
1649 depth++;
1650 }
1651 if (e == null) {
1652 return -1;
1653 }
1654 return depth;
1655 }
1656
1657 /**
1658 * Returns the deepest element at <code>offset</code> matching
1659 * <code>tag</code>.
1660 *
1661 * @param doc an instance of HTMLDocument
1662 * @param offset the specified offset >= 0
1663 * @param tag an instance of HTML.Tag
1664 *
1665 * @return the deepest element
1666 */
1667 protected Element findElementMatchingTag(HTMLDocument doc, int offset,
1668 HTML.Tag tag) {
1669 Element e = doc.getDefaultRootElement();
1670 Element lastMatch = null;
1671 while (e != null) {
1672 if (e.getAttributes().getAttribute
1673 (StyleConstants.NameAttribute) == tag) {
1674 lastMatch = e;
1675 }
1676 e = e.getElement(e.getElementIndex(offset));
1677 }
1678 return lastMatch;
1679 }
1856 e = e.getElement(e.getElementIndex(offset));
1857 pops++;
1858 }
1859
1860 // And how many pushes
1861 e = commonParent;
1862 offset++;
1863 while (e != null && e != insertElement) {
1864 e = e.getElement(e.getElementIndex(offset));
1865 pushes++;
1866 }
1867 }
1868 pops = Math.max(0, pops - 1);
1869
1870 // And insert!
1871 insertHTML(editor, doc, offset, html, pops, pushes, addTag);
1872 }
1873 }
1874
1875 /**
1876 * If there is an Element with name <code>tag</code> at
1877 * <code>offset</code>, this will invoke either insertAtBoundary
1878 * or <code>insertHTML</code>. This returns true if there is
1879 * a match, and one of the inserts is invoked.
1880 */
1881 /*protected*/
1882 boolean insertIntoTag(JEditorPane editor, HTMLDocument doc,
1883 int offset, HTML.Tag tag, HTML.Tag addTag) {
1884 Element e = findElementMatchingTag(doc, offset, tag);
1885 if (e != null && e.getStartOffset() == offset) {
1886 insertAtBoundary(editor, doc, offset, e, html,
1887 tag, addTag);
1888 return true;
1889 }
1890 else if (offset > 0) {
1891 int depth = elementCountToTag(doc, offset - 1, tag);
1892 if (depth != -1) {
1893 insertHTML(editor, doc, offset, html, depth, 0, addTag);
1894 return true;
1895 }
1896 }
1897 return false;
1898 }
2311 } finally {
2312 unlock(lock);
2313 }
2314 }
2315
2316 private View getView(View parent, Element elem, int start) {
2317 if (parent.getElement() == elem) {
2318 return parent;
2319 }
2320 int index = parent.getViewIndex(start, Position.Bias.Forward);
2321
2322 if (index != -1 && index < parent.getViewCount()) {
2323 return getView(parent.getView(index), elem, start);
2324 }
2325 return null;
2326 }
2327
2328 /*
2329 * If possible acquires a lock on the Document. If a lock has been
2330 * obtained a key will be retured that should be passed to
2331 * <code>unlock</code>.
2332 */
2333 private Object lock(JEditorPane editor) {
2334 Document document = editor.getDocument();
2335
2336 if (document instanceof AbstractDocument) {
2337 ((AbstractDocument)document).readLock();
2338 return document;
2339 }
2340 return null;
2341 }
2342
2343 /*
2344 * Releases a lock previously obtained via <code>lock</code>.
2345 */
2346 private void unlock(Object key) {
2347 if (key != null) {
2348 ((AbstractDocument)key).readUnlock();
2349 }
2350 }
2351
2352 /*
2353 * The operation to perform when this action is triggered.
2354 */
2355 public void actionPerformed(ActionEvent e) {
2356
2357 JTextComponent c = getTextComponent(e);
2358 if (c.isEditable() || !(c instanceof JEditorPane)) {
2359 return;
2360 }
2361 JEditorPane editor = (JEditorPane)c;
2362
2363 Document d = editor.getDocument();
2364 if (d == null || !(d instanceof HTMLDocument)) {
|
51 * The <applet> tag is not supported, but some support is provided
52 * for the <object> tag.
53 * <p>
54 * There are several goals of the HTML EditorKit provided, that have
55 * an effect upon the way that HTML is modeled. These
56 * have influenced its design in a substantial way.
57 * <dl>
58 * <dt>
59 * Support editing
60 * <dd>
61 * It might seem fairly obvious that a plug-in for JEditorPane
62 * should provide editing support, but that fact has several
63 * design considerations. There are a substantial number of HTML
64 * documents that don't properly conform to an HTML specification.
65 * These must be normalized somewhat into a correct form if one
66 * is to edit them. Additionally, users don't like to be presented
67 * with an excessive amount of structure editing, so using traditional
68 * text editing gestures is preferred over using the HTML structure
69 * exactly as defined in the HTML document.
70 * <p>
71 * The modeling of HTML is provided by the class {@code HTMLDocument}.
72 * Its documentation describes the details of how the HTML is modeled.
73 * The editing support leverages heavily off of the text package.
74 *
75 * <dt>
76 * Extendable/Scalable
77 * <dd>
78 * To maximize the usefulness of this kit, a great deal of effort
79 * has gone into making it extendable. These are some of the
80 * features.
81 * <ol>
82 * <li>
83 * The parser is replaceable. The default parser is the Hot Java
84 * parser which is DTD based. A different DTD can be used, or an
85 * entirely different parser can be used. To change the parser,
86 * reimplement the getParser method. The default parser is
87 * dynamically loaded when first asked for, so the class files
88 * will never be loaded if an alternative parser is used. The
89 * default parser is in a separate package called parser below
90 * this package.
91 * <li>
92 * The parser drives the ParserCallback, which is provided by
93 * HTMLDocument. To change the callback, subclass HTMLDocument
94 * and reimplement the createDefaultDocument method to return
95 * document that produces a different reader. The reader controls
96 * how the document is structured. Although the Document provides
97 * HTML support by default, there is nothing preventing support of
98 * non-HTML tags that result in alternative element structures.
99 * <li>
100 * The default view of the models are provided as a hierarchy of
101 * View implementations, so one can easily customize how a particular
102 * element is displayed or add capabilities for new kinds of elements
103 * by providing new View implementations. The default set of views
104 * are provided by the {@code HTMLFactory} class. This can
105 * be easily changed by subclassing or replacing the HTMLFactory
106 * and reimplementing the getViewFactory method to return the alternative
107 * factory.
108 * <li>
109 * The View implementations work primarily off of CSS attributes,
110 * which are kept in the views. This makes it possible to have
111 * multiple views mapped over the same model that appear substantially
112 * different. This can be especially useful for printing. For
113 * most HTML attributes, the HTML attributes are converted to CSS
114 * attributes for display. This helps make the View implementations
115 * more general purpose
116 * </ol>
117 *
118 * <dt>
119 * Asynchronous Loading
120 * <dd>
121 * Larger documents involve a lot of parsing and take some time
122 * to load. By default, this kit produces documents that will be
123 * loaded asynchronously if loaded using {@code JEditorPane.setPage}.
124 * This is controlled by a property on the document. The method
125 * {@link #createDefaultDocument createDefaultDocument} can
126 * be overriden to change this. The batching of work is done
127 * by the {@code HTMLDocument.HTMLReader} class. The actual
128 * work is done by the {@code DefaultStyledDocument} and
129 * {@code AbstractDocument} classes in the text package.
130 *
131 * <dt>
132 * Customization from current LAF
133 * <dd>
134 * HTML provides a well known set of features without exactly
135 * specifying the display characteristics. Swing has a theme
136 * mechanism for its look-and-feel implementations. It is desirable
137 * for the look-and-feel to feed display characteristics into the
138 * HTML views. An user with poor vision for example would want
139 * high contrast and larger than typical fonts.
140 * <p>
141 * The support for this is provided by the {@code StyleSheet}
142 * class. The presentation of the HTML can be heavily influenced
143 * by the setting of the StyleSheet property on the EditorKit.
144 *
145 * <dt>
146 * Not lossy
147 * <dd>
148 * An EditorKit has the ability to be read and save documents.
149 * It is generally the most pleasing to users if there is no loss
150 * of data between the two operation. The policy of the HTMLEditorKit
151 * will be to store things not recognized or not necessarily visible
152 * so they can be subsequently written out. The model of the HTML document
153 * should therefore contain all information discovered while reading the
154 * document. This is constrained in some ways by the need to support
155 * editing (i.e. incorrect documents sometimes must be normalized).
156 * The guiding principle is that information shouldn't be lost, but
157 * some might be synthesized to produce a more correct model or it might
158 * be rearranged.
159 * </dl>
160 *
161 * @author Timothy Prinzing
162 */
163 @SuppressWarnings("serial") // Same-version serialization only
164 public class HTMLEditorKit extends StyledEditorKit implements Accessible {
165
166 private JEditorPane theEditor;
167
168 /**
169 * Constructs an HTMLEditorKit, creates a StyleContext,
170 * and loads the style sheet.
171 */
172 public HTMLEditorKit() {
173
174 }
175
176 /**
177 * Get the MIME type of the data that this
178 * kit represents support for. This kit supports
179 * the type {@code text/html}.
180 *
181 * @return the type
182 */
183 public String getContentType() {
184 return "text/html";
185 }
186
187 /**
188 * Fetch a factory that is suitable for producing
189 * views of any models that are produced by this
190 * kit.
191 *
192 * @return the factory
193 */
194 public ViewFactory getViewFactory() {
195 return defaultFactory;
196 }
197
198 /**
199 * Create an uninitialized text storage model
214 return doc;
215 }
216
217 /**
218 * Try to get an HTML parser from the document. If no parser is set for
219 * the document, return the editor kit's default parser. It is an error
220 * if no parser could be obtained from the editor kit.
221 */
222 private Parser ensureParser(HTMLDocument doc) throws IOException {
223 Parser p = doc.getParser();
224 if (p == null) {
225 p = getParser();
226 }
227 if (p == null) {
228 throw new IOException("Can't load parser");
229 }
230 return p;
231 }
232
233 /**
234 * Inserts content from the given stream. If {@code doc} is
235 * an instance of HTMLDocument, this will read
236 * HTML 3.2 text. Inserting HTML into a non-empty document must be inside
237 * the body Element, if you do not insert into the body an exception will
238 * be thrown. When inserting into a non-empty document all tags outside
239 * of the body (head, title) will be dropped.
240 *
241 * @param in the stream to read from
242 * @param doc the destination for the insertion
243 * @param pos the location in the document to place the
244 * content
245 * @exception IOException on any I/O error
246 * @exception BadLocationException if pos represents an invalid
247 * location within the document
248 * @exception RuntimeException (will eventually be a BadLocationException)
249 * if pos is invalid
250 */
251 public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {
252
253 if (doc instanceof HTMLDocument) {
254 HTMLDocument hdoc = (HTMLDocument) doc;
428 new PrivilegedAction<InputStream>() {
429 public InputStream run() {
430 return HTMLEditorKit.class.getResourceAsStream(name);
431 }
432 });
433 }
434
435 /**
436 * Fetches the command list for the editor. This is
437 * the list of commands supported by the superclass
438 * augmented by the collection of commands defined
439 * locally for style operations.
440 *
441 * @return the command list
442 */
443 public Action[] getActions() {
444 return TextAction.augmentList(super.getActions(), defaultActions);
445 }
446
447 /**
448 * Copies the key/values in {@code element}s AttributeSet into
449 * {@code set}. This does not copy component, icon, or element
450 * names attributes. Subclasses may wish to refine what is and what
451 * isn't copied here. But be sure to first remove all the attributes that
452 * are in {@code set}.<p>
453 * This is called anytime the caret moves over a different location.
454 *
455 */
456 protected void createInputAttributes(Element element,
457 MutableAttributeSet set) {
458 set.removeAttributes(set);
459 set.addAttributes(element.getAttributes());
460 set.removeAttribute(StyleConstants.ComposedTextAttribute);
461
462 Object o = set.getAttribute(StyleConstants.NameAttribute);
463 if (o instanceof HTML.Tag) {
464 HTML.Tag tag = (HTML.Tag)o;
465 // PENDING: we need a better way to express what shouldn't be
466 // copied when editing...
467 if(tag == HTML.Tag.IMG) {
468 // Remove the related image attributes, src, width, height
469 set.removeAttribute(HTML.Attribute.SRC);
470 set.removeAttribute(HTML.Attribute.HEIGHT);
471 set.removeAttribute(HTML.Attribute.WIDTH);
472 set.addAttribute(StyleConstants.NameAttribute,
540 *
541 * @since 1.3
542 */
543 public void setLinkCursor(Cursor cursor) {
544 linkCursor = cursor;
545 }
546
547 /**
548 * Returns the cursor to use over hyper links.
549 *
550 * @return the cursor
551 *
552 * @since 1.3
553 */
554 public Cursor getLinkCursor() {
555 return linkCursor;
556 }
557
558 /**
559 * Indicates whether an html form submission is processed automatically
560 * or only {@code FormSubmitEvent} is fired.
561 *
562 * @return true if html form submission is processed automatically,
563 * false otherwise.
564 *
565 * @see #setAutoFormSubmission
566 * @since 1.5
567 */
568 public boolean isAutoFormSubmission() {
569 return isAutoFormSubmission;
570 }
571
572 /**
573 * Specifies if an html form submission is processed
574 * automatically or only {@code FormSubmitEvent} is fired.
575 * By default it is set to true.
576 *
577 * @param isAuto if {@code true}, html form submission is processed automatically.
578 *
579 * @see #isAutoFormSubmission()
580 * @see FormSubmitEvent
581 * @since 1.5
582 */
583 public void setAutoFormSubmission(boolean isAuto) {
584 isAutoFormSubmission = isAuto;
585 }
586
587 /**
588 * Creates a copy of the editor kit.
589 *
590 * @return the copy
591 */
592 public Object clone() {
593 HTMLEditorKit o = (HTMLEditorKit)super.clone();
594 if (o != null) {
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} contains
816 * the location {@code x}, {@code y}. {@code offset}
817 * gives the offset into the Document to check for.
818 */
819 private boolean doesElementContainLocation(JEditorPane editor,
820 Element e, int offset,
821 int x, int y) {
822 if (e != null && offset > 0 && e.getStartOffset() == offset) {
823 try {
824 TextUI ui = editor.getUI();
825 Shape s1 = ui.modelToView(editor, offset,
826 Position.Bias.Forward);
827 if (s1 == null) {
828 return false;
829 }
830 Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle)s1 :
831 s1.getBounds();
832 Shape s2 = ui.modelToView(editor, e.getEndOffset(),
833 Position.Bias.Backward);
834 if (s2 != null) {
835 Rectangle r2 = (s2 instanceof Rectangle) ? (Rectangle)s2 :
836 s2.getBounds();
842 }
843 return true;
844 }
845
846 /**
847 * Calls linkActivated on the associated JEditorPane
848 * if the given position represents a link.<p>This is implemented
849 * to forward to the method with the same name, but with the following
850 * args both == -1.
851 *
852 * @param pos the position
853 * @param editor the editor pane
854 */
855 protected void activateLink(int pos, JEditorPane editor) {
856 activateLink(pos, editor, null);
857 }
858
859 /**
860 * Calls linkActivated on the associated JEditorPane
861 * if the given position represents a link. If this was the result
862 * of a mouse click, {@code x} and
863 * {@code y} will give the location of the mouse, otherwise
864 * they will be {@literal <} 0.
865 *
866 * @param pos the position
867 * @param html the editor pane
868 */
869 void activateLink(int pos, JEditorPane html, MouseEvent mouseEvent) {
870 Document doc = html.getDocument();
871 if (doc instanceof HTMLDocument) {
872 HTMLDocument hdoc = (HTMLDocument) doc;
873 Element e = hdoc.getCharacterElement(pos);
874 AttributeSet a = e.getAttributes();
875 AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A);
876 HyperlinkEvent linkEvent = null;
877 String description;
878 int x = -1;
879 int y = -1;
880
881 if (mouseEvent != null) {
882 x = mouseEvent.getX();
883 y = mouseEvent.getY();
885
886 if (anchor == null) {
887 href = getMapHREF(html, hdoc, e, a, pos, x, y);
888 }
889 else {
890 href = (String)anchor.getAttribute(HTML.Attribute.HREF);
891 }
892
893 if (href != null) {
894 linkEvent = createHyperlinkEvent(html, hdoc, href, anchor,
895 e, mouseEvent);
896 }
897 if (linkEvent != null) {
898 html.fireHyperlinkUpdate(linkEvent);
899 }
900 }
901 }
902
903 /**
904 * Creates and returns a new instance of HyperlinkEvent. If
905 * {@code hdoc} is a frame document a HTMLFrameHyperlinkEvent
906 * will be created.
907 */
908 HyperlinkEvent createHyperlinkEvent(JEditorPane html,
909 HTMLDocument hdoc, String href,
910 AttributeSet anchor,
911 Element element,
912 MouseEvent mouseEvent) {
913 URL u;
914 try {
915 URL base = hdoc.getBase();
916 u = new URL(base, href);
917 // Following is a workaround for 1.2, in which
918 // new URL("file://...", "#...") causes the filename to
919 // be lost.
920 if (href != null && "file".equals(u.getProtocol()) &&
921 href.startsWith("#")) {
922 String baseFile = base.getFile();
923 String newFile = u.getFile();
924 if (baseFile != null && newFile != null &&
925 !newFile.startsWith(baseFile)) {
988 * implementation provided by this editor kit.
989 */
990 public abstract static class Parser {
991 /**
992 * Parse the given stream and drive the given callback
993 * with the results of the parse. This method should
994 * be implemented to be thread-safe.
995 *
996 * @param r a reader
997 * @param cb a parser callback
998 * @param ignoreCharSet if {@code true} charset is ignoring
999 * @throws IOException if an I/O exception occurs
1000 */
1001 public abstract void parse(Reader r, ParserCallback cb, boolean ignoreCharSet) throws IOException;
1002
1003 }
1004
1005 /**
1006 * The result of parsing drives these callback methods.
1007 * The open and close actions should be balanced. The
1008 * {@code flush} method will be the last method
1009 * called, to give the receiver a chance to flush any
1010 * pending data into the document.
1011 * <p>Refer to DocumentParser, the default parser used, for further
1012 * information on the contents of the AttributeSets, the positions, and
1013 * other info.
1014 *
1015 * @see javax.swing.text.html.parser.DocumentParser
1016 */
1017 public static class ParserCallback {
1018 /**
1019 * This is passed as an attribute in the attributeset to indicate
1020 * the element is implied eg, the string '<>foo<\t>'
1021 * contains an implied html element and an implied body element.
1022 *
1023 * @since 1.3
1024 */
1025 public static final Object IMPLIED = "_implied_";
1026
1027 /**
1028 * The last method called on the reader. It allows
1083 *
1084 * @param t an HTML tag
1085 * @param a a set of attributes
1086 * @param pos a position
1087 */
1088 public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
1089 }
1090
1091 /**
1092 * Callback from the parser. Route to the appropriate
1093 * handler for the error.
1094 *
1095 * @param errorMsg a error message
1096 * @param pos a position
1097 */
1098 public void handleError(String errorMsg, int pos) {
1099 }
1100
1101 /**
1102 * This is invoked after the stream has been parsed, but before
1103 * {@code flush}. {@code eol} will be one of \n, \r
1104 * or \r\n, which ever is encountered the most in parsing the
1105 * stream.
1106 *
1107 * @param eol value of eol
1108 *
1109 * @since 1.3
1110 */
1111 public void handleEndOfLineString(String eol) {
1112 }
1113 }
1114
1115 /**
1116 * A factory to build views for HTML. The following
1117 * table describes what this factory will build by
1118 * default.
1119 *
1120 * <table summary="Describes the tag and view created by this factory by default">
1121 * <tr>
1122 * <th align=left>Tag<th align=left>View created
1123 * </tr><tr>
1560 * An abstract Action providing some convenience methods that may
1561 * be useful in inserting HTML into an existing document.
1562 * <p>NOTE: None of the convenience methods obtain a lock on the
1563 * document. If you have another thread modifying the text these
1564 * methods may have inconsistent behavior, or return the wrong thing.
1565 */
1566 @SuppressWarnings("serial") // Superclass is not serializable across versions
1567 public abstract static class HTMLTextAction extends StyledTextAction {
1568
1569 /**
1570 * Creates a new HTMLTextAction from a string action name.
1571 *
1572 * @param name the name of the action
1573 */
1574 public HTMLTextAction(String name) {
1575 super(name);
1576 }
1577
1578 /**
1579 * @param e the JEditorPane
1580 * @return HTMLDocument of {@code e}.
1581 */
1582 protected HTMLDocument getHTMLDocument(JEditorPane e) {
1583 Document d = e.getDocument();
1584 if (d instanceof HTMLDocument) {
1585 return (HTMLDocument) d;
1586 }
1587 throw new IllegalArgumentException("document must be HTMLDocument");
1588 }
1589
1590 /**
1591 * @param e the JEditorPane
1592 * @return HTMLEditorKit for {@code e}.
1593 */
1594 protected HTMLEditorKit getHTMLEditorKit(JEditorPane e) {
1595 EditorKit k = e.getEditorKit();
1596 if (k instanceof HTMLEditorKit) {
1597 return (HTMLEditorKit) k;
1598 }
1599 throw new IllegalArgumentException("EditorKit must be HTMLEditorKit");
1600 }
1601
1602 /**
1603 * Returns an array of the Elements that contain {@code offset}.
1604 * The first elements corresponds to the root.
1605 *
1606 * @param doc an instance of HTMLDocument
1607 * @param offset value of offset
1608 * @return an array of the Elements that contain {@code offset}
1609 */
1610 protected Element[] getElementsAt(HTMLDocument doc, int offset) {
1611 return getElementsAt(doc.getDefaultRootElement(), offset, 0);
1612 }
1613
1614 /**
1615 * Recursive method used by getElementsAt.
1616 */
1617 private Element[] getElementsAt(Element parent, int offset,
1618 int depth) {
1619 if (parent.isLeaf()) {
1620 Element[] retValue = new Element[depth + 1];
1621 retValue[depth] = parent;
1622 return retValue;
1623 }
1624 Element[] retValue = getElementsAt(parent.getElement
1625 (parent.getElementIndex(offset)), offset, depth + 1);
1626 retValue[depth] = parent;
1627 return retValue;
1628 }
1629
1630 /**
1631 * Returns number of elements, starting at the deepest leaf, needed
1632 * to get to an element representing {@code tag}. This will
1633 * return -1 if no elements is found representing {@code tag},
1634 * or 0 if the parent of the leaf at {@code offset} represents
1635 * {@code tag}.
1636 *
1637 * @param doc an instance of HTMLDocument
1638 * @param offset an offset to start from
1639 * @param tag tag to represent
1640 * @return number of elements
1641 */
1642 protected int elementCountToTag(HTMLDocument doc, int offset,
1643 HTML.Tag tag) {
1644 int depth = -1;
1645 Element e = doc.getCharacterElement(offset);
1646 while (e != null && e.getAttributes().getAttribute
1647 (StyleConstants.NameAttribute) != tag) {
1648 e = e.getParentElement();
1649 depth++;
1650 }
1651 if (e == null) {
1652 return -1;
1653 }
1654 return depth;
1655 }
1656
1657 /**
1658 * Returns the deepest element at {@code offset} matching
1659 * {@code tag}.
1660 *
1661 * @param doc an instance of HTMLDocument
1662 * @param offset the specified offset >= 0
1663 * @param tag an instance of HTML.Tag
1664 *
1665 * @return the deepest element
1666 */
1667 protected Element findElementMatchingTag(HTMLDocument doc, int offset,
1668 HTML.Tag tag) {
1669 Element e = doc.getDefaultRootElement();
1670 Element lastMatch = null;
1671 while (e != null) {
1672 if (e.getAttributes().getAttribute
1673 (StyleConstants.NameAttribute) == tag) {
1674 lastMatch = e;
1675 }
1676 e = e.getElement(e.getElementIndex(offset));
1677 }
1678 return lastMatch;
1679 }
1856 e = e.getElement(e.getElementIndex(offset));
1857 pops++;
1858 }
1859
1860 // And how many pushes
1861 e = commonParent;
1862 offset++;
1863 while (e != null && e != insertElement) {
1864 e = e.getElement(e.getElementIndex(offset));
1865 pushes++;
1866 }
1867 }
1868 pops = Math.max(0, pops - 1);
1869
1870 // And insert!
1871 insertHTML(editor, doc, offset, html, pops, pushes, addTag);
1872 }
1873 }
1874
1875 /**
1876 * If there is an Element with name {@code tag} at
1877 * {@code offset}, this will invoke either insertAtBoundary
1878 * or {@code insertHTML}. This returns true if there is
1879 * a match, and one of the inserts is invoked.
1880 */
1881 /*protected*/
1882 boolean insertIntoTag(JEditorPane editor, HTMLDocument doc,
1883 int offset, HTML.Tag tag, HTML.Tag addTag) {
1884 Element e = findElementMatchingTag(doc, offset, tag);
1885 if (e != null && e.getStartOffset() == offset) {
1886 insertAtBoundary(editor, doc, offset, e, html,
1887 tag, addTag);
1888 return true;
1889 }
1890 else if (offset > 0) {
1891 int depth = elementCountToTag(doc, offset - 1, tag);
1892 if (depth != -1) {
1893 insertHTML(editor, doc, offset, html, depth, 0, addTag);
1894 return true;
1895 }
1896 }
1897 return false;
1898 }
2311 } finally {
2312 unlock(lock);
2313 }
2314 }
2315
2316 private View getView(View parent, Element elem, int start) {
2317 if (parent.getElement() == elem) {
2318 return parent;
2319 }
2320 int index = parent.getViewIndex(start, Position.Bias.Forward);
2321
2322 if (index != -1 && index < parent.getViewCount()) {
2323 return getView(parent.getView(index), elem, start);
2324 }
2325 return null;
2326 }
2327
2328 /*
2329 * If possible acquires a lock on the Document. If a lock has been
2330 * obtained a key will be retured that should be passed to
2331 * {@code unlock}.
2332 */
2333 private Object lock(JEditorPane editor) {
2334 Document document = editor.getDocument();
2335
2336 if (document instanceof AbstractDocument) {
2337 ((AbstractDocument)document).readLock();
2338 return document;
2339 }
2340 return null;
2341 }
2342
2343 /*
2344 * Releases a lock previously obtained via {@code lock}.
2345 */
2346 private void unlock(Object key) {
2347 if (key != null) {
2348 ((AbstractDocument)key).readUnlock();
2349 }
2350 }
2351
2352 /*
2353 * The operation to perform when this action is triggered.
2354 */
2355 public void actionPerformed(ActionEvent e) {
2356
2357 JTextComponent c = getTextComponent(e);
2358 if (c.isEditable() || !(c instanceof JEditorPane)) {
2359 return;
2360 }
2361 JEditorPane editor = (JEditorPane)c;
2362
2363 Document d = editor.getDocument();
2364 if (d == null || !(d instanceof HTMLDocument)) {
|