1 19 20 package org.netbeans.modules.editor.completion; 21 22 import java.awt.Color ; 23 import java.awt.Font ; 24 import java.awt.FontMetrics ; 25 import java.awt.Graphics ; 26 import java.awt.Rectangle ; 27 import java.awt.Shape ; 28 import java.awt.font.LineMetrics ; 29 import java.awt.geom.Area ; 30 import java.awt.geom.Rectangle2D ; 31 import java.util.Arrays ; 32 import java.util.HashSet ; 33 import java.util.Set ; 34 import java.util.Stack ; 35 import java.util.StringTokenizer ; 36 import javax.swing.SwingUtilities ; 37 import javax.swing.UIManager ; 38 import org.openide.ErrorManager; 39 import org.openide.util.Utilities; 40 41 45 public final class PatchedHtmlRenderer { 46 47 49 private static Stack <Color > colorStack = new Stack <Color >(); 50 51 56 public static final int STYLE_CLIP = 0; 57 58 64 public static final int STYLE_TRUNCATE = 1; 65 66 73 private static final int STYLE_WORDWRAP = 2; 74 75 77 private static final boolean STRICT_HTML = Boolean.getBoolean("netbeans.lwhtml.strict"); 79 81 private static Set <String > badStrings = null; 82 83 84 private static final Object [] entities = new Object [] { 85 new char[] { 'g', 't' }, new char[] { 'l', 't' }, new char[] { 'q', 'u', 'o', 't' }, new char[] { 'a', 'm', 'p' }, new char[] { 'l', 's', 'q', 'u', 'o' }, new char[] { 'r', 's', 'q', 'u', 'o' }, new char[] { 'l', 'd', 'q', 'u', 'o' }, new char[] { 'r', 'd', 'q', 'u', 'o' }, new char[] { 'n', 'd', 'a', 's', 'h' }, new char[] { 'm', 'd', 'a', 's', 'h' }, new char[] { 'n', 'e' }, new char[] { 'l', 'e' }, new char[] { 'g', 'e' }, new char[] { 'c', 'o', 'p', 'y' }, new char[] { 'r', 'e', 'g' }, new char[] { 't', 'r', 'a', 'd', 'e' }, new char[] { 'n', 'b', 's', 'p' } 101 }; 103 104 private static final char[] entitySubstitutions = new char[] { 105 '>', '<', '"', '&', 8216, 8217, 8220, 8221, 8211, 8212, 8800, 8804, 8805, 169, 174, 8482, ' ' 107 }; 108 private PatchedHtmlRenderer() { 109 } 111 112 122 public static double renderPlainString( 123 String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint 124 ) { 125 if ((style < 0) || (style > 1)) { 127 throw new IllegalArgumentException ("Unknown rendering mode: " + style); } 129 130 return _renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint); 131 } 132 133 private static double _renderPlainString( 134 String s, Graphics g, int x, int y, int w, int h, Font f, Color foreground, int style, boolean paint 135 ) { 136 if (f == null) { 137 f = UIManager.getFont("controlFont"); 139 if (f == null) { 140 int fs = 11; 141 Object cfs = UIManager.get("customFontSize"); 143 if (cfs instanceof Integer ) { 144 fs = ((Integer ) cfs).intValue(); 145 } 146 147 f = new Font ("Dialog", Font.PLAIN, fs); } 149 } 150 151 FontMetrics fm = g.getFontMetrics(f); 152 Rectangle2D r = fm.getStringBounds(s, g); 153 154 if (paint) { 155 g.setColor(foreground); 156 g.setFont(f); 157 158 if ((r.getWidth() <= w) || (style == STYLE_CLIP)) { 159 g.drawString(s, x, y); 160 } else { 161 char[] chars = s.toCharArray(); 162 163 if (chars.length == 0) { 164 return 0; 165 } 166 167 double chWidth = r.getWidth() / chars.length; 168 int estCharsOver = new Double ((r.getWidth() - w) / chWidth).intValue(); 169 170 if (style == STYLE_TRUNCATE) { 171 int length = chars.length - estCharsOver; 172 173 if (length <= 0) { 174 return 0; 175 } 176 177 if (paint) { 178 if (length > 3) { 179 Arrays.fill(chars, length - 3, length, '.'); g.drawChars(chars, 0, length, x, y); 181 } else { 182 Shape shape = g.getClip(); 183 184 if (s != null) { 185 Area area = new Area (shape); 186 area.intersect(new Area (new Rectangle (x, y, w, h))); 187 g.setClip(area); 188 } else { 189 g.setClip(new Rectangle (x, y, w, h)); 190 } 191 192 g.drawString("...", x, y); g.setClip(shape); 194 } 195 } 196 } else { 197 } 199 } 200 } 201 202 return r.getWidth(); 203 } 204 205 231 public static double renderString( 232 String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint 233 ) { 234 switch (style) { 235 case STYLE_CLIP: 236 case STYLE_TRUNCATE: 237 break; 238 239 default: 240 throw new IllegalArgumentException ("Unknown rendering mode: " + style); } 242 243 if (s.startsWith("<html") || s.startsWith("<HTML")) { 246 return _renderHTML(s, 6, g, x, y, w, h, f, defaultColor, style, paint, null, false); 247 } else { 248 return renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint); 249 } 250 } 251 252 292 public static double renderHTML( 293 String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint, boolean disableColorChange 294 ) { 295 if ((style < 0) || (style > 1)) { 297 throw new IllegalArgumentException ("Unknown rendering mode: " + style); } 299 300 return _renderHTML(s, 0, g, x, y, w, h, f, defaultColor, style, paint, null, disableColorChange); 301 } 302 303 304 static double _renderHTML( 305 String s, int pos, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint, 306 Color background, boolean disableColorChange 307 ) { 308 if (f == null) { 310 f = UIManager.getFont("controlFont"); 312 if (f == null) { 313 int fs = 11; 314 Object cfs = UIManager.get("customFontSize"); 316 if (cfs instanceof Integer ) { 317 fs = ((Integer ) cfs).intValue(); 318 } 319 320 f = new Font ("Dialog", Font.PLAIN, fs); } 322 } 323 324 Stack <Color > colorStack = SwingUtilities.isEventDispatchThread() ? PatchedHtmlRenderer.colorStack : new Stack <Color >(); 326 327 g.setColor(defaultColor); 328 g.setFont(f); 329 330 char[] chars = s.toCharArray(); 331 int origX = x; 332 boolean done = false; boolean inTag = false; boolean inClosingTag = false; boolean strikethrough = false; boolean underline = false; boolean bold = false; boolean italic = false; boolean truncated = false; double widthPainted = 0; double heightPainted = 0; boolean lastWasWhitespace = false; double lastHeight = 0; 345 double dotsWidth = 0; 346 347 if (style == STYLE_TRUNCATE) { 349 dotsWidth = g.getFontMetrics().stringWidth("..."); } 351 352 375 colorStack.clear(); 377 378 while (!done) { 380 if (pos == s.length()) { 381 return widthPainted; 382 } 383 384 try { 386 inTag |= (chars[pos] == '<'); 387 } catch (ArrayIndexOutOfBoundsException e) { 388 ArrayIndexOutOfBoundsException aib = new ArrayIndexOutOfBoundsException ( 391 "HTML rendering failed at position " + pos + " in String \"" +s + "\". Please report this at http://www.netbeans.org" 393 ); 395 if (STRICT_HTML) { 396 throw aib; 397 } else { 398 ErrorManager.getDefault().notify(ErrorManager.WARNING, aib); 399 400 return renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint); 401 } 402 } 403 404 inClosingTag = inTag && ((pos + 1) < chars.length) && (chars[pos + 1] == '/'); 406 if (truncated) { 407 g.setColor(defaultColor); 409 g.setFont(f); 410 411 if (paint) { 412 g.drawString("...", x, y); } 414 415 done = true; 416 } else if (inTag) { 417 pos++; 419 420 int tagEnd = pos; 421 422 done = tagEnd >= (chars.length - 1); 424 425 while (!done && (chars[tagEnd] != '>')) { 426 done = tagEnd == (chars.length - 1); 427 tagEnd++; 428 } 429 430 if (done) { 431 throw new IllegalArgumentException ("HTML rendering failed on string \"" + s + "\""); } 433 434 if (inClosingTag) { 435 pos++; 437 438 switch (chars[pos]) { 439 case 'P': case 'p': case 'H': case 'h': 443 break; 445 case 'B': case 'b': 448 if ((chars[pos + 1] == 'r') || (chars[pos + 1] == 'R')) { 449 break; 450 } 451 452 if (!bold) { 453 throwBadHTML("Closing bold tag w/o " + "opening bold tag", pos, chars ); } 457 458 if (italic) { 459 g.setFont(deriveFont(f, Font.ITALIC)); 460 } else { 461 g.setFont(deriveFont(f, Font.PLAIN)); 462 } 463 464 bold = false; 465 466 break; 467 468 case 'E': case 'e': case 'I': case 'i': 473 if (bold) { 474 g.setFont(deriveFont(f, Font.BOLD)); 475 } else { 476 g.setFont(deriveFont(f, Font.PLAIN)); 477 } 478 479 if (!italic) { 480 throwBadHTML("Closing italics tag w/o" +"opening italics tag", pos, chars ); } 484 485 italic = false; 486 487 break; 488 489 case 'S': case 's': 492 switch (chars[pos + 1]) { 493 case 'T': case 't': 495 496 if (italic) { g.setFont(deriveFont(f, Font.ITALIC)); 498 } else { 499 g.setFont(deriveFont(f, Font.PLAIN)); 500 } 501 502 bold = false; 503 504 break; 505 506 case '>': strikethrough = false; 508 509 break; 510 } 511 512 break; 513 514 case 'U': case 'u': 516 underline = false; 518 break; 519 520 case 'F': case 'f': 523 if (colorStack.isEmpty()) { 524 g.setColor(defaultColor); 525 } else { 526 g.setColor(colorStack.pop()); 527 } 528 529 break; 530 531 default: 532 throwBadHTML("Malformed or unsupported HTML", pos, chars 534 ); 535 } 536 } else { 537 switch (chars[pos]) { 539 case 'B': case 'b': 542 switch (chars[pos + 1]) { 543 case 'R': case 'r': 546 if (style == STYLE_WORDWRAP) { 547 x = origX; 548 549 int lineHeight = g.getFontMetrics().getHeight(); 550 y += lineHeight; 551 heightPainted += lineHeight; 552 widthPainted = 0; 553 } 554 555 break; 556 557 case '>': 558 bold = true; 559 560 if (italic) { 561 g.setFont(deriveFont(f, Font.BOLD | Font.ITALIC)); 562 } else { 563 g.setFont(deriveFont(f, Font.BOLD)); 564 } 565 566 break; 567 } 568 569 break; 570 571 case 'e': case 'E': case 'I': case 'i': italic = true; 576 577 if (bold) { 578 g.setFont(deriveFont(f, Font.ITALIC | Font.BOLD)); 579 } else { 580 g.setFont(deriveFont(f, Font.ITALIC)); 581 } 582 583 break; 584 585 case 'S': case 's': 588 switch (chars[pos + 1]) { 589 case '>': 590 strikethrough = true; 591 592 break; 593 594 case 'T': 595 case 't': 596 bold = true; 597 598 if (italic) { 599 g.setFont(deriveFont(f, Font.BOLD | Font.ITALIC)); 600 } else { 601 g.setFont(deriveFont(f, Font.BOLD)); 602 } 603 604 break; 605 } 606 607 break; 608 609 case 'U': case 'u': underline = true; 612 613 break; 614 615 case 'f': case 'F': 618 Color c = findColor(chars, pos, tagEnd); 619 colorStack.push(g.getColor()); 620 621 if (background != null) { 622 } 624 625 if (!disableColorChange) { 626 g.setColor(c); 627 } 628 629 break; 630 631 case 'P': case 'p': 634 if (style == STYLE_WORDWRAP) { 635 x = origX; 636 637 int lineHeight = g.getFontMetrics().getHeight(); 638 y += (lineHeight + (lineHeight / 2)); 639 heightPainted = y + lineHeight; 640 widthPainted = 0; 641 } 642 643 break; 644 645 case 'H': 646 case 'h': 648 if (pos == 1) { 649 break; 650 } 651 652 default: 653 throwBadHTML("Malformed or unsupported HTML", pos, chars); } 655 } 656 657 pos = tagEnd + (done ? 0 : 1); 658 inTag = false; 659 } else { 660 if (lastWasWhitespace) { 662 while ((pos < (s.length() - 1)) && Character.isWhitespace(chars[pos])) { 664 pos++; 665 } 666 667 if (pos == (chars.length - 1)) { 670 return (style != STYLE_WORDWRAP) ? widthPainted : heightPainted; 671 } 672 } 673 674 boolean isAmp = false; 678 679 boolean nextLtIsEntity = false; 683 int nextTag = chars.length - 1; 684 685 if ((chars[pos] == '&')) { 687 boolean inEntity = pos != (chars.length - 1); 688 689 if (inEntity) { 690 int newPos = substEntity(chars, pos + 1); 691 inEntity = newPos != -1; 692 693 if (inEntity) { 694 pos = newPos; 695 isAmp = chars[pos] == '&'; 697 nextLtIsEntity = chars[pos] == '<'; 698 } else { 699 nextLtIsEntity = false; 700 isAmp = true; 701 } 702 } 703 } else { 704 nextLtIsEntity = false; 705 } 706 707 for (int i = pos; i < chars.length; i++) { 708 if (((chars[i] == '<') && (!nextLtIsEntity)) || ((chars[i] == '&') && !isAmp)) { nextTag = i - 1; 710 711 break; 712 } 713 714 isAmp = false; 716 nextLtIsEntity = false; 717 } 718 719 FontMetrics fm = g.getFontMetrics(); 720 721 Rectangle2D r = fm.getStringBounds(chars, pos, nextTag + 1, g); 723 724 lastHeight = r.getHeight(); 727 728 int length = (nextTag + 1) - pos; 730 731 boolean goToNextRow = false; 733 734 boolean brutalWrap = false; 737 738 double chWidth = r.getWidth() / length;; 742 743 if (style == STYLE_TRUNCATE) { 744 double newWidth = widthPainted + r.getWidth(); 745 if (newWidth > (w - dotsWidth)) { 746 if (newWidth > w || _renderHTML(s, 0, g.create(), x, y, Integer.MAX_VALUE, h, f, defaultColor, STYLE_CLIP, false, background, disableColorChange) > w) { 747 double pixelsOff = widthPainted + r.getWidth() - w - dotsWidth; 748 749 double estCharsOver = pixelsOff / chWidth; 750 751 length = new Double ((w - dotsWidth - widthPainted) / chWidth).intValue(); 752 753 if (length < 0) { 754 length = 0; 755 } 756 757 r = fm.getStringBounds(chars, pos, pos + length, g); 758 759 truncated = true; 760 } 761 } 762 } else if (style == STYLE_WORDWRAP) { 763 if ((widthPainted + r.getWidth()) > w && chWidth > 3) { 764 double pixelsOff = (widthPainted + (r.getWidth() + 5)) - w; 765 766 double estCharsOver = pixelsOff / chWidth; 767 768 goToNextRow = true; 769 770 int lastChar = new Double (nextTag - estCharsOver).intValue(); 771 772 brutalWrap = x == 0; 775 776 for (int i = lastChar; i > pos; i--) { 777 lastChar--; 778 779 if (Character.isWhitespace(chars[i])) { 780 length = (lastChar - pos) + 1; 781 brutalWrap = false; 782 783 break; 784 } 785 } 786 787 if ((lastChar <= pos) && (length > estCharsOver) && !brutalWrap) { 788 x = origX; 789 y += r.getHeight(); 790 heightPainted += r.getHeight(); 791 792 boolean boundsChanged = false; 793 794 while (!done && Character.isWhitespace(chars[pos]) && (pos < nextTag)) { 795 pos++; 796 boundsChanged = true; 797 done = pos == (chars.length - 1); 798 } 799 800 if (pos == nextTag) { 801 lastWasWhitespace = true; 802 } 803 804 if (boundsChanged) { 805 r = fm.getStringBounds(chars, pos, nextTag + 1, g); 807 } 808 809 goToNextRow = false; 810 widthPainted = 0; 811 812 if (chars[pos - 1 + length] == '<') { 813 length--; 814 } 815 } else if (brutalWrap) { 816 length = (new Double ((w - widthPainted) / chWidth)).intValue(); 818 819 if ((pos + length) > nextTag) { 820 length = (nextTag - pos); 821 } 822 823 goToNextRow = true; 824 } 825 } 826 } 827 828 if (!done) { 829 if (paint) { 830 g.drawChars(chars, pos, length, x, y); 831 } 832 833 if (strikethrough || underline) { 834 LineMetrics lm = fm.getLineMetrics(chars, pos, length - 1, g); 835 int lineWidth = new Double (x + r.getWidth()).intValue(); 836 837 if (paint) { 838 if (strikethrough) { 839 int stPos = Math.round(lm.getStrikethroughOffset()) + 840 g.getFont().getBaselineFor(chars[pos]) + 1; 841 842 g.drawLine(x, y + stPos, lineWidth, y + stPos); 846 } 847 848 if (underline) { 849 int stPos = Math.round(lm.getUnderlineOffset()) + 850 g.getFont().getBaselineFor(chars[pos]) + 1; 851 852 g.drawLine(x, y + stPos, lineWidth, y + stPos); 856 } 857 } 858 } 859 860 if (goToNextRow) { 861 x = origX; 864 y += r.getHeight(); 865 heightPainted += r.getHeight(); 866 widthPainted = 0; 867 pos += (length); 868 869 while ((pos < chars.length) && (Character.isWhitespace(chars[pos])) && (chars[pos] != '<')) { 871 pos++; 872 } 873 874 lastWasWhitespace = true; 875 done |= (pos >= chars.length); 876 } else { 877 x += r.getWidth(); 878 widthPainted += r.getWidth(); 879 lastWasWhitespace = Character.isWhitespace(chars[nextTag]); 880 pos = nextTag + 1; 881 } 882 883 done |= (nextTag == chars.length); 884 } 885 } 886 } 887 888 if (style != STYLE_WORDWRAP) { 889 return widthPainted; 890 } else { 891 return heightPainted + lastHeight; 892 } 893 } 894 895 896 private static Color findColor(final char[] ch, final int pos, final int tagEnd) { 897 int colorPos = pos; 898 boolean useUIManager = false; 899 900 for (int i = pos; i < tagEnd; i++) { 901 if (ch[i] == 'c') { 902 colorPos = i + 6; 903 904 if ((ch[colorPos] == '\'') || (ch[colorPos] == '"')) { 905 colorPos++; 906 } 907 908 if (ch[colorPos] == '#') { 910 colorPos++; 911 } else if (ch[colorPos] == '!') { 912 useUIManager = true; 913 colorPos++; 914 } 915 916 break; 917 } 918 } 919 920 if (colorPos == pos) { 921 String out = "Could not find color identifier in font declaration"; throwBadHTML(out, pos, ch); 923 } 924 925 String s; 927 928 if (useUIManager) { 929 int end = ch.length - 1; 930 931 for (int i = colorPos; i < ch.length; i++) { 932 if ((ch[i] == '"') || (ch[i] == '\'')) { end = i; 934 935 break; 936 } 937 } 938 939 s = new String (ch, colorPos, end - colorPos); 940 } else { 941 s = new String (ch, colorPos, 6); 942 } 943 944 Color result = null; 945 946 if (useUIManager) { 947 result = UIManager.getColor(s); 948 949 if (result == null) { 951 throwBadHTML("Could not resolve logical font declared in HTML: " + s, pos, ch 953 ); 954 result = UIManager.getColor("textText"); 956 if (result == null) { 958 result = Color.BLACK; 959 } 960 } 961 } else { 962 try { 963 int rgb = Integer.parseInt(s, 16); 964 result = new Color (rgb); 965 } catch (NumberFormatException nfe) { 966 throwBadHTML("Illegal hexadecimal color text: " + s + " in HTML string", colorPos, ch ); } 970 } 971 972 if (result == null) { 973 throwBadHTML("Unresolvable html color: " + s +" in HTML string \n ", pos, ch ); } 977 978 return result; 979 } 980 981 987 private static final Font deriveFont(Font f, int style) { 988 Font result = Utilities.isMac() ? new Font (f.getName(), style, f.getSize()) : f.deriveFont(style); 991 992 return result; 993 } 994 995 1000 private static final int substEntity(char[] ch, int pos) { 1001 if (pos >= (ch.length - 2)) { 1003 return -1; 1004 } 1005 1006 if (ch[pos] == '#') { 1009 return substNumericEntity(ch, pos + 1); 1010 } 1011 1012 boolean match; 1014 1015 for (int i = 0; i < entities.length; i++) { 1016 char[] c = (char[]) entities[i]; 1017 match = true; 1018 1019 if (c.length < (ch.length - pos)) { 1020 for (int j = 0; j < c.length; j++) { 1021 match &= (c[j] == ch[j + pos]); 1022 } 1023 } else { 1024 match = false; 1025 } 1026 1027 if (match) { 1028 if (ch[pos + c.length] == ';') { 1031 ch[pos + c.length] = entitySubstitutions[i]; 1033 1034 return pos + c.length; 1035 } 1036 } 1037 } 1038 1039 return -1; 1040 } 1041 1042 1046 private static final int substNumericEntity(char[] ch, int pos) { 1047 for (int i = pos; i < ch.length; i++) { 1048 if (ch[i] == ';') { 1049 try { 1050 ch[i] = (char) Integer.parseInt(new String (ch, pos, i - pos)); 1051 1052 return i; 1053 } catch (NumberFormatException nfe) { 1054 throwBadHTML("Unparsable numeric entity: " + new String (ch, pos, i - pos), pos, ch 1056 ); } 1058 } 1059 } 1060 1061 return -1; 1062 } 1063 1064 1066 private static void throwBadHTML(String msg, int pos, char[] chars) { 1067 char[] chh = new char[pos]; 1068 Arrays.fill(chh, ' '); chh[pos - 1] = '^'; 1071 String out = msg + "\n " + new String (chars) + "\n " + new String (chh) + "\n Full HTML string:" + new String (chars); 1074 if (!STRICT_HTML) { 1075 if (ErrorManager.getDefault().isLoggable(ErrorManager.WARNING)) { 1076 if (badStrings == null) { 1077 badStrings = new HashSet <String >(); 1078 } 1079 1080 if (!badStrings.contains(msg)) { 1081 StringTokenizer tk = new StringTokenizer (out, "\n", false); 1086 while (tk.hasMoreTokens()) { 1087 ErrorManager.getDefault().log(ErrorManager.WARNING, tk.nextToken()); 1088 } 1089 1090 badStrings.add(msg.intern()); 1091 } 1092 } 1093 } else { 1094 throw new IllegalArgumentException (out); 1095 } 1096 } 1097 1098} 1099 | Popular Tags |