static double _renderHTML()

in platform/openide.awt/src/org/openide/awt/HtmlRenderer.java [520:1188]


    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, boolean forcedForeground
    ) {
        //        System.err.println ("rhs: " + y + " " + s);
        if (f == null) {
            f = UIManager.getFont("controlFont"); //NOI18N

            if (f == null) {
                int fs = 11;
                Object cfs = UIManager.get("customFontSize"); //NOI18N

                if (cfs instanceof Integer) {
                    fs = ((Integer) cfs).intValue();
                }

                f = new Font("Dialog", Font.PLAIN, fs); //NOI18N
            }
        }

        //Thread safety - avoid allocating memory for the common case
        LinkedList<Color> _colorStack = EventQueue.isDispatchThread() ? HtmlRenderer.colorStack : new LinkedList<Color>();

        g.setColor(defaultColor);
        g.setFont(f);
        GraphicsUtils.configureDefaultRenderingHints(g);

        char[] chars = s.toCharArray();
        int origX = x;
        boolean done = false; //flag if rendering completed, either by finishing the string or running out of space
        boolean inTag = false; //flag if the current position is inside a tag, and the tag should be processed rather than rendering
        boolean inClosingTag = false; //flag if the current position is inside a closing tag
        boolean strikethrough = false; //flag if a strikethrough line should be painted
        boolean underline = false; //flag if an underline should be painted
        boolean link = false; //flag if a link should be painted
        boolean bold = false; //flag if text is currently bold
        boolean italic = false; //flag if text is currently italic
        boolean truncated = false; //flag if the last possible character has been painted, and the next loop should paint "..." and return
        double widthPainted = 0; //the total width painted, for calculating needed space
        double heightPainted = 0; //the total height painted, for calculating needed space
        boolean lastWasWhitespace = false; //flag to skip additional whitespace if one whitespace char already painted
        double lastHeight = 0; //the last line height, for calculating total required height

        double dotWidth = 0;
        boolean dotsPainted = false;

        //Calculate the width of a . character if we may need to truncate
        if (style == STYLE_TRUNCATE) {
            dotWidth = g.getFontMetrics().charWidth('.'); //NOI18N
        }

        /* How this all works, for anyone maintaining this code (hopefully it will
          never need it):
          1. The string is converted to a char array
          2. Loop over the characters.  Variable pos is the current point.
            2a. See if we're in a tag by or'ing inTag with currChar == '<'
              If WE ARE IN A TAG:
               2a1: is it an opening tag?
                 If YES:
                   - Identify the tag, Configure the Graphics object with
                     the appropriate font, color, etc.  Set pos = the first
                     character after the tag
                 If NO (it's a closing tag)
                   - Identify the tag.  Reconfigure the Graphics object
                     with the state it should be in outside the tag
                     (reset the font if italic, pop a color off the stack, etc.)
            2b. If WE ARE NOT IN A TAG
               - Locate the next < or & character or the end of the string
               - Paint the characters using the Graphics object
               - Check underline and strikethrough tags, and paint line if
                 needed
            See if we're out of space, and do the right thing for the style
            (paint ..., give up or skip to the next line)
         */
        //Clear any junk left behind from a previous rendering loop
        _colorStack.clear();


        //Enter the painting loop
        while (!done) {
            if (pos == s.length()) {
                if( truncated && paint && !dotsPainted ) {
                    g.setColor(defaultColor);
                    g.setFont(f);
                    g.drawString("…", x, y); //NOI18N
                }
                return widthPainted;
            }

            //see if we're in a tag
            try {
                inTag |= (chars[pos] == '<');
            } catch (ArrayIndexOutOfBoundsException e) {
                //Should there be any problem, give a meaningful enough
                //message to reproduce the problem
                ArrayIndexOutOfBoundsException aib = new ArrayIndexOutOfBoundsException(
                        "HTML rendering failed at position " + pos + " in String \"" //NOI18N
                         +s + "\".  Please report this at http://www.netbeans.org"
                    ); //NOI18N

                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] == '/'); //NOI18N

            if (truncated) {
                //Then we've almost run out of space, time to print ... and quit
                g.setColor(defaultColor);
                g.setFont(f);

                if (paint) {
                    g.drawString("…", x, y); //NOI18N
                    dotsPainted = true; //make sure we paint the dots only once
                }

                done = true;
            } else if (inTag) {
                //If we're in a tag, don't paint, process it
                pos++;

                int tagEnd = pos;

                //#54237 - if done and end of string -> wrong html
                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) {
                    //Handle closing tags by resetting the Graphics object (font, etc.)
                    pos++;

                    switch (chars[pos]) {
                    case 'a': //NOI18N
                    case 'A': //NOI18N
                        
                        if (_colorStack.isEmpty() || forcedForeground) {
                            g.setColor(defaultColor);
                        } else {
                            g.setColor(_colorStack.pop());
                        }
                        link = false;

                        break;
                    case 'P': //NOI18N
                    case 'p': //NOI18N
                    case 'H': //NOI18N
                    case 'h':
                        break; //ignore html opening/closing tags

                    case 'B': //NOI18N
                    case 'b': //NOI18N

                        if ((chars[pos + 1] == 'r') || (chars[pos + 1] == 'R')) {
                            break;
                        }

                        if (!bold && !(chars[pos+1] == 'o' || chars[pos+1] == 'O')) {
                            throwBadHTML("Closing bold tag w/o " + //NOI18N
                                "opening bold tag", pos, chars
                            ); //NOI18N
                        }

                        if (italic) {
                            g.setFont(deriveFont(f, Font.ITALIC));
                        } else {
                            g.setFont(deriveFont(f, Font.PLAIN));
                        }

                        bold = false;

                        break;

                    case 'E': //NOI18N
                    case 'e': //em tag
                    case 'I': //NOI18N
                    case 'i': //NOI18N

                        if (bold) {
                            g.setFont(deriveFont(f, Font.BOLD));
                        } else {
                            g.setFont(deriveFont(f, Font.PLAIN));
                        }

                        if (!italic) {
                            throwBadHTML("Closing italics tag w/o" //NOI18N
                                 +"opening italics tag", pos, chars
                            ); //NOI18N
                        }

                        italic = false;

                        break;

                    case 'S': //NOI18N
                    case 's': //NOI18N

                        switch (chars[pos + 1]) {
                        case 'T': //NOI18N
                        case 't':

                            if (italic) { //NOI18N
                                g.setFont(deriveFont(f, Font.ITALIC));
                            } else {
                                g.setFont(deriveFont(f, Font.PLAIN));
                            }

                            bold = false;

                            break;

                        case '>': //NOI18N
                            strikethrough = false;

                            break;
                        }

                        break;

                    case 'U': //NOI18N
                    case 'u':
                        underline = false; //NOI18N

                        break;

                    case 'F': //NOI18N
                    case 'f': //NOI18N

                        if (_colorStack.isEmpty() || forcedForeground) {
                            g.setColor(defaultColor);
                        } else {
                            g.setColor(_colorStack.pop());
                        }

                        break;

                    default:
                        throwBadHTML("Malformed or unsupported HTML", //NOI18N
                            pos, chars
                        );
                    }
                } else {
                    //Okay, we're in an opening tag.  See which one and configure the Graphics object
                    switch (chars[pos]) {
                    case 'a': //NOI18N
                    case 'A': //NOI18N

                        if( !forcedForeground ) {
                            Color linkc = UIManager.getColor("nb.html.link.foreground");    // NOI18N
                            if (linkc == null) {
                                linkc = Color.BLUE;
                            }
                            _colorStack.push(g.getColor());

                            linkc = HtmlLabelUI.ensureContrastingColor(linkc, background);
                            g.setColor(linkc);
                        }
                        link = true;

                        break;
                        
                    case 'B': //NOI18N
                    case 'b': //NOI18N

                        switch (chars[pos + 1]) {
                        case 'R': //NOI18N
                        case 'r': //NOI18N

                            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': //NOI18N  //em tag
                    case 'E': //NOI18N
                    case 'I': //NOI18N
                    case 'i': //NOI18N
                        italic = true;

                        if (bold) {
                            g.setFont(deriveFont(f, Font.ITALIC | Font.BOLD));
                        } else {
                            g.setFont(deriveFont(f, Font.ITALIC));
                        }

                        break;

                    case 'S': //NOI18N
                    case 's': //NOI18N

                        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': //NOI18N
                    case 'u': //NOI18N
                        underline = true;

                        break;

                    case 'f': //NOI18N
                    case 'F': //NOI18N
                        if( !forcedForeground ) {
                            Color c = findColor(chars, pos, tagEnd);
                            _colorStack.push(g.getColor());

                            c = HtmlLabelUI.ensureContrastingColor(c, background);

                            g.setColor(c);
                        }

                        break;

                    case 'P': //NOI18N
                    case 'p': //NOI18N

                        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': //Just an opening HTML tag

                        if (pos == 1) {
                            break;
                        } else { // fallthrough warning
                            throwBadHTML("Malformed or unsupported HTML", pos, chars); //NOI18N
                            break;
                        }

                    default:
                        throwBadHTML("Malformed or unsupported HTML", pos, chars); //NOI18N
                    }
                }

                pos = tagEnd + (done ? 0 : 1);
                inTag = false;
            } else {
                //Okay, we're not in a tag, we need to paint
                if (lastWasWhitespace) {
                    //Skip multiple whitespace characters
                    while ((pos < (s.length() - 1)) && Character.isWhitespace(chars[pos])) {
                        pos++;
                    }

                    //Check strings terminating with multiple whitespace -
                    //otherwise could get an AIOOBE here
                    if (pos == (chars.length - 1)) {
                        return (style != STYLE_WORDWRAP) ? widthPainted : heightPainted;
                    }
                }

                //Flag to indicate if an ampersand entity was processed,
                //so the resulting & doesn't get treated as the beginning of
                //another entity (and loop endlessly)
                boolean isAmp = false;

                //Flag to indicate the next found < character really should
                //be painted (it came from an entity), it is not the beginning
                //of a tag
                boolean nextLtIsEntity = false;
                int nextTag = chars.length - 1;

                if ((chars[pos] == '&')) { //NOI18N

                    boolean inEntity = pos != (chars.length - 1);

                    if (inEntity) {
                        int newPos = substEntity(chars, pos + 1);
                        inEntity = newPos != -1;

                        if (inEntity) {
                            pos = newPos;
                            isAmp = chars[pos] == '&'; //NOI18N

                            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;
                    }

                    //Reset these flags so we don't skip all & or < chars for the rest of the string
                    isAmp = false;
                    nextLtIsEntity = false;
                }

                FontMetrics fm = g.getFontMetrics();

                //Get the bounds of the substring we'll paint
                Rectangle2D r = fm.getStringBounds(chars, pos, nextTag + 1, g);
                if (Utilities.isMac()) {
                    // #54257 - on macosx + chinese/japanese fonts, the getStringBounds() method returns bad value
                    r.setRect(r.getX(), r.getY(), (double)fm.stringWidth(new String(chars, pos, nextTag - pos + 1)), r.getHeight());
                } 

                //Store the height, so we can add it if we're in word wrap mode,
                //to return the height painted
                lastHeight = r.getHeight();

                //Work out the length of this tag
                int length = (nextTag + 1) - pos;

                //Flag to be set to true if we run out of space
                boolean goToNextRow = false;

                //Flag that the current line is longer than the available width,
                //and should be wrapped without finding a word boundary
                boolean brutalWrap = false;

                //Work out the per-character avg width of the string, for estimating
                //when we'll be out of space and should start the ... in truncate
                //mode
                double chWidth;

                if (truncated) {
                    //if we're truncating, use the width of one dot from an
                    //ellipsis to get an accurate result for truncation
                    chWidth = dotWidth;
                } else {
                    //calculate an average character width
                    chWidth = r.getWidth() / (nextTag+1 - pos);

                    //can return this sometimes, so handle it
                    if ((chWidth == Double.POSITIVE_INFINITY) || (chWidth == Double.NEGATIVE_INFINITY)) {
                        chWidth = fm.getMaxAdvance();
                    }
                }

                if (
                    ((style != STYLE_CLIP) &&
                        ((style == STYLE_TRUNCATE) && ((widthPainted + r.getWidth()) > (w /*- (chWidth * 3)*/)))) ||
                        /** mkleint - commented out the "- (chWidth *3) because it makes no sense to strip the text and add dots when it fits exactly
                         * into the rendering rectangle.. with this condition we stripped even strings that came close to the limit..
                         **/
                        ((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)));

                            /*                            System.err.println("estCharsOver = " + estCharsOver);
                                                        System.err.println("Chars to paint " + charsToPaint + " chwidth = " + chWidth + " widthPainted " + widthPainted);
                                                        System.err.println("Width painted + width of tag: " + (widthPainted + r.getWidth()) + " available: " + w);
                             */
                            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()) {
                                // #54257 - on macosx + chinese/japanese fonts, the getStringBounds() method returns bad value
                                r.setRect(r.getX(), r.getY(), (double)fm.stringWidth(new String(chars, pos, length)), r.getHeight());
                            } 

                            //                            System.err.println("Truncated set to true at " + pos + " (" + chars[pos] + ")");
                            truncated = true;
                        } else {
                            //Word wrap mode
                            goToNextRow = true;

                            int lastChar = (int)(nextTag - estCharsOver);

                            //Unlike Swing's word wrap, which does not wrap on tag boundaries correctly, if we're out of space,
                            //we're out of space
                            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) {
                                    //recalculate the width we will add
                                    r = fm.getStringBounds(chars, pos, nextTag + 1, g);
                                    if (Utilities.isMac()) {
                                        // #54257 - on macosx + chinese/japanese fonts, the getStringBounds() method returns bad value
                                        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) {
                                //wrap without checking word boundaries
                                length = (int)((w - widthPainted) / chWidth);

                                if ((pos + length) > nextTag) {
                                    length = (nextTag - pos);
                                }

                                goToNextRow = true;
                            }
                        }
                    }
                }

                if (!done) {
                    if (paint) {
                        g.drawChars(chars, pos, length, x, y);
                    }

                    if (strikethrough || underline || link) {
                        LineMetrics lm = fm.getLineMetrics(chars, pos, length - 1, g);
                        int lineWidth = (int)(x + r.getWidth());

                        if (paint) {
                            if (strikethrough) {
                                int stPos = Math.round(lm.getStrikethroughOffset()) +
                                    g.getFont().getBaselineFor(chars[pos]) + 1;

                                //PENDING - worth supporting with g.setStroke()? A one pixel line is most likely
                                //good enough
                                //int stThick = Math.round (lm.getStrikethroughThickness());
                                g.drawLine(x, y + stPos, lineWidth, y + stPos);
                            }

                            if (underline || link) {
                                int stPos = Math.round(lm.getUnderlineOffset()) +
                                    g.getFont().getBaselineFor(chars[pos]) + 1;

                                //PENDING - worth supporting with g.setStroke()? A one pixel line is most likely
                                //good enough
                                //int stThick = new Float (lm.getUnderlineThickness()).intValue();
                                g.drawLine(x, y + stPos, lineWidth, y + stPos);
                            }
                        }
                    }
                    
                    if (goToNextRow) {
                        //if we're in word wrap mode and need to go to the next
                        //line, reconfigure the x and y coordinates
                        x = origX;
                        y += r.getHeight();
                        heightPainted += r.getHeight();
                        widthPainted = 0;
                        pos += (length);

                        //skip any leading whitespace
                        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;
        }
    }