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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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 
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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} 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      */
 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}.
 732      */
 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 
 741     private boolean isValidCharacter(char character) {
 742         return ((character >= 'a' && character <= 'z') ||
 743                 (character >= 'A' && character <= 'Z'));
 744     }
 745 }