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      * @exception IOException on any I/O error
199      */
writeStartTag(String tag)200     protected void writeStartTag(String tag) throws IOException {
201         indent();
202         write(tag);
203         write(NEWLINE);
204         incrIndent();
205     }
206 
207 
208     /**
209      * Writes out an end tag appropriately
210      * indented.  Also decrements the indent level.
211      *
212      * @exception IOException on any I/O error
213      */
writeEndTag(String endTag)214     protected void writeEndTag(String endTag) throws IOException {
215         decrIndent();
216         indent();
217         write(endTag);
218         write(NEWLINE);
219     }
220 
221 
222     /**
223      * Writes out the &lt;head&gt; and &lt;style&gt;
224      * tags, and then invokes writeStyles() to write
225      * out all the named styles as the content of the
226      * &lt;style&gt; tag.  The content is surrounded by
227      * valid HTML comment markers to ensure that the
228      * document is viewable in applications/browsers
229      * that do not support the tag.
230      *
231      * @exception IOException on any I/O error
232      */
writeHeader()233     protected void writeHeader() throws IOException {
234         writeStartTag("<head>");
235         writeStartTag("<style>");
236         writeStartTag("<!--");
237         writeStyles();
238         writeEndTag("-->");
239         writeEndTag("</style>");
240         writeEndTag("</head>");
241     }
242 
243 
244 
245     /**
246      * Writes out all the named styles as the
247      * content of the &lt;style&gt; tag.
248      *
249      * @exception IOException on any I/O error
250      */
writeStyles()251     protected void writeStyles() throws IOException {
252         /*
253          *  Access to DefaultStyledDocument done to workaround
254          *  a missing API in styled document to access the
255          *  stylenames.
256          */
257         DefaultStyledDocument styledDoc =  ((DefaultStyledDocument)getDocument());
258         Enumeration styleNames = styledDoc.getStyleNames();
259 
260         while (styleNames.hasMoreElements()) {
261             Style s = styledDoc.getStyle((String)styleNames.nextElement());
262 
263             /** PENDING: Once the name attribute is removed
264                 from the list we check check for 0. **/
265             if (s.getAttributeCount() == 1 &&
266                 s.isDefined(StyleConstants.NameAttribute)) {
267                 continue;
268             }
269             indent();
270             write("p." + addStyleName(s.getName()));
271             write(" {\n");
272             incrIndent();
273             writeAttributes(s);
274             decrIndent();
275             indent();
276             write("}\n");
277         }
278     }
279 
280 
281     /**
282      * Iterates over the elements in the document
283      * and processes elements based on whether they are
284      * branch elements or leaf elements.  This method specially handles
285      * leaf elements that are text.
286      *
287      * @exception IOException on any I/O error
288      */
writeBody()289     protected void writeBody() throws IOException, BadLocationException {
290         ElementIterator it = getElementIterator();
291 
292         /*
293           This will be a section element for a styled document.
294           We represent this element in HTML as the body tags.
295           Therefore we ignore it.
296          */
297         it.current();
298 
299         Element next;
300 
301         writeStartTag("<body>");
302 
303         boolean inContent = false;
304 
305         while((next = it.next()) != null) {
306             if (!inRange(next)) {
307                 continue;
308             }
309             if (next instanceof AbstractDocument.BranchElement) {
310                 if (inContent) {
311                     writeEndParagraph();
312                     inContent = false;
313                     fontMask = 0;
314                 }
315                 writeStartParagraph(next);
316             } else if (isText(next)) {
317                 writeContent(next, !inContent);
318                 inContent = true;
319             } else {
320                 writeLeaf(next);
321                 inContent = true;
322             }
323         }
324         if (inContent) {
325             writeEndParagraph();
326         }
327         writeEndTag("</body>");
328     }
329 
330 
331     /**
332      * Emits an end tag for a &lt;p&gt;
333      * tag.  Before writing out the tag, this method ensures
334      * that all other tags that have been opened are
335      * appropriately closed off.
336      *
337      * @exception IOException on any I/O error
338      */
writeEndParagraph()339     protected void writeEndParagraph() throws IOException {
340         writeEndMask(fontMask);
341         if (inFontTag()) {
342             endSpanTag();
343         } else {
344             write(NEWLINE);
345         }
346         writeEndTag("</p>");
347     }
348 
349 
350     /**
351      * Emits the start tag for a paragraph. If
352      * the paragraph has a named style associated with it,
353      * then this method also generates a class attribute for the
354      * &lt;p&gt; tag and sets its value to be the name of the
355      * style.
356      *
357      * @exception IOException on any I/O error
358      */
writeStartParagraph(Element elem)359     protected void writeStartParagraph(Element elem) throws IOException {
360         AttributeSet attr = elem.getAttributes();
361         Object resolveAttr = attr.getAttribute(StyleConstants.ResolveAttribute);
362         if (resolveAttr instanceof StyleContext.NamedStyle) {
363             writeStartTag("<p class=" + mapStyleName(((StyleContext.NamedStyle)resolveAttr).getName()) + ">");
364         } else {
365             writeStartTag("<p>");
366         }
367     }
368 
369 
370     /**
371      * Responsible for writing out other non-text leaf
372      * elements.
373      *
374      * @exception IOException on any I/O error
375      */
writeLeaf(Element elem)376     protected void writeLeaf(Element elem) throws IOException {
377         indent();
378         if (elem.getName() == StyleConstants.IconElementName) {
379             writeImage(elem);
380         } else if (elem.getName() == StyleConstants.ComponentElementName) {
381             writeComponent(elem);
382         }
383     }
384 
385 
386     /**
387      * Responsible for handling Icon Elements;
388      * deliberately unimplemented.  How to implement this method is
389      * an issue of policy.  For example, if you're generating
390      * an &lt;img&gt; tag, how should you
391      * represent the src attribute (the location of the image)?
392      * In certain cases it could be a URL, in others it could
393      * be read from a stream.
394      *
395      * @param elem element of type StyleConstants.IconElementName
396      */
writeImage(Element elem)397     protected void writeImage(Element elem) throws IOException {
398     }
399 
400 
401     /**
402      * Responsible for handling Component Elements;
403      * deliberately unimplemented.
404      * How this method is implemented is a matter of policy.
405      */
writeComponent(Element elem)406     protected void writeComponent(Element elem) throws IOException {
407     }
408 
409 
410     /**
411      * Returns true if the element is a text element.
412      *
413      */
isText(Element elem)414     protected boolean isText(Element elem) {
415         return (elem.getName() == AbstractDocument.ContentElementName);
416     }
417 
418 
419     /**
420      * Writes out the attribute set
421      * in an HTML-compliant manner.
422      *
423      * @exception IOException on any I/O error
424      * @exception BadLocationException if pos represents an invalid
425      *            location within the document.
426      */
writeContent(Element elem, boolean needsIndenting)427     protected void writeContent(Element elem,  boolean needsIndenting)
428         throws IOException, BadLocationException {
429 
430         AttributeSet attr = elem.getAttributes();
431         writeNonHTMLAttributes(attr);
432         if (needsIndenting) {
433             indent();
434         }
435         writeHTMLTags(attr);
436         text(elem);
437     }
438 
439 
440     /**
441      * Generates
442      * bold &lt;b&gt;, italic &lt;i&gt;, and &lt;u&gt; tags for the
443      * text based on its attribute settings.
444      *
445      * @exception IOException on any I/O error
446      */
447 
writeHTMLTags(AttributeSet attr)448     protected void writeHTMLTags(AttributeSet attr) throws IOException {
449 
450         int oldMask = fontMask;
451         setFontMask(attr);
452 
453         int endMask = 0;
454         int startMask = 0;
455         if ((oldMask & BOLD) != 0) {
456             if ((fontMask & BOLD) == 0) {
457                 endMask |= BOLD;
458             }
459         } else if ((fontMask & BOLD) != 0) {
460             startMask |= BOLD;
461         }
462 
463         if ((oldMask & ITALIC) != 0) {
464             if ((fontMask & ITALIC) == 0) {
465                 endMask |= ITALIC;
466             }
467         } else if ((fontMask & ITALIC) != 0) {
468             startMask |= ITALIC;
469         }
470 
471         if ((oldMask & UNDERLINE) != 0) {
472             if ((fontMask & UNDERLINE) == 0) {
473                 endMask |= UNDERLINE;
474             }
475         } else if ((fontMask & UNDERLINE) != 0) {
476             startMask |= UNDERLINE;
477         }
478         writeEndMask(endMask);
479         writeStartMask(startMask);
480     }
481 
482 
483     /**
484      * Tweaks the appropriate bits of fontMask
485      * to reflect whether the text is to be displayed in
486      * bold, italic, and/or with an underline.
487      *
488      */
setFontMask(AttributeSet attr)489     private void setFontMask(AttributeSet attr) {
490         if (StyleConstants.isBold(attr)) {
491             fontMask |= BOLD;
492         }
493 
494         if (StyleConstants.isItalic(attr)) {
495             fontMask |= ITALIC;
496         }
497 
498         if (StyleConstants.isUnderline(attr)) {
499             fontMask |= UNDERLINE;
500         }
501     }
502 
503 
504 
505 
506     /**
507      * Writes out start tags &lt;u&gt;, &lt;i&gt;, and &lt;b&gt; based on
508      * the mask settings.
509      *
510      * @exception IOException on any I/O error
511      */
writeStartMask(int mask)512     private void writeStartMask(int mask) throws IOException  {
513         if (mask != 0) {
514             if ((mask & UNDERLINE) != 0) {
515                 write("<u>");
516             }
517             if ((mask & ITALIC) != 0) {
518                 write("<i>");
519             }
520             if ((mask & BOLD) != 0) {
521                 write("<b>");
522             }
523         }
524     }
525 
526     /**
527      * Writes out end tags for &lt;u&gt;, &lt;i&gt;, and &lt;b&gt; based on
528      * the mask settings.
529      *
530      * @exception IOException on any I/O error
531      */
writeEndMask(int mask)532     private void writeEndMask(int mask) throws IOException {
533         if (mask != 0) {
534             if ((mask & BOLD) != 0) {
535                 write("</b>");
536             }
537             if ((mask & ITALIC) != 0) {
538                 write("</i>");
539             }
540             if ((mask & UNDERLINE) != 0) {
541                 write("</u>");
542             }
543         }
544     }
545 
546 
547     /**
548      * Writes out the remaining
549      * character-level attributes (attributes other than bold,
550      * italic, and underline) in an HTML-compliant way.  Given that
551      * attributes such as font family and font size have no direct
552      * mapping to HTML tags, a &lt;span&gt; tag is generated and its
553      * style attribute is set to contain the list of remaining
554      * attributes just like inline styles.
555      *
556      * @exception IOException on any I/O error
557      */
writeNonHTMLAttributes(AttributeSet attr)558     protected void writeNonHTMLAttributes(AttributeSet attr) throws IOException {
559 
560         String style = "";
561         String separator = "; ";
562 
563         if (inFontTag() && fontAttributes.isEqual(attr)) {
564             return;
565         }
566 
567         boolean first = true;
568         Color color = (Color)attr.getAttribute(StyleConstants.Foreground);
569         if (color != null) {
570             style += "color: " + css.styleConstantsValueToCSSValue
571                                     ((StyleConstants)StyleConstants.Foreground,
572                                      color);
573             first = false;
574         }
575         Integer size = (Integer)attr.getAttribute(StyleConstants.FontSize);
576         if (size != null) {
577             if (!first) {
578                 style += separator;
579             }
580             style += "font-size: " + size.intValue() + "pt";
581             first = false;
582         }
583 
584         String family = (String)attr.getAttribute(StyleConstants.FontFamily);
585         if (family != null) {
586             if (!first) {
587                 style += separator;
588             }
589             style += "font-family: " + family;
590             first = false;
591         }
592 
593         if (style.length() > 0) {
594             if (fontMask != 0) {
595                 writeEndMask(fontMask);
596                 fontMask = 0;
597             }
598             startSpanTag(style);
599             fontAttributes = attr;
600         }
601         else if (fontAttributes != null) {
602             writeEndMask(fontMask);
603             fontMask = 0;
604             endSpanTag();
605         }
606     }
607 
608 
609     /**
610      * Returns true if we are currently in a &lt;font&gt; tag.
611      */
inFontTag()612     protected boolean inFontTag() {
613         return (fontAttributes != null);
614     }
615 
616     /**
617      * This is no longer used, instead &lt;span&gt; will be written out.
618      * <p>
619      * Writes out an end tag for the &lt;font&gt; tag.
620      *
621      * @exception IOException on any I/O error
622      */
endFontTag()623     protected void endFontTag() throws IOException {
624         write(NEWLINE);
625         writeEndTag("</font>");
626         fontAttributes = null;
627     }
628 
629 
630     /**
631      * This is no longer used, instead &lt;span&gt; will be written out.
632      * <p>
633      * Writes out a start tag for the &lt;font&gt; tag.
634      * Because font tags cannot be nested,
635      * this method closes out
636      * any enclosing font tag before writing out a
637      * new start tag.
638      *
639      * @exception IOException on any I/O error
640      */
startFontTag(String style)641     protected void startFontTag(String style) throws IOException {
642         boolean callIndent = false;
643         if (inFontTag()) {
644             endFontTag();
645             callIndent = true;
646         }
647         writeStartTag("<font style=\"" + style + "\">");
648         if (callIndent) {
649             indent();
650         }
651     }
652 
653     /**
654      * Writes out a start tag for the &lt;font&gt; tag.
655      * Because font tags cannot be nested,
656      * this method closes out
657      * any enclosing font tag before writing out a
658      * new start tag.
659      *
660      * @exception IOException on any I/O error
661      */
startSpanTag(String style)662     private void startSpanTag(String style) throws IOException {
663         boolean callIndent = false;
664         if (inFontTag()) {
665             endSpanTag();
666             callIndent = true;
667         }
668         writeStartTag("<span style=\"" + style + "\">");
669         if (callIndent) {
670             indent();
671         }
672     }
673 
674     /**
675      * Writes out an end tag for the &lt;span&gt; tag.
676      *
677      * @exception IOException on any I/O error
678      */
endSpanTag()679     private void endSpanTag() throws IOException {
680         write(NEWLINE);
681         writeEndTag("</span>");
682         fontAttributes = null;
683     }
684 
685     /**
686      * Adds the style named <code>style</code> to the style mapping. This
687      * returns the name that should be used when outputting. CSS does not
688      * allow the full Unicode set to be used as a style name.
689      */
addStyleName(String style)690     private String addStyleName(String style) {
691         if (styleNameMapping == null) {
692             return style;
693         }
694         StringBuilder sb = null;
695         for (int counter = style.length() - 1; counter >= 0; counter--) {
696             if (!isValidCharacter(style.charAt(counter))) {
697                 if (sb == null) {
698                     sb = new StringBuilder(style);
699                 }
700                 sb.setCharAt(counter, 'a');
701             }
702         }
703         String mappedName = (sb != null) ? sb.toString() : style;
704         while (styleNameMapping.get(mappedName) != null) {
705             mappedName = mappedName + 'x';
706         }
707         styleNameMapping.put(style, mappedName);
708         return mappedName;
709     }
710 
711     /**
712      * Returns the mapped style name corresponding to <code>style</code>.
713      */
mapStyleName(String style)714     private String mapStyleName(String style) {
715         if (styleNameMapping == null) {
716             return style;
717         }
718         String retValue = styleNameMapping.get(style);
719         return (retValue == null) ? style : retValue;
720     }
721 
isValidCharacter(char character)722     private boolean isValidCharacter(char character) {
723         return ((character >= 'a' && character <= 'z') ||
724                 (character >= 'A' && character <= 'Z'));
725     }
726 }
727