com.avapira.bobroreader.hanabira.HanabiraParser.java Source code

Java tutorial

Introduction

Here is the source code for com.avapira.bobroreader.hanabira.HanabiraParser.java

Source

/*
 * Bobroreader is open source software, created, maintained, and shared under
 * the MIT license by Avadend Piroserpen Arts. The project includes components
 * from other open source projects which remain under their existing licenses,
 * detailed in their respective source files.
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2015. Avadend Piroserpen Arts Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 *
 */

package com.avapira.bobroreader.hanabira;

import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.text.Layout;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.style.*;
import android.text.util.Linkify;
import android.util.Log;
import android.view.View;
import com.avapira.bobroreader.PostDialogFragment;
import com.avapira.bobroreader.R;
import com.avapira.bobroreader.hanabira.entity.HanabiraBoard;
import com.avapira.bobroreader.hanabira.entity.HanabiraPost;
import com.mikepenz.iconics.utils.Utils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 */
public class HanabiraParser {

    private static boolean contextInitCompleted = false;
    private static int bulletMarginPerLevel;
    private static int quoteColor;
    private static int spoilerHiddenColor;
    private static int spoilerShownColor;
    private static int refLinkColor;
    private static boolean showSpoilers;
    private final SpannableStringBuilder builder;
    private final HanabiraPost post;
    Pattern refPost = Pattern.compile(">>(/?[a-z]{1,4}/)?([0-9]+)");
    Pattern bold = Pattern.compile("(\\*\\*|__)(.*?)\\1");
    Pattern italic = Pattern.compile("(\\*|_)(.*?)\\1");
    Pattern ulList = Pattern.compile("\\n\\*(.*)");
    Pattern olList = Pattern.compile("\\n[0-9]+\\.(.*)");
    Pattern code = Pattern.compile("`(.*?)`");

    public HanabiraParser(HanabiraPost post) {
        Context context = Hanabira.getFlower();
        if (!contextInitCompleted) {
            bulletMarginPerLevel = Utils.convertDpToPx(context.getApplicationContext(), 12);
            spoilerHiddenColor = context.getColor(R.color.dobro_dark);
            spoilerShownColor = context.getColor(R.color.dobro_primary_text);
            refLinkColor = context.getColor(R.color.dobro_ref_text);
            quoteColor = context.getColor(R.color.dobro_quote);
            showSpoilers = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("show_spoilers",
                    false);
            contextInitCompleted = true;
        }
        this.post = post;
        builder = new SpannableStringBuilder(replaceInternalLinkWithReference(post.getMessage()));
        Linkify.addLinks(builder, Linkify.WEB_URLS);
    }

    private static abstract class SpanObjectFactory {

        public static final SpanObjectFactory ITALIC = new SpanObjectFactory() {
            @Override
            Object getSpan() {
                return new StyleSpan(Typeface.ITALIC);
            }
        };
        public static final SpanObjectFactory BOLD = new SpanObjectFactory() {
            @Override
            Object getSpan() {
                return new StyleSpan(Typeface.BOLD);
            }
        };
        public static final SpanObjectFactory BOLD_ITALIC = new SpanObjectFactory() {
            @Override
            Object getSpan() {
                return new StyleSpan(Typeface.BOLD_ITALIC);
            }
        };
        public static final SpanObjectFactory SPOILER = new SpanObjectFactory() {
            @Override
            Object getSpan() {
                return new SpoilerSpan();
            }
        };
        public static final SpanObjectFactory CODE_BLOCK = new SpanObjectFactory() {
            @Override
            Object getSpan() {
                return new CodeBlockSpan();
            }
        };

        abstract Object getSpan();
    }

    private static class QuoteSpan extends CharacterStyle implements UpdateAppearance {

        @Override
        public void updateDrawState(TextPaint tp) {
            tp.setColor(quoteColor);
        }

    }

    private static class SpoilerSpan extends ClickableSpan implements UpdateAppearance {

        Boolean wasShown;

        public SpoilerSpan() {
            super();
            wasShown = !HanabiraParser.showSpoilers;
            // todo maybe make it as "always show"/"show on start(hideable)"/"hidden"?
        }

        @Override
        public void onClick(View widget) {
            Log.d("Spoiler#onClick", "clickclick; wasShown = " + wasShown);
            wasShown = !wasShown;
            widget.invalidate();
        }

        @Override
        public void updateDrawState(@NonNull TextPaint tp) {
            tp.bgColor = spoilerHiddenColor;
            if (wasShown) {
                tp.setColor(spoilerHiddenColor);
            } else {
                tp.setColor(spoilerShownColor);
            }
        }
    }

    private static class CodeBlockSpan extends TypefaceSpan implements UpdateAppearance {

        public CodeBlockSpan() {
            super("monospaced");
        }

        private static void applyCustomTypeFace(Paint paint) {
            int oldStyle;
            Typeface old = paint.getTypeface();
            if (old == null) {
                oldStyle = 0;
            } else {
                oldStyle = old.getStyle();
            }

            int fake = oldStyle & ~Typeface.MONOSPACE.getStyle();
            if ((fake & Typeface.BOLD) != 0) {
                paint.setFakeBoldText(true);
            }

            if ((fake & Typeface.ITALIC) != 0) {
                paint.setTextSkewX(-0.25f);
            }

            paint.setTypeface(Typeface.MONOSPACE);
        }

        @Override
        public void updateDrawState(@NonNull TextPaint tp) {
            tp.setColor(spoilerHiddenColor);
            applyCustomTypeFace(tp);
        }

        @Override
        public void updateMeasureState(@NonNull TextPaint paint) {
            applyCustomTypeFace(paint);
        }
    }

    private static class BulletListSpan implements LeadingMarginSpan {

        private int level;

        public BulletListSpan(int level) {
            if (level < 1 || level > 2) {
                throw new IllegalArgumentException("Only 1- and 2-level bullet lists available");
            }
            this.level = level;
        }

        @Override
        public int getLeadingMargin(boolean first) {
            switch (level) {
            case 1:
                return first ? 0 : bulletMarginPerLevel;
            case 2:
                return first ? bulletMarginPerLevel : 2 * bulletMarginPerLevel;
            default:
                throw new InternalError(String.format(
                        "Found level value equals %s instead \'1\' or \'2\' after constructor check", level));

            }
        }

        @Override
        public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom,
                CharSequence text, int start, int end, boolean first, Layout layout) {

        }
    }

    private static class HanabiraLinkSpan extends ClickableSpan implements UpdateAppearance {

        private final String board;
        private final int postDisplayId;

        public HanabiraLinkSpan(@NonNull String board, @NonNull String post) {
            this.board = board;
            postDisplayId = Integer.parseInt(post);
        }

        @Override
        public void onClick(View widget) {
            Log.i("Link span", String.format("Opening post >>%s/%d", board, postDisplayId));
            LocalBroadcastManager.getInstance(Hanabira.getFlower())
                    .sendBroadcast(new Intent().setAction(Hanabira.Action.OPEN_POST.name())
                            .putExtra(PostDialogFragment.ARG_BOARD, board)
                            .putExtra(PostDialogFragment.ARG_ID, postDisplayId));
        }

        @Override
        public void updateDrawState(@NonNull TextPaint ds) {
            ds.setColor(refLinkColor);
            ds.setUnderlineText(true);
        }
    }

    private static String replaceInternalLinkWithReference(String message) {
        Pattern p = Pattern
                .compile("https?://dobrochan\\.(?:ru|com|org)/([a-z]{1,4})/res/(\\d+)\\.xhtml(?:#i(\\d+))?");
        Matcher matcher = p.matcher(message);
        if (matcher.groupCount() == 4) { // whole, board, thread, (opt) post
            return matcher.replaceAll(">>$1/$3"); //>>/board/display_id_post
        } else {
            return matcher.replaceAll(">>$1/$2"); //>>/board/thread_display_id
        }
    }

    public CharSequence getFormatted() {
        formatTwoLevelBulletList();
        paintQuotes();

        //replaceAll
        formatCode();
        formatSpoiler();
        paintBoldItalic();
        paintBold();
        paintItalic();

        embedRefs();
        formatCharStrike();
        formatWordStrike();
        return builder;

    }

    private void formatWordStrike() {
        int removedCharactersDelta = 0;
        Pattern pattern = Pattern.compile("(\\^W)+");
        Matcher matcher = pattern.matcher(builder);
        while (matcher.find()) {
            final int start = matcher.start() - removedCharactersDelta;
            final int end = matcher.end() - removedCharactersDelta;
            CodeBlockSpan[] spans = builder.getSpans(start, start, CodeBlockSpan.class);
            if (spans != null && spans.length != 0) {
                continue;
            }

            int wordsAmount = matcher.group().length() / 2;
            char[] chars = new char[builder.length()];
            builder.getChars(0, builder.length(), chars, 0);
            int runner = start;
            try {
                while (wordsAmount > 0) {
                    if (Character.isSpaceChar(chars[runner--])) {
                        wordsAmount--;
                    }
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                runner = 0;
            }
            builder.setSpan(new StrikethroughSpan(), runner, start, 0);
            builder.delete(start, end);
            removedCharactersDelta = end - start;
        }
    }

    private void formatCharStrike() {
        int removedFormatCharsDelta = 0;
        Pattern pattern = Pattern.compile("(\\^H)+");
        Matcher matcher = pattern.matcher(builder);
        while (matcher.find()) {
            int pos_start = matcher.start() - removedFormatCharsDelta;
            int pos_end = matcher.end() - removedFormatCharsDelta;
            // don't reformat in code blocks
            CodeBlockSpan[] spans = builder.getSpans(pos_start, pos_end, CodeBlockSpan.class);
            if (spans != null && spans.length != 0) {
                continue;
            }

            builder.setSpan(new StrikethroughSpan(), pos_start - (pos_end - pos_start) / 2, pos_start, 0);
            builder.delete(pos_start, pos_end);
            removedFormatCharsDelta += pos_end - pos_start;
        }
    }

    private void embedRefs() {
        Pattern referencePattern = Pattern.compile(">>(/?([a-z]{1,4})/)?(\\d+)");
        Matcher refMatcher = referencePattern.matcher(builder);

        while (refMatcher.find()) {
            int start = refMatcher.start();
            int end = refMatcher.end();

            // don't reformat in code blocks
            CodeBlockSpan[] spans = builder.getSpans(start, end, CodeBlockSpan.class);
            if (spans != null && spans.length != 0) {
                continue;
            }

            String board = refMatcher.group(2);
            board = board == null ? HanabiraBoard.Info.getKeyForId(HanabiraParser.this.post.getBoardId()) : board;
            builder.setSpan(new HanabiraLinkSpan(board, refMatcher.group(3)), start, end, 0);
        }
    }

    private void formatCode() {
        replaceAll("``", "``", SpanObjectFactory.CODE_BLOCK);
        replaceAll("`", "`", SpanObjectFactory.CODE_BLOCK);
        replaceAll("^``\r\n", "\r\n``$", SpanObjectFactory.CODE_BLOCK, Pattern.DOTALL);
    }

    private void paintItalic() {
        replaceAll("\\*", "\\*", SpanObjectFactory.ITALIC);
        replaceAll("_", "_", SpanObjectFactory.ITALIC);
    }

    private void paintBold() {
        replaceAll("\\*\\*", "\\*\\*", SpanObjectFactory.BOLD);
        replaceAll("__", "__", SpanObjectFactory.BOLD);
    }

    private void paintBoldItalic() {
        replaceAll("_\\*\\*", "\\*\\*_", SpanObjectFactory.BOLD_ITALIC);
        replaceAll("__\\*", "\\*__", SpanObjectFactory.BOLD_ITALIC);
    }

    private void formatSpoiler() {
        replaceAll("%%", "%%", SpanObjectFactory.SPOILER);
        replaceAll("%%", "%%", SpanObjectFactory.SPOILER);
        replaceAll("^%%\r\n", "\r\n%%$", SpanObjectFactory.SPOILER, Pattern.DOTALL);
    }

    private void paintQuotes() {
        Pattern pattern = Pattern.compile("^>[^>].*?$", Pattern.MULTILINE);
        Matcher matcher = pattern.matcher(builder);
        while (matcher.find()) {
            int start = matcher.start();
            int end = matcher.end();
            builder.setSpan(new QuoteSpan(), start, end, 0);
        }
    }

    private void formatTwoLevelBulletList() {
        Pattern p = Pattern.compile("^(\\*\\s){1,2}([^\\*\\s].*?)$", Pattern.MULTILINE);
        Matcher m = p.matcher(builder);

        // when happens replacement with such strings as old.length!=new.length, actual size of builder changing and
        // indexing broken for following matching. Since that we need to track that discrepancy
        int delta = 0;
        final int replacementLength = 2; // == " ".length() == "? ".length()

        while (m.find()) {
            int start = m.start() - delta;
            int paraLength = m.group().length();
            boolean single = !m.group().substring(0, m.start(2) - m.start()).matches("\\*\\s\\*\\s");
            String replacement = single ? "? " : " ";
            int level = single ? 1 : 2;
            int oldStringLength = 2 * level; // "* " or "* * "
            int diff = oldStringLength - replacementLength;

            builder.replace(start, start + oldStringLength, replacement);
            builder.setSpan(new BulletListSpan(level), start - delta, start + paraLength - delta - diff, 0);
            delta += diff;
        }
    }

    private void replaceAll(String begin, String end, SpanObjectFactory factory) {
        replaceAll(begin, end, factory, 0);
    }

    @SuppressWarnings("ConstantConditions")
    private void replaceAll(String begin, String end, SpanObjectFactory factory, int flag) {
        int removedFormatCharsDelta = 0;
        Pattern pattern = Pattern.compile(String.format("(%s)(.+?)(%s)", begin, end), Pattern.MULTILINE | flag);
        String beginRestore = restoreBreaks(begin);
        String endRestore = restoreBreaks(end);
        Matcher matcher = pattern.matcher(builder);
        String inlinedString;

        boolean code = begin.contains("`");
        while (matcher.find()) {
            int start = matcher.start(2);
            int finish = matcher.end(2);

            // don't reformat in code blocks
            if (!code) {
                CodeBlockSpan[] spans = builder.getSpans(start, finish, CodeBlockSpan.class);
                if (spans != null && spans.length != 0) {
                    continue;
                }
            }

            // don't reformat double borders while searchin for sinlges
            // e.g.: searching for "*abc*", found "**"
            inlinedString = matcher.group(2);
            if (inlinedString == null || "".equals(inlinedString)) {
                System.out.println(matcher.group());
                continue;
            }

            int lengthPrefix = matcher.group(1).length();
            builder.replace(matcher.start(1) - removedFormatCharsDelta, matcher.end(1) - removedFormatCharsDelta,
                    beginRestore);

            builder.replace(matcher.start(3) - lengthPrefix - removedFormatCharsDelta + beginRestore.length(),
                    matcher.end(3) - lengthPrefix - removedFormatCharsDelta + beginRestore.length(), endRestore);

            SpannableString rep = new SpannableString(matcher.group(2));
            rep.setSpan(factory.getSpan(), 0, rep.length(), 0);
            if (!code) {
                Linkify.addLinks(rep, Linkify.WEB_URLS);
                // fixme twice used Linkify? try remove and just setSpan to builder
            }
            builder.replace(matcher.start() - removedFormatCharsDelta + beginRestore.length(),
                    matcher.start() + rep.length() - removedFormatCharsDelta + endRestore.length(), rep);

            // store deletions
            removedFormatCharsDelta += matcher.group(1).length() - beginRestore.length();
            removedFormatCharsDelta += matcher.group(3).length() - endRestore.length();
        }
    }

    private String restoreBreaks(String border) {
        Pattern newline = Pattern.compile("\\r*\\n*");
        Matcher nlMatchBegin = newline.matcher(border);
        StringBuilder sb = new StringBuilder();
        while (nlMatchBegin.find()) {
            sb.append(nlMatchBegin.group());
        }
        return sb.toString();
    }
}