package org.chartsy.main.utils;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.font.LineMetrics;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.openide.util.Utilities;
/**
*
* @author Viorel
*/
public final class HtmlRenderer
{
private static HtmlRendererImpl LABEL = null;
private static Stack<Color> colorStack = new Stack<Color>();
public static final int STYLE_CLIP = 0;
public static final int STYLE_TRUNCATE = 1;
private static final int STYLE_WORDWRAP = 2;
private static final boolean STRICT_HTML = Boolean.getBoolean("netbeans.lwhtml.strict"); //NOI18N
private static Set<String> badStrings = null;
private static final Logger LOG = Logger.getLogger(HtmlRenderer.class.getName());
private static final Object[] entities = new Object[]
{
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'}
};
private static final char[] entitySubstitutions = new char[]
{'>', '<', '"', '&', 8216, 8217, 8220, 8221, 8211, 8212, 8800, 8804, 8805, 169, 174, 8482, ' '};
private HtmlRenderer()
{}
public static final Renderer createRenderer()
{
return new HtmlRendererImpl();
}
public static final JLabel createLabel()
{
return new HtmlRendererImpl();
}
public static double renderPlainString
(String s, Graphics g, int x, int y, int w, int h, Font f,
Color defaultColor, int style, boolean paint)
{
if ((style < 0) || (style > 1))
throw new IllegalArgumentException("Unknown rendering mode: " + style);
return _renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint);
}
private static double _renderPlainString
(String s, Graphics g, int x, int y, int w, int h, Font f,
Color foreground, int style, boolean paint)
{
if (f == null)
{
f = UIManager.getFont("controlFont");
if (f == null)
{
int fs = 11;
Object cfs = UIManager.get("customFontSize");
if (cfs instanceof Integer)
fs = ((Integer) cfs).intValue();
f = new Font("Dialog", Font.PLAIN, fs);
}
}
FontMetrics fm = g.getFontMetrics(f);
int wid;
if (Utilities.isMac())
wid = fm.stringWidth(s);
else
wid = (int)fm.getStringBounds(s, g).getWidth();
if (paint)
{
g.setColor(foreground);
g.setFont(f);
if ((wid <= w) || (style == STYLE_CLIP))
{
g.drawString(s, x, y);
}
else
{
char[] chars = s.toCharArray();
if (chars.length == 0)
return 0;
double chWidth = wid / chars.length;
int estCharsToPaint = new Double(w / chWidth).intValue();
if( estCharsToPaint > chars.length )
estCharsToPaint = chars.length;
while( estCharsToPaint > 3 )
{
if( estCharsToPaint < chars.length )
Arrays.fill(chars, estCharsToPaint - 3, estCharsToPaint, '.');
int newWidth;
if (Utilities.isMac())
newWidth = fm.stringWidth(new String(chars, 0, estCharsToPaint));
else
newWidth = (int)fm.getStringBounds(chars, 0, estCharsToPaint, g).getWidth();
if( newWidth <= w )
break;
estCharsToPaint--;
}
if (style == STYLE_TRUNCATE)
{
int length = estCharsToPaint;
if (length <= 0)
return 0;
if (paint)
{
if (length > 3)
g.drawChars(chars, 0, length, x, y);
else
{
Shape shape = g.getClip();
if (shape != null)
{
if (s != null)
{
Area area = new Area(shape);
area.intersect(new Area(new Rectangle(x, y, w, h)));
g.setClip(area);
}
else
{
g.setClip(new Rectangle(x, y, w, h));
}
}
g.drawString("...", x, y);
if (shape != null)
g.setClip(shape);
}
}
}
}
}
return wid;
}
public static double renderString
(String s, Graphics g, int x, int y, int w, int h, Font f,
Color defaultColor, int style, boolean paint)
{
switch (style)
{
case STYLE_CLIP:
case STYLE_TRUNCATE:
break;
default:
throw new IllegalArgumentException("Unknown rendering mode: " + style);
}
if (s.startsWith("<html") || s.startsWith("<HTML"))
return _renderHTML(s, 6, g, x, y, w, h, f, defaultColor, style, paint, null);
else
return renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint);
}
public static double renderHTML
(String s, Graphics g, int x, int y, int w, int h, Font f,
Color defaultColor, int style, boolean paint)
{
if ((style < 0) || (style > 1))
throw new IllegalArgumentException("Unknown rendering mode: " + style);
return _renderHTML(s, 0, g, x, y, w, h, f, defaultColor, style, paint, null);
}
static double _renderHTML
(String s, int pos, Graphics g, int x, int y, int w, int h, Font f,
Color defaultColor, int style, boolean paint, Color background)
{
if (f == null)
{
f = UIManager.getFont("controlFont");
if (f == null)
{
int fs = 11;
Object cfs = UIManager.get("customFontSize");
if (cfs instanceof Integer)
fs = ((Integer) cfs).intValue();
f = new Font("Dialog", Font.PLAIN, fs);
}
}
Stack<Color> _colorStack = SwingUtilities.isEventDispatchThread()
? HtmlRenderer.colorStack
: new Stack<Color>();
g.setColor(defaultColor);
g.setFont(f);
char[] chars = s.toCharArray();
int origX = x;
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;
double dotWidth = 0;
if (style == STYLE_TRUNCATE)
dotWidth = g.getFontMetrics().charWidth('.');
_colorStack.clear();
while (!done)
{
if (pos == s.length())
return widthPainted;
try
{
inTag |= (chars[pos] == '<');
}
catch (ArrayIndexOutOfBoundsException e)
{
ArrayIndexOutOfBoundsException aib
= new ArrayIndexOutOfBoundsException
("HTML rendering failed at position " + pos
+ " in String \"" + s + "\". Please report this "
+ "at http://www.netbeans.org");
if (STRICT_HTML)
throw aib;
else
{
Logger.getLogger(HtmlRenderer.class.getName()).log(Level.WARNING, null, aib);
return renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint);
}
}
inClosingTag = inTag
&& ((pos + 1) < chars.length)
&& (chars[pos + 1] == '/');
if (truncated)
{
g.setColor(defaultColor);
g.setFont(f);
if (paint)
g.drawString("...", x, y);
done = true;
}
else if (inTag)
{
pos++;
int tagEnd = pos;
done = tagEnd >= (chars.length - 1);
while (!done
&& (chars[tagEnd] != '>'))
{
done = tagEnd == (chars.length - 1);
tagEnd++;
}
if (done)
{
throwBadHTML("Matching '>' not found", pos, chars);
break;
}
if (inClosingTag)
{
pos++;
switch (chars[pos])
{
case 'P':
case 'p':
case 'H':
case 'h':
break;
case 'B':
case 'b':
if ((chars[pos + 1] == 'r')
|| (chars[pos + 1] == 'R'))
break;
if (!bold)
throwBadHTML("Closing bold tag w/o " +
"opening bold tag", pos, chars);
if (italic)
g.setFont(deriveFont(f, Font.ITALIC));
else
g.setFont(deriveFont(f, Font.PLAIN));
bold = false;
break;
case 'E':
case 'e':
case 'I':
case 'i':
if (bold)
g.setFont(deriveFont(f, Font.BOLD));
else
g.setFont(deriveFont(f, Font.PLAIN));
if (!italic)
throwBadHTML("Closing italics tag w/o"
+"opening italics tag", pos, chars);
italic = false;
break;
case 'S':
case 's':
switch (chars[pos + 1])
{
case 'T':
case 't':
if (italic)
g.setFont(deriveFont(f, Font.ITALIC));
else
g.setFont(deriveFont(f, Font.PLAIN));
bold = false;
break;
case '>':
strikethrough = false;
break;
}
break;
case 'U':
case 'u':
underline = false;
break;
case 'F':
case 'f':
if (_colorStack.isEmpty())
g.setColor(defaultColor);
else
g.setColor(_colorStack.pop());
break;
default:
throwBadHTML("Malformed or unsupported HTML",
pos, chars);
}
}
else
{
switch (chars[pos])
{
case 'B':
case 'b':
switch (chars[pos + 1])
{
case 'R':
case 'r':
if (style == STYLE_WORDWRAP)
{
x = origX;
int lineHeight = g.getFontMetrics().getHeight();
y += lineHeight;
heightPainted += lineHeight;
widthPainted = 0;
}
break;
case '>':
bold = true;
if (italic)
g.setFont(deriveFont(f, Font.BOLD | Font.ITALIC));
else
g.setFont(deriveFont(f, Font.BOLD));
break;
}
break;
case 'e':
case 'E':
case 'I':
case 'i':
italic = true;
if (bold)
g.setFont(deriveFont(f, Font.ITALIC | Font.BOLD));
else
g.setFont(deriveFont(f, Font.ITALIC));
break;
case 'S':
case 's':
switch (chars[pos + 1])
{
case '>':
strikethrough = true;
break;
case 'T':
case 't':
bold = true;
if (italic)
g.setFont(deriveFont(f, Font.BOLD | Font.ITALIC));
else
g.setFont(deriveFont(f, Font.BOLD));
break;
}
break;
case 'U':
case 'u':
underline = true;
break;
case 'f':
case 'F':
Color c = findColor(chars, pos, tagEnd);
_colorStack.push(g.getColor());
if (background != null)
c = HtmlLabelUI.ensureContrastingColor(c, background);
g.setColor(c);
break;
case 'P':
case 'p':
if (style == STYLE_WORDWRAP)
{
x = origX;
int lineHeight = g.getFontMetrics().getHeight();
y += (lineHeight + (lineHeight / 2));
heightPainted = y + lineHeight;
widthPainted = 0;
}
break;
case 'H':
case 'h':
if (pos == 1)
break;
else {
throwBadHTML("Malformed or unsupported HTML",
pos, chars);
break;
}
default:
throwBadHTML("Malformed or unsupported HTML",
pos, chars);
}
}
pos = tagEnd + (done ? 0 : 1);
inTag = false;
}
else
{
if (lastWasWhitespace)
{
while ((pos < (s.length() - 1))
&& Character.isWhitespace(chars[pos]))
pos++;
if (pos == (chars.length - 1))
return (style != STYLE_WORDWRAP)
? widthPainted
: heightPainted;
}
boolean isAmp = false;
boolean nextLtIsEntity = false;
int nextTag = chars.length - 1;
if ((chars[pos] == '&'))
{
boolean inEntity = pos != (chars.length - 1);
if (inEntity)
{
int newPos = substEntity(chars, pos + 1);
inEntity = newPos != -1;
if (inEntity)
{
pos = newPos;
isAmp = chars[pos] == '&';
nextLtIsEntity = chars[pos] == '<';
}
else
{
nextLtIsEntity = false;
isAmp = true;
}
}
}
else
{
nextLtIsEntity = false;
}
for (int i = pos; i < chars.length; i++)
{
if ((chars[i] == '<' && !nextLtIsEntity)
|| (chars[i] == '&' && !isAmp && i != chars.length - 1))
{
nextTag = i - 1;
break;
}
isAmp = false;
nextLtIsEntity = false;
}
FontMetrics fm = g.getFontMetrics();
Rectangle2D r = fm.getStringBounds(chars, pos, nextTag + 1, g);
if (Utilities.isMac())
r.setRect(r.getX(), r.getY(),
(double)fm.stringWidth(new String(chars, pos, nextTag - pos + 1)),
r.getHeight());
lastHeight = r.getHeight();
int length = (nextTag + 1) - pos;
boolean goToNextRow = false;
boolean brutalWrap = false;
double chWidth;
if (truncated)
chWidth = dotWidth;
else
{
chWidth = r.getWidth() / (nextTag+1 - pos);
if ((chWidth == Double.POSITIVE_INFINITY)
|| (chWidth == Double.NEGATIVE_INFINITY))
chWidth = fm.getMaxAdvance();
}
if (((style != STYLE_CLIP)
&& ((style == STYLE_TRUNCATE)
&& ((widthPainted + r.getWidth()) > w)))
|| ((style == STYLE_WORDWRAP)
&& ((widthPainted + r.getWidth()) > w)))
{
if (chWidth > 3)
{
double pixelsOff = (widthPainted + (r.getWidth() + 5)) - w;
double estCharsOver = pixelsOff / chWidth;
if (style == STYLE_TRUNCATE)
{
int charsToPaint = Math.round(Math.round(Math.ceil((w - widthPainted) / chWidth)));
int startPeriodsPos = (pos + charsToPaint) - 3;
if (startPeriodsPos >= chars.length)
startPeriodsPos = chars.length - 4;
length = (startPeriodsPos - pos);
if (length < 0)
length = 0;
r = fm.getStringBounds(chars, pos, pos + length, g);
if (Utilities.isMac())
r.setRect(r.getX(), r.getY(), (double)fm.stringWidth(new String(chars, pos, length)), r.getHeight());
truncated = true;
}
else
{
goToNextRow = true;
int lastChar = new Double(nextTag - estCharsOver).intValue();
brutalWrap = x == 0;
for (int i = lastChar; i > pos; i--)
{
lastChar--;
if (Character.isWhitespace(chars[i]))
{
length = (lastChar - pos) + 1;
brutalWrap = false;
break;
}
}
if ((lastChar <= pos)
&& (length > estCharsOver)
&& !brutalWrap)
{
x = origX;
y += r.getHeight();
heightPainted += r.getHeight();
boolean boundsChanged = false;
while (!done
&& Character.isWhitespace(chars[pos])
&& (pos < nextTag))
{
pos++;
boundsChanged = true;
done = pos == (chars.length - 1);
}
if (pos == nextTag)
lastWasWhitespace = true;
if (boundsChanged)
{
r = fm.getStringBounds(chars, pos, nextTag + 1, g);
if (Utilities.isMac())
r.setRect(r.getX(), r.getY(), (double)fm.stringWidth(new String(chars, pos, nextTag - pos + 1)), r.getHeight());
}
goToNextRow = false;
widthPainted = 0;
if (chars[pos - 1 + length] == '<')
length--;
}
else if (brutalWrap)
{
length = (new Double((w - widthPainted) / chWidth)).intValue();
if ((pos + length) > nextTag)
length = (nextTag - pos);
goToNextRow = true;
}
}
}
}
if (!done)
{
if (paint)
g.drawChars(chars, pos, length, x, y);
if (strikethrough || underline)
{
LineMetrics lm = fm.getLineMetrics(chars, pos, length - 1, g);
int lineWidth = new Double(x + r.getWidth()).intValue();
if (paint)
{
if (strikethrough)
{
int stPos = Math.round(lm.getStrikethroughOffset()) +
g.getFont().getBaselineFor(chars[pos]) + 1;
g.drawLine(x, y + stPos, lineWidth, y + stPos);
}
if (underline)
{
int stPos = Math.round(lm.getUnderlineOffset()) +
g.getFont().getBaselineFor(chars[pos]) + 1;
g.drawLine(x, y + stPos, lineWidth, y + stPos);
}
}
}
if (goToNextRow)
{
x = origX;
y += r.getHeight();
heightPainted += r.getHeight();
widthPainted = 0;
pos += (length);
while ((pos < chars.length)
&& (Character.isWhitespace(chars[pos]))
&& (chars[pos] != '<'))
pos++;
lastWasWhitespace = true;
done |= (pos >= chars.length);
}
else
{
x += r.getWidth();
widthPainted += r.getWidth();
lastWasWhitespace = Character.isWhitespace(chars[nextTag]);
pos = nextTag + 1;
}
done |= (nextTag == chars.length);
}
}
}
if (style != STYLE_WORDWRAP)
return widthPainted;
else
return heightPainted + lastHeight;
}
private static Color findColor
(final char[] ch, final int pos, final int tagEnd)
{
int colorPos = pos;
boolean useUIManager = false;
for (int i = pos; i < tagEnd; i++)
{
if (ch[i] == 'c') {
colorPos = i + 6;
if ((ch[colorPos] == '\'')
|| (ch[colorPos] == '"'))
colorPos++;
if (ch[colorPos] == '#')
{
colorPos++;
}
else if (ch[colorPos] == '!')
{
useUIManager = true;
colorPos++;
}
break;
}
}
if (colorPos == pos)
{
String out = "Could not find color identifier in font declaration";
throwBadHTML(out, pos, ch);
}
String s;
if (useUIManager)
{
int end = ch.length - 1;
for (int i = colorPos; i < ch.length; i++)
{
if ((ch[i] == '"') || (ch[i] == '\''))
{
end = i;
break;
}
}
s = new String(ch, colorPos, end - colorPos);
}
else
{
s = new String(ch, colorPos, 6);
}
Color result = null;
if (useUIManager)
{
result = UIManager.getColor(s);
if (result == null)
{
throwBadHTML("Could not resolve logical font declared in HTML: " + s,
pos, ch);
result = UIManager.getColor("textText");
if (result == null)
result = Color.BLACK;
}
}
else
{
try
{
int rgb = Integer.parseInt(s, 16);
result = new Color(rgb);
}
catch (NumberFormatException nfe)
{
throwBadHTML("Illegal hexadecimal color text: " + s +
" in HTML string", colorPos, ch);
}
}
if (result == null)
{
throwBadHTML("Unresolvable html color: " + s +
" in HTML string \n ", pos, ch);
}
return result;
}
private static final Font deriveFont(Font f, int style)
{
Font result = Utilities.isMac()
? new Font(f.getName(), style, f.getSize())
: f.deriveFont(style);
return result;
}
private static final int substEntity(char[] ch, int pos)
{
if (pos >= (ch.length - 2))
return -1;
if (ch[pos] == '#')
return substNumericEntity(ch, pos + 1);
boolean match;
for (int i = 0; i < entities.length; i++)
{
char[] c = (char[]) entities[i];
match = true;
if (c.length < (ch.length - pos))
{
for (int j = 0; j < c.length; j++)
match &= (c[j] == ch[j + pos]);
}
else
{
match = false;
}
if (match)
{
if (ch[pos + c.length] == ';')
{
ch[pos + c.length] = entitySubstitutions[i];
return pos + c.length;
}
}
}
return -1;
}
private static final int substNumericEntity(char[] ch, int pos)
{
for (int i = pos; i < ch.length; i++)
{
if (ch[i] == ';')
{
try
{
ch[i] = (char) Integer.parseInt(new String(ch, pos, i - pos));
return i;
}
catch (NumberFormatException nfe)
{
throwBadHTML("Unparsable numeric entity: " +
new String(ch, pos, i - pos), pos, ch);
}
}
}
return -1;
}
private static void throwBadHTML(String msg, int pos, char[] chars)
{
char[] chh = new char[pos];
Arrays.fill(chh, ' ');
chh[pos - 1] = '^';
String out = msg + "\n " + new String(chars) + "\n "
+ new String(chh) + "\n Full HTML string:" + new String(chars);
if (!STRICT_HTML)
{
if (LOG.isLoggable(Level.WARNING))
{
if (badStrings == null)
badStrings = new HashSet<String>();
if (!badStrings.contains(msg))
{
StringTokenizer tk = new StringTokenizer(out, "\n", false);
while (tk.hasMoreTokens())
LOG.warning(tk.nextToken());
badStrings.add(msg.intern());
}
}
}
else
{
throw new IllegalArgumentException(out);
}
}
public interface Renderer extends ListCellRenderer
{
void setParentFocused(boolean parentFocused);
void setCentered(boolean centered);
void setIndent(int pixels);
void setHtml(boolean val);
void setRenderStyle(int style);
void setIcon(Icon icon);
void reset();
void setText(String txt);
void setIconTextGap(int gap);
}
}