1 /*
2  * Copyright (c) 1998, 2010, 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 
26 package javax.swing.text.html;
27 
28 import java.io.Writer;
29 import java.io.IOException;
30 import java.util.*;
31 import java.awt.Color;
32 import javax.swing.text.*;
33 
34 /**
35  * MinimalHTMLWriter is a fallback writer used by the
36  * HTMLEditorKit to write out HTML for a document that
37  * is a not produced by the EditorKit.
38  *
39  * The format for the document is:
40  * <pre>
41  * &lt;html&gt;
42  *   &lt;head&gt;
43  *     &lt;style&gt;
44  *        &lt;!-- list of named styles
45  *         p.normal {
46  *            font-family: SansSerif;
47  *            margin-height: 0;
48  *            font-size: 14
49  *         }
50  *        --&gt;
51  *      &lt;/style&gt;
52  *   &lt;/head&gt;
53  *   &lt;body&gt;
54  *    &lt;p style=normal&gt;
55  *        <b>Bold, italic, and underline attributes
56  *        of the run are emitted as HTML tags.
57  *        The remaining attributes are emitted as
58  *        part of the style attribute of a &lt;span&gt; tag.
59  *        The syntax is similar to inline styles.</b>
60  *    &lt;/p&gt;
61  *   &lt;/body&gt;
62  * &lt;/html&gt;
63  * </pre>
64  *
65  * @author Sunita Mani
66  */
67 
68 public class MinimalHTMLWriter extends AbstractWriter {
69 
70     /**
71      * These static finals are used to
72      * tweak and query the fontMask about which
73      * of these tags need to be generated or
74      * terminated.
75      */
76     private static final int BOLD = 0x01;
77     private static final int ITALIC = 0x02;
78     private static final int UNDERLINE = 0x04;
79 
80     // Used to map StyleConstants to CSS.
81     private static final CSS css = new CSS();
82 
83     private int fontMask = 0;
84 
85     int startOffset = 0;
86     int endOffset = 0;
87 
88     /**
89      * Stores the attributes of the previous run.
90      * Used to compare with the current run's
91      * attributeset.  If identical, then a
92      * &lt;span&gt; tag is not emitted.
93      */
94     private AttributeSet fontAttributes;
95 
96     /**
97      * Maps from style name as held by the Document, to the archived
98      * style name (style name written out). These may differ.
99      */
100     private Hashtable<String, String> styleNameMapping;
101 
102     /**
103      * Creates a new MinimalHTMLWriter.
104      *
105      * @param w  Writer
106      * @param doc StyledDocument
107      *
108      */
MinimalHTMLWriter(Writer w, StyledDocument doc)109     public MinimalHTMLWriter(Writer w, StyledDocument doc) {
110         super(w, doc);
111     }
112 
113     /**
114      * Creates a new MinimalHTMLWriter.
115      *
116      * @param w  Writer
117      * @param doc StyledDocument
118      * @param pos The location in the document to fetch the
119      *   content.
120      * @param len The amount to write out.
121      *
122      */
MinimalHTMLWriter(Writer w, StyledDocument doc, int pos, int len)123     public MinimalHTMLWriter(Writer w, StyledDocument doc, int pos, int len) {
124         super(w, doc, pos, len);
125     }
126 
127     /**
128      * Generates HTML output
129      * from a StyledDocument.
130      *
131      * @exception IOException on any I/O error
132      * @exception BadLocationException if pos represents an invalid
133      *            location within the document.
134      *
135      */
write()136     public void write() throws IOException, BadLocationException {
137         styleNameMapping = new Hashtable<String, String>();
138         writeStartTag("<html>");
139         writeHeader();
140         writeBody();
141         writeEndTag("</html>");
142     }
143 
144 
145     /**
146      * Writes out all the attributes for the
147      * following types:
148      *  StyleConstants.ParagraphConstants,
149      *  StyleConstants.CharacterConstants,
150      *  StyleConstants.FontConstants,
151      *  StyleConstants.ColorConstants.
152      * The attribute name and value are separated by a colon.
153      * Each pair is separated by a semicolon.
154      *
155      * @exception IOException on any I/O error
156      */
writeAttributes(AttributeSet attr)157     protected void writeAttributes(AttributeSet attr) throws IOException {
158         Enumeration<?> attributeNames = attr.getAttributeNames();
159         while (attributeNames.hasMoreElements()) {
160             Object name = attributeNames.nextElement();
161             if ((name instanceof StyleConstants.ParagraphConstants) ||
162                 (name instanceof StyleConstants.CharacterConstants) ||
163                 (name instanceof StyleConstants.FontConstants) ||
164                 (name instanceof StyleConstants.ColorConstants)) {
165                 indent();
166                 write(name.toString());
167                 write(':');
168                 write(css.styleConstantsValueToCSSValue
169                       ((StyleConstants)name, attr.getAttribute(name)).
170                       toString());
171                 write(';');
172                 write(NEWLINE);
173             }
174         }
175     }
176 
177 
178     /**
179      * Writes out text.
180      *
181      * @exception IOException on any I/O error
182      */
text(Element elem)183     protected void text(Element elem) throws IOException, BadLocationException {
184         String contentStr = getText(elem);
185         if ((contentStr.length() > 0) &&
186             (contentStr.charAt(contentStr.length()-1) == NEWLINE)) {
187             contentStr = contentStr.substring(0, contentStr.length()-1);
188         }
189         if (contentStr.length() > 0) {
190             write(contentStr);
191         }
192     }
193 
194     /**
195      * Writes out a start tag appropriately
196      * indented.  Also increments the indent level.
197      *
198      * @param tag a start tag
199      * @exception IOException on any I/O error
200      */
writeStartTag(String tag)201     protected void writeStartTag(String tag) throws IOException {
202         indent();
203         write(tag);
204         write(NEWLINE);
205         incrIndent();
206     }
207 
208 
209     /**
210      * Writes out an end tag appropriately
211      * indented.  Also decrements the indent level.
212      *
213      * @param endTag an end tag
214      * @exception IOException on any I/O error
215      */
writeEndTag(String endTag)216     protected void writeEndTag(String endTag) throws IOException {
217         decrIndent();
218         indent();
219         write(endTag);
220         write(NEWLINE);
221     }
222 
223 
224     /**
225      * Writes out the &lt;head&gt; and &lt;style&gt;
226      * tags, and then invokes writeStyles() to write
227      * out all the named styles as the content of the
228      * &lt;style&gt; tag.  The content is surrounded by
229      * valid HTML comment markers to ensure that the
230      * document is viewable in applications/browsers
231      * that do not support the tag.
232      *
233      * @exception IOException on any I/O error
234      */
writeHeader()235     protected void writeHeader() throws IOException {
236         writeStartTag("<head>");
237         writeStartTag("<style>");
238         writeStartTag("<!--");
239         writeStyles();
240         writeEndTag("-->");
241         writeEndTag("</style>");
242         writeEndTag("</head>");
243     }
244 
245 
246 
247     /**
248      * Writes out all the named styles as the
249      * content of the &lt;style&gt; tag.
250      *
251      * @exception IOException on any I/O error
252      */
writeStyles()253     protected void writeStyles() throws IOException {
254         /*
255          *  Access to DefaultStyledDocument done to workaround
256          *  a missing API in styled document to access the
257          *  stylenames.
258          */
259         DefaultStyledDocument styledDoc =  ((DefaultStyledDocument)getDocument());
260         Enumeration<?> styleNames = styledDoc.getStyleNames();
261 
262         while (styleNames.hasMoreElements()) {
263             Style s = styledDoc.getStyle((String)styleNames.nextElement());
264 
265             /** PENDING: Once the name attribute is removed
266                 from the list we check check for 0. **/
267             if (s.getAttributeCount() == 1 &&
268                 s.isDefined(StyleConstants.NameAttribute)) {
269                 continue;
270             }
271             indent();
272             write("p." + addStyleName(s.getName()));
273             write(" {\n");
274             incrIndent();
275             writeAttributes(s);
276             decrIndent();
277             indent();
278             write("}\n");
279         }
280     }
281 
282 
283     /**
284      * Iterates over the elements in the document
285      * and processes elements based on whether they are
286      * branch elements or leaf elements.  This method specially handles
287      * leaf elements that are text.
288      *
289      * @throws IOException on any I/O error
290      * @throws BadLocationException if we are in an invalid
291      *            location within the document.
292      */
writeBody()293     protected void writeBody() throws IOException, BadLocationException {
294         ElementIterator it = getElementIterator();
295 
296         /*
297           This will be a section element for a styled document.
298           We represent this element in HTML as the body tags.
299           Therefore we ignore it.
300          */
301         it.current();
302 
303         Element next;
304 
305         writeStartTag("<body>");
306 
307         boolean inContent = false;
308 
309         while((next = it.next()) != null) {
310             if (!inRange(next)) {
311                 continue;
312             }
313             if (next instanceof AbstractDocument.BranchElement) {
314                 if (inContent) {
315                     writeEndParagraph();
316                     inContent = false;
317                     fontMask = 0;
318                 }
319                 writeStartParagraph(next);
320             } else if (isText(next)) {
321                 writeContent(next, !inContent);
322                 inContent = true;
323             } else {
324                 writeLeaf(next);
325                 inContent = true;
326             }
327         }
328         if (inContent) {
329             writeEndParagraph();
330         }
331         writeEndTag("</body>");
332     }
333 
334 
335     /**
336      * Emits an end tag for a &lt;p&gt;
337      * tag.  Before writing out the tag, this method ensures
338      * that all other tags that have been opened are
339      * appropriately closed off.
340      *
341      * @exception IOException on any I/O error
342      */
writeEndParagraph()343     protected void writeEndParagraph() throws IOException {
344         writeEndMask(fontMask);
345         if (inFontTag()) {
346             endSpanTag();
347         } else {
348             write(NEWLINE);
349         }
350         writeEndTag("</p>");
351     }
352 
353 
354     /**
355      * Emits the start tag for a paragraph. If
356      * the paragraph has a named style associated with it,
357      * then this method also generates a class attribute for the
358      * &lt;p&gt; tag and sets its value to be the name of the
359      * style.
360      *
361      * @param elem an element
362      * @exception IOException on any I/O error
363      */
writeStartParagraph(Element elem)364     protected void writeStartParagraph(Element elem) throws IOException {
365         AttributeSet attr = elem.getAttributes();
366         Object resolveAttr = attr.getAttribute(StyleConstants.ResolveAttribute);
367         if (resolveAttr instanceof StyleContext.NamedStyle) {
368             writeStartTag("<p class=" + mapStyleName(((StyleContext.NamedStyle)resolveAttr).getName()) + ">");
369         } else {
370             writeStartTag("<p>");
371         }
372     }
373 
374 
375     /**
376      * Responsible for writing out other non-text leaf
377      * elements.
378      *
379      * @param elem an element
380      * @exception IOException on any I/O error
381      */
writeLeaf(Element elem)382     protected void writeLeaf(Element elem) throws IOException {
383         indent();
384         if (elem.getName() == StyleConstants.IconElementName) {
385             writeImage(elem);
386         } else if (elem.getName() == StyleConstants.ComponentElementName) {
387             writeComponent(elem);
388         }
389     }
390 
391 
392     /**
393      * Responsible for handling Icon Elements;
394      * deliberately unimplemented.  How to implement this method is
395      * an issue of policy.  For example, if you're generating
396      * an &lt;img&gt; tag, how should you
397      * represent the src attribute (the location of the image)?
398      * In certain cases it could be a URL, in others it could
399      * be read from a stream.
400      *
401      * @param elem an element of type StyleConstants.IconElementName
402      * @throws IOException if I/O error occured.
403      */
writeImage(Element elem)404     protected void writeImage(Element elem) throws IOException {
405     }
406 
407 
408     /**
409      * Responsible for handling Component Elements;
410      * deliberately unimplemented.
411      * How this method is implemented is a matter of policy.
412      *
413      * @param elem an element of type StyleConstants.ComponentElementName
414      * @throws IOException if I/O error occured.
415      */
writeComponent(Element elem)416     protected void writeComponent(Element elem) throws IOException {
417     }
418 
419 
420     /**
421      * Returns true if the element is a text element.
422      *
423      * @param elem an element
424      * @return {@code true} if the element is a text element.
425      */
isText(Element elem)426     protected boolean isText(Element elem) {
427         return (elem.getName() == AbstractDocument.ContentElementName);
428     }
429 
430 
431     /**
432      * Writes out the attribute set
433      * in an HTML-compliant manner.
434      *
435      * @param elem an element
436      * @param needsIndenting indention will be added if {@code needsIndenting} is {@code true}
437      * @exception IOException on any I/O error
438      * @exception BadLocationException if pos represents an invalid
439      *            location within the document.
440      */
writeContent(Element elem, boolean needsIndenting)441     protected void writeContent(Element elem,  boolean needsIndenting)
442         throws IOException, BadLocationException {
443 
444         AttributeSet attr = elem.getAttributes();
445         writeNonHTMLAttributes(attr);
446         if (needsIndenting) {
447             indent();
448         }
449         writeHTMLTags(attr);
450         text(elem);
451     }
452 
453 
454     /**
455      * Generates
456      * bold &lt;b&gt;, italic &lt;i&gt;, and &lt;u&gt; tags for the
457      * text based on its attribute settings.
458      *
459      * @param attr a set of attributes
460      * @exception IOException on any I/O error
461      */
462 
writeHTMLTags(AttributeSet attr)463     protected void writeHTMLTags(AttributeSet attr) throws IOException {
464 
465         int oldMask = fontMask;
466         setFontMask(attr);
467 
468         int endMask = 0;
469         int startMask = 0;
470         if ((oldMask & BOLD) != 0) {
471             if ((fontMask & BOLD) == 0) {
472                 endMask |= BOLD;
473             }
474         } else if ((fontMask & BOLD) != 0) {
475             startMask |= BOLD;
476         }
477 
478         if ((oldMask & ITALIC) != 0) {
479             if ((fontMask & ITALIC) == 0) {
480                 endMask |= ITALIC;
481             }
482         } else if ((fontMask & ITALIC) != 0) {
483             startMask |= ITALIC;
484         }
485 
486         if ((oldMask & UNDERLINE) != 0) {
487             if ((fontMask & UNDERLINE) == 0) {
488                 endMask |= UNDERLINE;
489             }
490         } else if ((fontMask & UNDERLINE) != 0) {
491             startMask |= UNDERLINE;
492         }
493         writeEndMask(endMask);
494         writeStartMask(startMask);
495     }
496 
497 
498     /**
499      * Tweaks the appropriate bits of fontMask
500      * to reflect whether the text is to be displayed in
501      * bold, italic, and/or with an underline.
502      *
503      */
setFontMask(AttributeSet attr)504     private void setFontMask(AttributeSet attr) {
505         if (StyleConstants.isBold(attr)) {
506             fontMask |= BOLD;
507         }
508 
509         if (StyleConstants.isItalic(attr)) {
510             fontMask |= ITALIC;
511         }
512 
513         if (StyleConstants.isUnderline(attr)) {
514             fontMask |= UNDERLINE;
515         }
516     }
517 
518 
519 
520 
521     /**
522      * Writes out start tags &lt;u&gt;, &lt;i&gt;, and &lt;b&gt; based on
523      * the mask settings.
524      *
525      * @exception IOException on any I/O error
526      */
writeStartMask(int mask)527     private void writeStartMask(int mask) throws IOException  {
528         if (mask != 0) {
529             if ((mask & UNDERLINE) != 0) {
530                 write("<u>");
531             }
532             if ((mask & ITALIC) != 0) {
533                 write("<i>");
534             }
535             if ((mask & BOLD) != 0) {
536                 write("<b>");
537             }
538         }
539     }
540 
541     /**
542      * Writes out end tags for &lt;u&gt;, &lt;i&gt;, and &lt;b&gt; based on
543      * the mask settings.
544      *
545      * @exception IOException on any I/O error
546      */
writeEndMask(int mask)547     private void writeEndMask(int mask) throws IOException {
548         if (mask != 0) {
549             if ((mask & BOLD) != 0) {
550                 write("</b>");
551             }
552             if ((mask & ITALIC) != 0) {
553                 write("</i>");
554             }
555             if ((mask & UNDERLINE) != 0) {
556                 write("</u>");
557             }
558         }
559     }
560 
561 
562     /**
563      * Writes out the remaining
564      * character-level attributes (attributes other than bold,
565      * italic, and underline) in an HTML-compliant way.  Given that
566      * attributes such as font family and font size have no direct
567      * mapping to HTML tags, a &lt;span&gt; tag is generated and its
568      * style attribute is set to contain the list of remaining
569      * attributes just like inline styles.
570      *
571      * @param attr a set of attributes
572      * @exception IOException on any I/O error
573      */
writeNonHTMLAttributes(AttributeSet attr)574     protected void writeNonHTMLAttributes(AttributeSet attr) throws IOException {
575 
576         String style = "";
577         String separator = "; ";
578 
579         if (inFontTag() && fontAttributes.isEqual(attr)) {
580             return;
581         }
582 
583         boolean first = true;
584         Color color = (Color)attr.getAttribute(StyleConstants.Foreground);
585         if (color != null) {
586             style += "color: " + css.styleConstantsValueToCSSValue
587                                     ((StyleConstants)StyleConstants.Foreground,
588                                      color);
589             first = false;
590         }
591         Integer size = (Integer)attr.getAttribute(StyleConstants.FontSize);
592         if (size != null) {
593             if (!first) {
594                 style += separator;
595             }
596             style += "font-size: " + size.intValue() + "pt";
597             first = false;
598         }
599 
600         String family = (String)attr.getAttribute(StyleConstants.FontFamily);
601         if (family != null) {
602             if (!first) {
603                 style += separator;
604             }
605             style += "font-family: " + family;
606             first = false;
607         }
608 
609         if (style.length() > 0) {
610             if (fontMask != 0) {
611                 writeEndMask(fontMask);
612                 fontMask = 0;
613             }
614             startSpanTag(style);
615             fontAttributes = attr;
616         }
617         else if (fontAttributes != null) {
618             writeEndMask(fontMask);
619             fontMask = 0;
620             endSpanTag();
621         }
622     }
623 
624 
625     /**
626      * Returns true if we are currently in a &lt;font&gt; tag.
627      *
628      * @return {@code true} if we are currently in a &lt;font&gt; tag.
629      */
inFontTag()630     protected boolean inFontTag() {
631         return (fontAttributes != null);
632     }
633 
634     /**
635      * This is no longer used, instead &lt;span&gt; will be written out.
636      * <p>
637      * Writes out an end tag for the &lt;font&gt; tag.
638      *
639      * @exception IOException on any I/O error
640      */
endFontTag()641     protected void endFontTag() throws IOException {
642         write(NEWLINE);
643         writeEndTag("</font>");
644         fontAttributes = null;
645     }
646 
647 
648     /**
649      * This is no longer used, instead &lt;span&gt; will be written out.
650      * <p>
651      * Writes out a start tag for the &lt;font&gt; tag.
652      * Because font tags cannot be nested,
653      * this method closes out
654      * any enclosing font tag before writing out a
655      * new start tag.
656      *
657      * @param style a font style
658      * @exception IOException on any I/O error
659      */
startFontTag(String style)660     protected void startFontTag(String style) throws IOException {
661         boolean callIndent = false;
662         if (inFontTag()) {
663             endFontTag();
664             callIndent = true;
665         }
666         writeStartTag("<font style=\"" + style + "\">");
667         if (callIndent) {
668             indent();
669         }
670     }
671 
672     /**
673      * Writes out a start tag for the &lt;font&gt; tag.
674      * Because font tags cannot be nested,
675      * this method closes out
676      * any enclosing font tag before writing out a
677      * new start tag.
678      *
679      * @exception IOException on any I/O error
680      */
startSpanTag(String style)681     private void startSpanTag(String style) throws IOException {
682         boolean callIndent = false;
683         if (inFontTag()) {
684             endSpanTag();
685             callIndent = true;
686         }
687         writeStartTag("<span style=\"" + style + "\">");
688         if (callIndent) {
689             indent();
690         }
691     }
692 
693     /**
694      * Writes out an end tag for the &lt;span&gt; tag.
695      *
696      * @exception IOException on any I/O error
697      */
endSpanTag()698     private void endSpanTag() throws IOException {
699         write(NEWLINE);
700         writeEndTag("</span>");
701         fontAttributes = null;
702     }
703 
704     /**
705      * Adds the style named <code>style</code> to the style mapping. This
706      * returns the name that should be used when outputting. CSS does not
707      * allow the full Unicode set to be used as a style name.
708      */
addStyleName(String style)709     private String addStyleName(String style) {
710         if (styleNameMapping == null) {
711             return style;
712         }
713         StringBuilder sb = null;
714         for (int counter = style.length() - 1; counter >= 0; counter--) {
715             if (!isValidCharacter(style.charAt(counter))) {
716                 if (sb == null) {
717                     sb = new StringBuilder(style);
718                 }
719                 sb.setCharAt(counter, 'a');
720             }
721         }
722         String mappedName = (sb != null) ? sb.toString() : style;
723         while (styleNameMapping.get(mappedName) != null) {
724             mappedName = mappedName + 'x';
725         }
726         styleNameMapping.put(style, mappedName);
727         return mappedName;
728     }
729 
730     /**
731      * Returns the mapped style name corresponding to <code>style</code>.
732      */
mapStyleName(String style)733     private String mapStyleName(String style) {
734         if (styleNameMapping == null) {
735             return style;
736         }
737         String retValue = styleNameMapping.get(style);
738         return (retValue == null) ? style : retValue;
739     }
740 
isValidCharacter(char character)741     private boolean isValidCharacter(char character) {
742         return ((character >= 'a' && character <= 'z') ||
743                 (character >= 'A' && character <= 'Z'));
744     }
745 }
746