All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.rjeschke.txtmark.Emitter Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
/*
 * Copyright (C) 2011-2015 René Jeschke 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.rjeschke.txtmark;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * Emitter class responsible for generating HTML output.
 *
 * @author René Jeschke 
 */
class Emitter
{
    /** Link references. */
    private final HashMap linkRefs      = new HashMap();
    /** The configuration. */
    private final Configuration            config;
    /** Extension flag. */
    public boolean                         useExtensions = false;

    /** Constructor. */
    public Emitter(final Configuration config)
    {
        this.config = config;
        this.useExtensions = config.forceExtendedProfile;
    }

    /**
     * Adds a LinkRef to this set of LinkRefs.
     *
     * @param key
     *            The key/id.
     * @param linkRef
     *            The LinkRef.
     */
    public void addLinkRef(final String key, final LinkRef linkRef)
    {
        this.linkRefs.put(key.toLowerCase(), linkRef);
    }

    /**
     * Transforms the given block recursively into HTML.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param root
     *            The Block to process.
     */
    public void emit(final StringBuilder out, final Block root)
    {
        root.removeSurroundingEmptyLines();

        switch (root.type)
        {
        case RULER:
            this.config.decorator.horizontalRuler(out);
            return;
        case NONE:
        case XML:
            break;
        case HEADLINE:
            this.config.decorator.openHeadline(out, root.hlDepth);
            if (this.useExtensions && root.id != null)
            {
                out.append(" id=\"");
                Utils.appendCode(out, root.id, 0, root.id.length());
                out.append('"');
            }
            out.append('>');
            break;
        case PARAGRAPH:
            this.config.decorator.openParagraph(out);
            break;
        case CODE:
        case FENCED_CODE:
            if (this.config.codeBlockEmitter == null)
            {
                this.config.decorator.openCodeBlock(out);
            }
            break;
        case BLOCKQUOTE:
            this.config.decorator.openBlockquote(out);
            break;
        case UNORDERED_LIST:
            this.config.decorator.openUnorderedList(out);
            break;
        case ORDERED_LIST:
            this.config.decorator.openOrderedList(out);
            break;
        case LIST_ITEM:
            this.config.decorator.openListItem(out);
            if (this.useExtensions && root.id != null)
            {
                out.append(" id=\"");
                Utils.appendCode(out, root.id, 0, root.id.length());
                out.append('"');
            }
            out.append('>');
            break;
        }

        if (root.hasLines())
        {
            this.emitLines(out, root);
        }
        else
        {
            Block block = root.blocks;
            while (block != null)
            {
                this.emit(out, block);
                block = block.next;
            }
        }

        switch (root.type)
        {
        case RULER:
        case NONE:
        case XML:
            break;
        case HEADLINE:
            this.config.decorator.closeHeadline(out, root.hlDepth);
            break;
        case PARAGRAPH:
            this.config.decorator.closeParagraph(out);
            break;
        case CODE:
        case FENCED_CODE:
            if (this.config.codeBlockEmitter == null)
            {
                this.config.decorator.closeCodeBlock(out);
            }
            break;
        case BLOCKQUOTE:
            this.config.decorator.closeBlockquote(out);
            break;
        case UNORDERED_LIST:
            this.config.decorator.closeUnorderedList(out);
            break;
        case ORDERED_LIST:
            this.config.decorator.closeOrderedList(out);
            break;
        case LIST_ITEM:
            this.config.decorator.closeListItem(out);
            break;
        }
    }

    /**
     * Transforms lines into HTML.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param block
     *            The Block to process.
     */
    private void emitLines(final StringBuilder out, final Block block)
    {
        switch (block.type)
        {
        case CODE:
            this.emitCodeLines(out, block.lines, block.meta, true);
            break;
        case FENCED_CODE:
            this.emitCodeLines(out, block.lines, block.meta, false);
            break;
        case XML:
            this.emitRawLines(out, block.lines);
            break;
        default:
            this.emitMarkedLines(out, block.lines);
            break;
        }
    }

    /**
     * Finds the position of the given Token in the given String.
     *
     * @param in
     *            The String to search on.
     * @param start
     *            The starting character position.
     * @param token
     *            The token to find.
     * @return The position of the token or -1 if none could be found.
     */
    private int findToken(final String in, final int start, final MarkToken token)
    {
        int pos = start;
        while (pos < in.length())
        {
            if (this.getToken(in, pos) == token)
            {
                return pos;
            }
            pos++;
        }
        return -1;
    }

    /**
     * Checks if there is a valid markdown link definition.
     *
     * @param out
     *            The StringBuilder containing the generated output.
     * @param in
     *            Input String.
     * @param start
     *            Starting position.
     * @param token
     *            Either LINK or IMAGE.
     * @return The new position or -1 if there is no valid markdown link.
     */
    private int checkLink(final StringBuilder out, final String in, final int start, final MarkToken token)
    {
        boolean isAbbrev = false;
        int pos = start + (token == MarkToken.LINK ? 1 : 2);
        final StringBuilder temp = new StringBuilder();

        temp.setLength(0);
        pos = Utils.readMdLinkId(temp, in, pos);
        if (pos < start)
        {
            return -1;
        }

        final String name = temp.toString();
        String link = null, comment = null;
        final int oldPos = pos++;
        pos = Utils.skipSpaces(in, pos);
        if (pos < start)
        {
            final LinkRef lr = this.linkRefs.get(name.toLowerCase());
            if (lr != null)
            {
                isAbbrev = lr.isAbbrev;
                link = lr.link;
                comment = lr.title;
                pos = oldPos;
            }
            else
            {
                return -1;
            }
        }
        else if (in.charAt(pos) == '(')
        {
            pos++;
            pos = Utils.skipSpaces(in, pos);
            if (pos < start)
            {
                return -1;
            }
            temp.setLength(0);
            final boolean useLt = in.charAt(pos) == '<';
            pos = useLt ? Utils.readUntil(temp, in, pos + 1, '>') : Utils.readMdLink(temp, in, pos);
            if (pos < start)
            {
                return -1;
            }
            if (useLt)
            {
                pos++;
            }
            link = temp.toString();

            if (in.charAt(pos) == ' ')
            {
                pos = Utils.skipSpaces(in, pos);
                if (pos > start && in.charAt(pos) == '"')
                {
                    pos++;
                    temp.setLength(0);
                    pos = Utils.readUntil(temp, in, pos, '"');
                    if (pos < start)
                    {
                        return -1;
                    }
                    comment = temp.toString();
                    pos++;
                    pos = Utils.skipSpaces(in, pos);
                    if (pos == -1)
                    {
                        return -1;
                    }
                }
            }
            if (in.charAt(pos) != ')')
            {
                return -1;
            }
        }
        else if (in.charAt(pos) == '[')
        {
            pos++;
            temp.setLength(0);
            pos = Utils.readRawUntil(temp, in, pos, ']');
            if (pos < start)
            {
                return -1;
            }
            final String id = temp.length() > 0 ? temp.toString() : name;
            final LinkRef lr = this.linkRefs.get(id.toLowerCase());
            if (lr != null)
            {
                link = lr.link;
                comment = lr.title;
            }
        }
        else
        {
            final LinkRef lr = this.linkRefs.get(name.toLowerCase());
            if (lr != null)
            {
                isAbbrev = lr.isAbbrev;
                link = lr.link;
                comment = lr.title;
                pos = oldPos;
            }
            else
            {
                return -1;
            }
        }

        if (link == null)
        {
            return -1;
        }

        if (token == MarkToken.LINK)
        {
            if (isAbbrev && comment != null)
            {
                if (!this.useExtensions)
                {
                    return -1;
                }
                out.append("");
                this.recursiveEmitLine(out, name, 0, MarkToken.NONE);
                out.append("");
            }
            else
            {
                this.config.decorator.openLink(out);
                out.append(" href=\"");
                Utils.appendValue(out, link, 0, link.length());
                out.append('"');
                if (comment != null)
                {
                    out.append(" title=\"");
                    Utils.appendValue(out, comment, 0, comment.length());
                    out.append('"');
                }
                out.append('>');
                this.recursiveEmitLine(out, name, 0, MarkToken.NONE);
                this.config.decorator.closeLink(out);
            }
        }
        else
        {
            this.config.decorator.openImage(out);
            out.append(" src=\"");
            Utils.appendValue(out, link, 0, link.length());
            out.append("\" alt=\"");
            Utils.appendValue(out, name, 0, name.length());
            out.append('"');
            if (comment != null)
            {
                out.append(" title=\"");
                Utils.appendValue(out, comment, 0, comment.length());
                out.append('"');
            }
            this.config.decorator.closeImage(out);
        }

        return pos;
    }

    /**
     * Check if there is a valid HTML tag here. This method also transforms auto
     * links and mailto auto links.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Starting position.
     * @return The new position or -1 if nothing valid has been found.
     */
    private int checkHtml(final StringBuilder out, final String in, final int start)
    {
        final StringBuilder temp = new StringBuilder();
        int pos;

        // Check for auto links
        temp.setLength(0);
        pos = Utils.readUntil(temp, in, start + 1, ':', ' ', '>', '\n');
        if (pos != -1 && in.charAt(pos) == ':' && HTML.isLinkPrefix(temp.toString()))
        {
            pos = Utils.readUntil(temp, in, pos, '>');
            if (pos != -1)
            {
                final String link = temp.toString();
                this.config.decorator.openLink(out);
                out.append(" href=\"");
                Utils.appendValue(out, link, 0, link.length());
                out.append("\">");
                Utils.appendValue(out, link, 0, link.length());
                this.config.decorator.closeLink(out);
                return pos;
            }
        }

        // Check for mailto auto link
        temp.setLength(0);
        pos = Utils.readUntil(temp, in, start + 1, '@', ' ', '>', '\n');
        if (pos != -1 && in.charAt(pos) == '@')
        {
            pos = Utils.readUntil(temp, in, pos, '>');
            if (pos != -1)
            {
                final String link = temp.toString();
                this.config.decorator.openLink(out);
                out.append(" href=\"");
                Utils.appendMailto(out, "mailto:", 0, 7);
                Utils.appendMailto(out, link, 0, link.length());
                out.append("\">");
                Utils.appendMailto(out, link, 0, link.length());
                this.config.decorator.closeLink(out);
                return pos;
            }
        }

        // Check for inline html
        if (start + 2 < in.length())
        {
            temp.setLength(0);
            return Utils.readXML(out, in, start, this.config.safeMode);
        }

        return -1;
    }

    /**
     * Check if this is a valid XML/HTML entity.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Starting position
     * @return The new position or -1 if this entity in invalid.
     */
    private static int checkEntity(final StringBuilder out, final String in, final int start)
    {
        final int pos = Utils.readUntil(out, in, start, ';');
        if (pos < 0 || out.length() < 3)
        {
            return -1;
        }
        if (out.charAt(1) == '#')
        {
            if (out.charAt(2) == 'x' || out.charAt(2) == 'X')
            {
                if (out.length() < 4)
                {
                    return -1;
                }
                for (int i = 3; i < out.length(); i++)
                {
                    final char c = out.charAt(i);
                    if ((c < '0' || c > '9') && ((c < 'a' || c > 'f') && (c < 'A' || c > 'F')))
                    {
                        return -1;
                    }
                }
            }
            else
            {
                for (int i = 2; i < out.length(); i++)
                {
                    final char c = out.charAt(i);
                    if (c < '0' || c > '9')
                    {
                        return -1;
                    }
                }
            }
            out.append(';');
        }
        else
        {
            for (int i = 1; i < out.length(); i++)
            {
                final char c = out.charAt(i);
                if (!Character.isLetterOrDigit(c))
                {
                    return -1;
                }
            }
            out.append(';');
            return HTML.isEntity(out.toString()) ? pos : -1;
        }

        return pos;
    }

    /**
     * Recursively scans through the given line, taking care of any markdown
     * stuff.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Start position.
     * @param token
     *            The matching Token (for e.g. '*')
     * @return The position of the matching Token or -1 if token was NONE or no
     *         Token could be found.
     */
    private int recursiveEmitLine(final StringBuilder out, final String in, final int start, final MarkToken token)
    {
        int pos = start, a, b;
        final StringBuilder temp = new StringBuilder();
        while (pos < in.length())
        {
            final MarkToken mt = this.getToken(in, pos);
            if (token != MarkToken.NONE
                    && (mt == token || token == MarkToken.EM_STAR && mt == MarkToken.STRONG_STAR || token == MarkToken.EM_UNDERSCORE
                            && mt == MarkToken.STRONG_UNDERSCORE))
            {
                return pos;
            }

            switch (mt)
            {
            case IMAGE:
            case LINK:
                temp.setLength(0);
                b = this.checkLink(temp, in, pos, mt);
                if (b > 0)
                {
                    out.append(temp);
                    pos = b;
                }
                else
                {
                    out.append(in.charAt(pos));
                }
                break;
            case EM_STAR:
            case EM_UNDERSCORE:
                temp.setLength(0);
                b = this.recursiveEmitLine(temp, in, pos + 1, mt);
                if (b > 0)
                {
                    this.config.decorator.openEmphasis(out);
                    out.append(temp);
                    this.config.decorator.closeEmphasis(out);
                    pos = b;
                }
                else
                {
                    out.append(in.charAt(pos));
                }
                break;
            case STRONG_STAR:
            case STRONG_UNDERSCORE:
                temp.setLength(0);
                b = this.recursiveEmitLine(temp, in, pos + 2, mt);
                if (b > 0)
                {
                    this.config.decorator.openStrong(out);
                    out.append(temp);
                    this.config.decorator.closeStrong(out);
                    pos = b + 1;
                }
                else
                {
                    out.append(in.charAt(pos));
                }
                break;
            case SUPER:
                temp.setLength(0);
                b = this.recursiveEmitLine(temp, in, pos + 1, mt);
                if (b > 0)
                {
                    this.config.decorator.openSuper(out);
                    out.append(temp);
                    this.config.decorator.closeSuper(out);
                    pos = b;
                }
                else
                {
                    out.append(in.charAt(pos));
                }
                break;
            case CODE_SINGLE:
            case CODE_DOUBLE:
                a = pos + (mt == MarkToken.CODE_DOUBLE ? 2 : 1);
                b = this.findToken(in, a, mt);
                if (b > 0)
                {
                    pos = b + (mt == MarkToken.CODE_DOUBLE ? 1 : 0);
                    while (a < b && in.charAt(a) == ' ')
                    {
                        a++;
                    }
                    if (a < b)
                    {
                        while (in.charAt(b - 1) == ' ')
                        {
                            b--;
                        }
                        this.config.decorator.openCodeSpan(out);
                        Utils.appendCode(out, in, a, b);
                        this.config.decorator.closeCodeSpan(out);
                    }
                }
                else
                {
                    out.append(in.charAt(pos));
                }
                break;
            case HTML:
                temp.setLength(0);
                b = this.checkHtml(temp, in, pos);
                if (b > 0)
                {
                    out.append(temp);
                    pos = b;
                }
                else
                {
                    out.append("<");
                }
                break;
            case ENTITY:
                temp.setLength(0);
                b = checkEntity(temp, in, pos);
                if (b > 0)
                {
                    out.append(temp);
                    pos = b;
                }
                else
                {
                    out.append("&");
                }
                break;
            case X_LINK_OPEN:
                temp.setLength(0);
                b = this.recursiveEmitLine(temp, in, pos + 2, MarkToken.X_LINK_CLOSE);
                if (b > 0 && this.config.specialLinkEmitter != null)
                {
                    this.config.specialLinkEmitter.emitSpan(out, temp.toString());
                    pos = b + 1;
                }
                else
                {
                    out.append(in.charAt(pos));
                }
                break;
            case X_COPY:
                out.append("©");
                pos += 2;
                break;
            case X_REG:
                out.append("®");
                pos += 2;
                break;
            case X_TRADE:
                out.append("™");
                pos += 3;
                break;
            case X_NDASH:
                out.append("–");
                pos++;
                break;
            case X_MDASH:
                out.append("—");
                pos += 2;
                break;
            case X_HELLIP:
                out.append("…");
                pos += 2;
                break;
            case X_LAQUO:
                out.append("«");
                pos++;
                break;
            case X_RAQUO:
                out.append("»");
                pos++;
                break;
            case X_RDQUO:
                out.append("”");
                break;
            case X_LDQUO:
                out.append("“");
                break;
            case ESCAPE:
                pos++;
                //$FALL-THROUGH$
            default:
                out.append(in.charAt(pos));
                break;
            }
            pos++;
        }
        return -1;
    }

    /**
     * Turns every whitespace character into a space character.
     *
     * @param c
     *            Character to check
     * @return 32 is c was a whitespace, c otherwise
     */
    private static char whitespaceToSpace(final char c)
    {
        return Character.isWhitespace(c) ? ' ' : c;
    }

    /**
     * Check if there is any markdown Token.
     *
     * @param in
     *            Input String.
     * @param pos
     *            Starting position.
     * @return The Token.
     */
    private MarkToken getToken(final String in, final int pos)
    {
        final char c0 = pos > 0 ? whitespaceToSpace(in.charAt(pos - 1)) : ' ';
        final char c = whitespaceToSpace(in.charAt(pos));
        final char c1 = pos + 1 < in.length() ? whitespaceToSpace(in.charAt(pos + 1)) : ' ';
        final char c2 = pos + 2 < in.length() ? whitespaceToSpace(in.charAt(pos + 2)) : ' ';
        final char c3 = pos + 3 < in.length() ? whitespaceToSpace(in.charAt(pos + 3)) : ' ';

        switch (c)
        {
        case '*':
            if (c1 == '*')
            {
                return c0 != ' ' || c2 != ' ' ? MarkToken.STRONG_STAR : MarkToken.EM_STAR;
            }
            return c0 != ' ' || c1 != ' ' ? MarkToken.EM_STAR : MarkToken.NONE;
        case '_':
            if (c1 == '_')
            {
                return c0 != ' ' || c2 != ' ' ? MarkToken.STRONG_UNDERSCORE : MarkToken.EM_UNDERSCORE;
            }
            if (this.useExtensions)
            {
                return Character.isLetterOrDigit(c0) && c0 != '_' && Character.isLetterOrDigit(c1) ? MarkToken.NONE
                        : MarkToken.EM_UNDERSCORE;
            }
            return c0 != ' ' || c1 != ' ' ? MarkToken.EM_UNDERSCORE : MarkToken.NONE;
        case '!':
            if (c1 == '[')
            {
                return MarkToken.IMAGE;
            }
            return MarkToken.NONE;
        case '[':
            if (this.useExtensions && c1 == '[')
            {
                return MarkToken.X_LINK_OPEN;
            }
            return MarkToken.LINK;
        case ']':
            if (this.useExtensions && c1 == ']')
            {
                return MarkToken.X_LINK_CLOSE;
            }
            return MarkToken.NONE;
        case '`':
            return c1 == '`' ? MarkToken.CODE_DOUBLE : MarkToken.CODE_SINGLE;
        case '\\':
            switch (c1)
            {
            case '\\':
            case '[':
            case ']':
            case '(':
            case ')':
            case '{':
            case '}':
            case '#':
            case '"':
            case '\'':
            case '.':
            case '>':
            case '<':
            case '*':
            case '+':
            case '-':
            case '_':
            case '!':
            case '`':
            case '~':
            case '^':
                return MarkToken.ESCAPE;
            default:
                return MarkToken.NONE;
            }
        case '<':
            if (this.useExtensions && c1 == '<')
            {
                return MarkToken.X_LAQUO;
            }
            return MarkToken.HTML;
        case '&':
            return MarkToken.ENTITY;
        default:
            if (this.useExtensions)
            {
                switch (c)
                {
                case '-':
                    if (c1 == '-')
                    {
                        return c2 == '-' ? MarkToken.X_MDASH : MarkToken.X_NDASH;
                    }
                    break;
                case '^':
                    return c0 == '^' || c1 == '^' ? MarkToken.NONE : MarkToken.SUPER;
                case '>':
                    if (c1 == '>')
                    {
                        return MarkToken.X_RAQUO;
                    }
                    break;
                case '.':
                    if (c1 == '.' && c2 == '.')
                    {
                        return MarkToken.X_HELLIP;
                    }
                    break;
                case '(':
                    if (c1 == 'C' && c2 == ')')
                    {
                        return MarkToken.X_COPY;
                    }
                    if (c1 == 'R' && c2 == ')')
                    {
                        return MarkToken.X_REG;
                    }
                    if (c1 == 'T' & c2 == 'M' & c3 == ')')
                    {
                        return MarkToken.X_TRADE;
                    }
                    break;
                case '"':
                    if (!Character.isLetterOrDigit(c0) && c1 != ' ')
                    {
                        return MarkToken.X_LDQUO;
                    }
                    if (c0 != ' ' && !Character.isLetterOrDigit(c1))
                    {
                        return MarkToken.X_RDQUO;
                    }
                    break;
                }
            }
            return MarkToken.NONE;
        }
    }

    /**
     * Writes a set of markdown lines into the StringBuilder.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param lines
     *            The lines to write.
     */
    private void emitMarkedLines(final StringBuilder out, final Line lines)
    {
        final StringBuilder in = new StringBuilder();
        Line line = lines;
        while (line != null)
        {
            if (!line.isEmpty)
            {
                in.append(line.value.substring(line.leading, line.value.length() - line.trailing));
                if (line.trailing >= 2)
                {
                    in.append("
"); } } if (line.next != null) { in.append('\n'); } line = line.next; } this.recursiveEmitLine(out, in.toString(), 0, MarkToken.NONE); } /** * Writes a set of raw lines into the StringBuilder. * * @param out * The StringBuilder to write to. * @param lines * The lines to write. */ private void emitRawLines(final StringBuilder out, final Line lines) { Line line = lines; if (this.config.safeMode) { final StringBuilder temp = new StringBuilder(); while (line != null) { if (!line.isEmpty) { temp.append(line.value); } temp.append('\n'); line = line.next; } final String in = temp.toString(); for (int pos = 0; pos < in.length(); pos++) { if (in.charAt(pos) == '<') { temp.setLength(0); final int t = Utils.readXML(temp, in, pos, this.config.safeMode); if (t != -1) { out.append(temp); pos = t; } else { out.append(in.charAt(pos)); } } else { out.append(in.charAt(pos)); } } } else { while (line != null) { if (!line.isEmpty) { out.append(line.value); } out.append('\n'); line = line.next; } } } /** * Writes a code block into the StringBuilder. * * @param out * The StringBuilder to write to. * @param lines * The lines to write. * @param meta * Meta information. */ private void emitCodeLines(final StringBuilder out, final Line lines, final String meta, final boolean removeIndent) { Line line = lines; if (this.config.codeBlockEmitter != null) { final ArrayList list = new ArrayList(); while (line != null) { if (line.isEmpty) { list.add(""); } else { list.add(removeIndent ? line.value.substring(4) : line.value); } line = line.next; } this.config.codeBlockEmitter.emitBlock(out, list, meta); } else { while (line != null) { if (!line.isEmpty) { for (int i = removeIndent ? 4 : 0; i < line.value.length(); i++) { final char c; switch (c = line.value.charAt(i)) { case '&': out.append("&"); break; case '<': out.append("<"); break; case '>': out.append(">"); break; default: out.append(c); break; } } } out.append('\n'); line = line.next; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy