package ru.noties.markwon.image;

import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

public class AsyncDrawable extends Drawable {

    private final String destination;
    private final AsyncDrawableLoader loader;
    private final ImageSize imageSize;
    private final ImageSizeResolver imageSizeResolver;

    private Drawable result;
    private Callback callback;

    private int canvasWidth;
    private float textSize;

    // @since 2.0.1 for use-cases when image is loaded faster than span is drawn and knows canvas width
    private boolean waitingForDimensions;

    /**
     * @since 1.0.1
     */
    public AsyncDrawable(
            @NonNull String destination,
            @NonNull AsyncDrawableLoader loader,
            @Nullable ImageSizeResolver imageSizeResolver,
            @Nullable ImageSize imageSize
    ) {
        this.destination = destination;
        this.loader = loader;
        this.imageSizeResolver = imageSizeResolver;
        this.imageSize = imageSize;

        final Drawable placeholder = loader.placeholder();
        if (placeholder != null) {
            setPlaceholderResult(placeholder);
        }
    }

    @NonNull
    public String getDestination() {
        return destination;
    }

    public Drawable getResult() {
        return result;
    }

    public boolean hasResult() {
        return result != null;
    }

    public boolean isAttached() {
        return getCallback() != null;
    }

    // yeah
    public void setCallback2(@Nullable Callback callback) {

        this.callback = callback;
        super.setCallback(callback);

        // if not null -> means we are attached
        if (callback != null) {

            // as we have a placeholder now, it's important to check it our placeholder
            // has a proper callback at this point. This is not required in most cases,
            // as placeholder should be static, but if it's not -> it can operate as usual
            if (result != null
                    && result.getCallback() == null) {
                result.setCallback(callback);
            }

            loader.load(destination, this);
        } else {
            if (result != null) {

                result.setCallback(null);

                // let's additionally stop if it Animatable
                if (result instanceof Animatable) {
                    ((Animatable) result).stop();
                }
            }
            loader.cancel(destination);
        }
    }

    /**
     * @since 3.0.1
     */
    protected void setPlaceholderResult(@NonNull Drawable placeholder) {
        // okay, if placeholder has bounds -> use it, otherwise use original imageSize

        final Rect rect = placeholder.getBounds();

        if (rect.isEmpty()) {
            // if bounds are empty -> just use placeholder as a regular result
            DrawableUtils.applyIntrinsicBounds(placeholder);
            setResult(placeholder);
        } else {

            // this condition should not be true for placeholder (at least for now)
            if (result != null) {
                // but it is, unregister current result
                result.setCallback(null);
            }

            // placeholder has bounds specified -> use them until we have real result
            this.result = placeholder;
            this.result.setCallback(callback);

            // use bounds directly
            setBounds(rect);

            // just in case -> so we do not update placeholder when we have canvas dimensions
            waitingForDimensions = false;
        }
    }

    public void setResult(@NonNull Drawable result) {

        // if we have previous one, detach it
        if (this.result != null) {
            this.result.setCallback(null);
        }

        this.result = result;
        this.result.setCallback(callback);

        initBounds();
    }

    /**
     * Remove result from this drawable (for example, in case of cancellation)
     *
     * @since 3.0.1
     */
    public void clearResult() {

        final Drawable result = this.result;

        if (result != null) {
            result.setCallback(null);
            this.result = null;

            // clear bounds
            setBounds(0, 0, 0, 0);
        }
    }

    private void initBounds() {

        if (canvasWidth == 0) {
            // we still have no bounds - wait for them
            waitingForDimensions = true;
            return;
        }

        waitingForDimensions = false;

        final Rect bounds = resolveBounds();
        result.setBounds(bounds);
        setBounds(bounds);

        invalidateSelf();
    }

    /**
     * @since 1.0.1
     */
    @SuppressWarnings("WeakerAccess")
    public void initWithKnownDimensions(int width, float textSize) {
        this.canvasWidth = width;
        this.textSize = textSize;

        if (waitingForDimensions) {
            initBounds();
        }
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        if (hasResult()) {
            result.draw(canvas);
        }
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {

    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        final int opacity;
        if (hasResult()) {
            opacity = result.getOpacity();
        } else {
            opacity = PixelFormat.TRANSPARENT;
        }
        return opacity;
    }

    @Override
    public int getIntrinsicWidth() {
        final int out;
        if (hasResult()) {
            out = result.getIntrinsicWidth();
        } else {
            out = 0;
        }
        return out;
    }

    @Override
    public int getIntrinsicHeight() {
        final int out;
        if (hasResult()) {
            out = result.getIntrinsicHeight();
        } else {
            out = 0;
        }
        return out;
    }

    /**
     * @since 1.0.1
     */
    @NonNull
    private Rect resolveBounds() {

        // @since 2.0.0 previously we were checking if image is greater than canvas width here
        //          but as imageSizeResolver won't be null anymore, we should transfer this logic
        //          there
        return imageSizeResolver != null
                ? imageSizeResolver.resolveImageSize(imageSize, result.getBounds(), canvasWidth, textSize)
                : result.getBounds();
    }
}


package ru.noties.markwon;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spanned;
import android.widget.TextView;

import org.commonmark.node.Node;

import ru.noties.markwon.core.CorePlugin;

/**
 * Class to parse and render markdown. Since version 3.0.0 instance specific (previously consisted
 * of static stateless methods). An instance of builder can be obtained via {@link #builder(Context)}
 * method.
 *
 * @see #create(Context)
 * @see #builder(Context)
 * @see Builder
 */
public abstract class Markwon {

    /**
     * Factory method to create a minimally functional {@link Markwon} instance. This
     * instance will have only {@link CorePlugin} registered. If you wish
     * to configure this instance more consider using {@link #builder(Context)} method.
     *
     * @return {@link Markwon} instance with only CorePlugin registered
     * @since 3.0.0
     */
    @NonNull
    public static Markwon create(@NonNull Context context) {
        return builder(context)
                .usePlugin(CorePlugin.create())
                .build();
    }

    /**
     * Factory method to obtain an instance of {@link Builder}.
     *
     * @see Builder
     * @since 3.0.0
     */
    @NonNull
    public static Builder builder(@NonNull Context context) {
        return new MarkwonBuilderImpl(context);
    }

    /**
     * Method to parse markdown (without rendering)
     *
     * @param input markdown input to parse
     * @return parsed via commonmark-java org.commonmark.node.Node
     * @see #render(Node)
     * @since 3.0.0
     */
    @NonNull
    public abstract Node parse(@NonNull String input);

    /**
     * Create Spanned markdown from parsed Node (via {@link #parse(String)} call).
     *

     * Please note that returned Spanned has few limitations. For example, images, tables
     * and ordered lists require TextView to be properly displayed. This is why images and tables
     * most likely won\'t work in this case. Ordered lists might have mis-measurements. Whenever
     * possible use {@link #setMarkdown(TextView, String)} or {@link #setParsedMarkdown(TextView, Spanned)}
     * as these methods will additionally call specific {@link MarkwonPlugin} methods to prepare
     * proper display.
     *
     * @since 3.0.0
     */
    @NonNull
    public abstract Spanned render(@NonNull Node node);

    /**
     * This method will {@link #parse(String)} and {@link #render(Node)} supplied markdown. Returned
     * Spanned has the same limitations as from {@link #render(Node)} method.
     *
     * @param input markdown input
     * @see #parse(String)
     * @see #render(Node)
     * @since 3.0.0
     */
    @NonNull
    public abstract Spanned toMarkdown(@NonNull String input);

    public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown);

    public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown);

    /**
     * Requests information if certain plugin has been registered. Please note that this
     * method will check for super classes also, so if supplied with {@code markwon.hasPlugin(MarkwonPlugin.class)}
     * this method (if has at least one plugin) will return true. If for example a custom
     * (subclassed) version of a {@link CorePlugin} has been registered and given name
     * {@code CorePlugin2}, then both {@code markwon.hasPlugin(CorePlugin2.class)} and
     * {@code markwon.hasPlugin(CorePlugin.class)} will return true.
     *
     * @param plugin type to query
     * @return true if a plugin is used when configuring this {@link Markwon} instance
     */
    public abstract boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> plugin);

    @Nullable
    public abstract
 P getPlugin(@NonNull Class
 type);

    /**
     * Builder for {@link Markwon}.
     *

     * Please note that the order in which plugins are supplied is important as this order will be
     * used through the whole usage of built Markwon instance
     *
     * @since 3.0.0
     */
    public interface Builder {

        /**
         * Specify bufferType when applying text to a TextView {@code textView.setText(CharSequence,BufferType)}.
         * By default `BufferType.SPANNABLE` is used
         *
         * @param bufferType BufferType
         */
        @NonNull
        Builder bufferType(@NonNull TextView.BufferType bufferType);

        @NonNull
        Builder usePlugin(@NonNull MarkwonPlugin plugin);

        @NonNull
        Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins);

        @NonNull
        Markwon build();
    }
}


package ru.noties.markwon.sample.core;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.Spanned;
import android.widget.TextView;
import android.widget.Toast;

import org.commonmark.node.Node;

import ru.noties.markwon.Markwon;
import ru.noties.markwon.core.CorePlugin;

public class CoreActivity extends Activity {

    private TextView textView;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        textView = new TextView(this);
        setContentView(textView);

        step_1();

        step_2();

        step_3();

        step_4();
    }

    /**
     * Create a simple instance of Markwon with only Core plugin registered
     * this will handle all _natively_ supported by commonmark-java nodes:
     *

     *
StrongEmphasis

     *
Emphasis

     *
BlockQuote

     *
Code

     *
FencedCodeBlock

     *
IndentedCodeBlock

     *
ListItem (bullet-list and ordered list

     *
Heading

     *
Link

     *
ThematicBreak

     *
Paragraph (please note that there is no default span for a paragraph registered)

     *

     *

     * and basic core functionality:
     *

     *
Append text

     *
Insert new lines (soft and hard breaks)

     *

     */
    private void step_1() {

        // short call
        final Markwon markwon = Markwon.create(this);

        // this is the same as calling
        final Markwon markwon2 = Markwon.builder(this)
                .usePlugin(CorePlugin.create())
                .build();
    }

    /**
     * To simply apply raw (non-parsed) markdown call {@link Markwon#setMarkdown(TextView, String)}
     */
    private void step_2() {

        // this is raw markdown
        final String markdown = "Hello **markdown**!";

        final Markwon markwon = Markwon.create(this);

        // this will parse raw markdown and set parsed content to specified TextView
        markwon.setMarkdown(textView, markdown);
    }

    /**
     * To apply markdown in a different context (other than textView) use {@link Markwon#toMarkdown(String)}
     *

     * Please note that some features won't work unless they are used in a TextView context. For example
     * there might be misplaced ordered lists (ordered list must have TextPaint in order to properly measure
     * its number). But also images and tables (they belong to independent modules now). Images and tables
     * are using some work-arounds in order to be displayed in relatively limited context without proper way
     * of invalidation. But if a Toast for example is created with a custom view
     * ({@code new Toast(this).setView(...) }) and has access to a TextView everything should work.
     */
    private void step_3() {

        final String markdown = "*Toast* __here__!\n\n> And a quote!";

        final Markwon markwon = Markwon.create(this);

        final Spanned spanned = markwon.toMarkdown(markdown);

        Toast.makeText(this, spanned, Toast.LENGTH_LONG).show();
    }

    /**
     * To apply already parsed markdown use {@link Markwon#setParsedMarkdown(TextView, Spanned)}
     */
    private void step_4() {

        final String markdown = "This **is** pre-parsed [markdown](#)";

        final Markwon markwon = Markwon.create(this);

        // parse markdown to obtain a Node
        final Node node = markwon.parse(markdown);

        // create a spanned content from parsed node
        final Spanned spanned = markwon.render(node);

        // apply parsed markdown
        markwon.setParsedMarkdown(textView, spanned);
    }
}


package ru.noties.markwon.recycler.table;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorInt;
import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Px;
import android.support.annotation.VisibleForTesting;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;

import org.commonmark.ext.gfm.tables.TableBlock;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ru.noties.markwon.Markwon;
import ru.noties.markwon.ext.tables.Table;
import ru.noties.markwon.recycler.MarkwonAdapter;
import ru.noties.markwon.utils.NoCopySpannableFactory;

/**
 * @since 3.0.0
 */
public class TableEntry extends MarkwonAdapter.Entry<TableBlock, TableEntry.Holder> {

    public interface Builder {

        /**
         * @param tableLayoutResId layout with TableLayout
         * @param tableIdRes       id of the TableLayout inside specified layout
         * @see #tableLayoutIsRoot(int)
         */
        @NonNull
        Builder tableLayout(@LayoutRes int tableLayoutResId, @IdRes int tableIdRes);

        /**
         * @param tableLayoutResId layout with TableLayout as the root view
         * @see #tableLayout(int, int)
         */
        @NonNull
        Builder tableLayoutIsRoot(@LayoutRes int tableLayoutResId);

        /**
         * @param textLayoutResId layout with TextView
         * @param textIdRes       id of the TextView inside specified layout
         * @see #textLayoutIsRoot(int)
         */
        @NonNull
        Builder textLayout(@LayoutRes int textLayoutResId, @IdRes int textIdRes);

        /**
         * @param textLayoutResId layout with TextView as the root view
         * @see #textLayout(int, int)
         */
        @NonNull
        Builder textLayoutIsRoot(@LayoutRes int textLayoutResId);

        /**
         * @param cellTextCenterVertical if text inside a table cell should centered
         *                               vertically (by default `true`)
         */
        @NonNull
        Builder cellTextCenterVertical(boolean cellTextCenterVertical);

        /**
         * @param isRecyclable flag to set on RecyclerView.ViewHolder (by default `true`)
         */
        @NonNull
        Builder isRecyclable(boolean isRecyclable);

        @NonNull
        TableEntry build();
    }

    public interface BuilderConfigure {
        void configure(@NonNull Builder builder);
    }

    @NonNull
    public static Builder builder() {
        return new BuilderImpl();
    }

    @NonNull
    public static TableEntry create(@NonNull BuilderConfigure configure) {
        final Builder builder = builder();
        configure.configure(builder);
        return builder.build();
    }

    private final int tableLayoutResId;
    private final int tableIdRes;

    private final int textLayoutResId;
    private final int textIdRes;

    private final boolean isRecyclable;
    private final boolean cellTextCenterVertical; // by default true

    private LayoutInflater inflater;

    private final Map<TableBlock, Table> map = new HashMap<>(3);

    TableEntry(
            @LayoutRes int tableLayoutResId,
            @IdRes int tableIdRes,
            @LayoutRes int textLayoutResId,
            @IdRes int textIdRes,
            boolean isRecyclable,
            boolean cellTextCenterVertical) {
        this.tableLayoutResId = tableLayoutResId;
        this.tableIdRes = tableIdRes;
        this.textLayoutResId = textLayoutResId;
        this.textIdRes = textIdRes;
        this.isRecyclable = isRecyclable;
        this.cellTextCenterVertical = cellTextCenterVertical;
    }

    @NonNull
    @Override
    public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        return new Holder(
                isRecyclable,
                tableIdRes,
                inflater.inflate(tableLayoutResId, parent, false));
    }

    @Override
    public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull TableBlock node) {

        Table table = map.get(node);
        if (table == null) {
            table = Table.parse(markwon, node);
            map.put(node, table);
        }

        // check if this exact TableBlock was already applied
        // set tag of tableLayoutResId as it's 100% to be present (we still allow 0 as
        // tableIdRes if tableLayoutResId has TableLayout as root view)
        final TableLayout layout = holder.tableLayout;
        if (table == null
                || table == layout.getTag(tableLayoutResId)) {
            return;
        }

        // set this flag to indicate what table instance we current display
        layout.setTag(tableLayoutResId, table);

        final TableEntryPlugin plugin = markwon.getPlugin(TableEntryPlugin.class);
        if (plugin == null) {
            throw new IllegalStateException("No TableEntryPlugin is found. Make sure that it " +
                    "is _used_ whilst configuring Markwon instance");
        }

        // we must remove unwanted ones (rows and columns)

        final TableEntryTheme theme = plugin.theme();
        final int borderWidth;
        final int borderColor;
        final int cellPadding;
        {
            final TextView textView = ensureTextView(layout, 0, 0);
            borderWidth = theme.tableBorderWidth(textView.getPaint());
            borderColor = theme.tableBorderColor(textView.getPaint());
            cellPadding = theme.tableCellPadding();
        }

        ensureTableBorderBackground(layout, borderWidth, borderColor);

        //noinspection SuspiciousNameCombination
//        layout.setPadding(borderWidth, borderWidth, borderWidth, borderWidth);
//        layout.setClipToPadding(borderWidth == 0);

        final List<Table.Row> rows = table.rows();

        final int rowsSize = rows.size();

        // all rows should have equal number of columns
        final int columnsSize = rowsSize > 0
                ? rows.get(0).columns().size()
                : 0;

        Table.Row row;
        Table.Column column;

        TableRow tableRow;

        for (int y = 0; y < rowsSize; y++) {

            row = rows.get(y);
            tableRow = ensureRow(layout, y);

            for (int x = 0; x < columnsSize; x++) {

                column = row.columns().get(x);

                final TextView textView = ensureTextView(layout, y, x);
                textView.setGravity(textGravity(column.alignment(), cellTextCenterVertical));
                textView.getPaint().setFakeBoldText(row.header());

                // apply padding only if not specified in theme (otherwise just use the value from layout)
                if (cellPadding > 0) {
                    textView.setPadding(cellPadding, cellPadding, cellPadding, cellPadding);
                }

                ensureTableBorderBackground(textView, borderWidth, borderColor);
                markwon.setParsedMarkdown(textView, column.content());
            }

            // row appearance
            if (row.header()) {
                tableRow.setBackgroundColor(theme.tableHeaderRowBackgroundColor());
            } else {
                // as we currently have no support for tables without head
                // we shift even/odd calculation a bit (head should not be included in even/odd calculation)
                final boolean isEven = (y % 2) == 1;
                if (isEven) {
                    tableRow.setBackgroundColor(theme.tableEvenRowBackgroundColor());
                } else {
                    // just take first
                    final TextView textView = ensureTextView(layout, y, 0);
                    tableRow.setBackgroundColor(
                            theme.tableOddRowBackgroundColor(textView.getPaint()));
                }
            }
        }

        // clean up here of un-used rows and columns
        removeUnused(layout, rowsSize, columnsSize);
    }

    @NonNull
    private TableRow ensureRow(@NonNull TableLayout layout, int row) {

        final int count = layout.getChildCount();

        // fill the requested views until we have added the `row` one
        if (row >= count) {

            final Context context = layout.getContext();

            int diff = row - count + 1;
            while (diff > 0) {
                layout.addView(new TableRow(context));
                diff -= 1;
            }
        }

        // return requested child (here it always should be the last one)
        return (TableRow) layout.getChildAt(row);
    }

    @NonNull
    private TextView ensureTextView(@NonNull TableLayout layout, int row, int column) {

        final TableRow tableRow = ensureRow(layout, row);
        final int count = tableRow.getChildCount();

        if (column >= count) {

            final LayoutInflater inflater = ensureInflater(layout.getContext());

            boolean textViewChecked = false;

            View view;
            TextView textView;
            ViewGroup.LayoutParams layoutParams;

            int diff = column - count + 1;

            while (diff > 0) {

                view = inflater.inflate(textLayoutResId, tableRow, false);

                // we should have `match_parent` as height (important for borders and text-vertical-align)
                layoutParams = view.getLayoutParams();
                if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
                    layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
                }

                // it will be enough to check only once
                if (!textViewChecked) {

                    if (textIdRes == 0) {
                        if (!(view instanceof TextView)) {
                            final String name = layout.getContext().getResources().getResourceName(textLayoutResId);
                            throw new IllegalStateException(String.format("textLayoutResId(R.layout.%s) " +
                                    "has other than TextView root view. Specify TextView ID explicitly", name));
                        }
                        textView = (TextView) view;
                    } else {
                        textView = view.findViewById(textIdRes);
                        if (textView == null) {
                            final Resources r = layout.getContext().getResources();
                            final String layoutName = r.getResourceName(textLayoutResId);
                            final String idName = r.getResourceName(textIdRes);
                            throw new NullPointerException(String.format("textLayoutResId(R.layout.%s) " +
                                    "has no TextView found by id(R.id.%s): %s", layoutName, idName, view));
                        }
                    }
                    // mark as checked
                    textViewChecked = true;
                } else {
                    if (textIdRes == 0) {
                        textView = (TextView) view;
                    } else {
                        textView = view.findViewById(textIdRes);
                    }
                }

                // we should set SpannableFactory during creation (to avoid another setText method)
                textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
                tableRow.addView(textView);

                diff -= 1;
            }
        }

        // we can skip all the validation here as we have validated our views whilst inflating them
        final View last = tableRow.getChildAt(column);
        if (textIdRes == 0) {
            return (TextView) last;
        } else {
            return last.findViewById(textIdRes);
        }
    }

    private void ensureTableBorderBackground(@NonNull View view, @Px int borderWidth, @ColorInt int borderColor) {
        if (borderWidth == 0) {
            view.setBackground(null);
        } else {
            final Drawable drawable = view.getBackground();
            if (!(drawable instanceof TableBorderDrawable)) {
                final TableBorderDrawable borderDrawable = new TableBorderDrawable();
                borderDrawable.update(borderWidth, borderColor);
                view.setBackground(borderDrawable);
            } else {
                ((TableBorderDrawable) drawable).update(borderWidth, borderColor);
            }
        }
    }

    @NonNull
    private LayoutInflater ensureInflater(@NonNull Context context) {
        if (inflater == null) {
            inflater = LayoutInflater.from(context);
        }
        return inflater;
    }

    @SuppressWarnings("WeakerAccess")
    @VisibleForTesting
    static void removeUnused(@NonNull TableLayout layout, int usedRows, int usedColumns) {

        // clean up rows
        final int rowsCount = layout.getChildCount();
        if (rowsCount > usedRows) {
            layout.removeViews(usedRows, (rowsCount - usedRows));
        }

        // validate columns
        // here we can use usedRows as children count

        TableRow tableRow;
        int columnCount;

        for (int i = 0; i < usedRows; i++) {
            tableRow = (TableRow) layout.getChildAt(i);
            columnCount = tableRow.getChildCount();
            if (columnCount > usedColumns) {
                tableRow.removeViews(usedColumns, (columnCount - usedColumns));
            }
        }
    }

    @Override
    public void clear() {
        map.clear();
    }

    public static class Holder extends MarkwonAdapter.Holder {

        final TableLayout tableLayout;

        public Holder(boolean isRecyclable, @IdRes int tableLayoutIdRes, @NonNull View itemView) {
            super(itemView);

            // we must call this method only once (it's somehow _paired_ inside, so
            // any call in `onCreateViewHolder` or `onBindViewHolder` will log an error
            // `isRecyclable decremented below 0` which make little sense here)
            setIsRecyclable(isRecyclable);

            final TableLayout tableLayout;
            if (tableLayoutIdRes == 0) {
                // try to cast directly
                if (!(itemView instanceof TableLayout)) {
                    throw new IllegalStateException("Root view is not TableLayout. Please provide " +
                            "TableLayout ID explicitly");
                }
                tableLayout = (TableLayout) itemView;
            } else {
                tableLayout = requireView(tableLayoutIdRes);
            }
            this.tableLayout = tableLayout;
        }
    }

    // we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17)
    @SuppressWarnings("WeakerAccess")
    @SuppressLint("RtlHardcoded")
    @VisibleForTesting
    static int textGravity(@NonNull Table.Alignment alignment, boolean cellTextCenterVertical) {

        final int gravity;

        switch (alignment) {

            case LEFT:
                gravity = Gravity.LEFT;
                break;

            case CENTER:
                gravity = Gravity.CENTER_HORIZONTAL;
                break;

            case RIGHT:
                gravity = Gravity.RIGHT;
                break;

            default:
                throw new IllegalStateException("Unknown table alignment: " + alignment);
        }

        if (cellTextCenterVertical) {
            return gravity | Gravity.CENTER_VERTICAL;
        }

        // do not center vertically
        return gravity;
    }

    static class BuilderImpl implements Builder {

        private int tableLayoutResId;
        private int tableIdRes;

        private int textLayoutResId;
        private int textIdRes;

        private boolean cellTextCenterVertical = true;

        private boolean isRecyclable = true;

        @NonNull
        @Override
        public Builder tableLayout(int tableLayoutResId, int tableIdRes) {
            this.tableLayoutResId = tableLayoutResId;
            this.tableIdRes = tableIdRes;
            return this;
        }

        @NonNull
        @Override
        public Builder tableLayoutIsRoot(int tableLayoutResId) {
            this.tableLayoutResId = tableLayoutResId;
            this.tableIdRes = 0;
            return this;
        }

        @NonNull
        @Override
        public Builder textLayout(int textLayoutResId, int textIdRes) {
            this.textLayoutResId = textLayoutResId;
            this.textIdRes = textIdRes;
            return this;
        }

        @NonNull
        @Override
        public Builder textLayoutIsRoot(int textLayoutResId) {
            this.textLayoutResId = textLayoutResId;
            this.textIdRes = 0;
            return this;
        }

        @NonNull
        @Override
        public Builder cellTextCenterVertical(boolean cellTextCenterVertical) {
            this.cellTextCenterVertical = cellTextCenterVertical;
            return this;
        }

        @NonNull
        @Override
        public Builder isRecyclable(boolean isRecyclable) {
            this.isRecyclable = isRecyclable;
            return this;
        }

        @NonNull
        @Override
        public TableEntry build() {

            if (tableLayoutResId == 0) {
                throw new IllegalStateException("`tableLayoutResId` argument is required");
            }

            if (textLayoutResId == 0) {
                throw new IllegalStateException("`textLayoutResId` argument is required");
            }

            return new TableEntry(
                    tableLayoutResId, tableIdRes,
                    textLayoutResId, textIdRes,
                    isRecyclable, cellTextCenterVertical
            );
        }
    }
}


package ru.noties.markwon.core;

import android.content.Context;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Px;
import android.support.annotation.Size;
import android.text.TextPaint;

import java.util.Arrays;
import java.util.Locale;

import ru.noties.markwon.utils.ColorUtils;
import ru.noties.markwon.utils.Dip;

/**
 * Class to hold theming information for rending of markdown.
 *

 * Since version 3.0.0 this class should be considered as CoreTheme as it\'s
 * information holds data for core features only. But based on this other components can still use it
 * to display markdown consistently.
 *

 * Since version 3.0.0 this class should not be instantiated manually. Instead a {@link ru.noties.markwon.MarkwonPlugin}
 * should be used: {@link ru.noties.markwon.MarkwonPlugin#configureTheme(Builder)}
 *

 * Since version 3.0.0 properties related to strike-through, tables and HTML
 * are moved to specific plugins in independent artifacts
 *
 * @see CorePlugin
 * @see ru.noties.markwon.MarkwonPlugin#configureTheme(Builder)
 */
@SuppressWarnings("WeakerAccess")
public class MarkwonTheme {

    /**
     * Factory method to obtain an instance of {@link MarkwonTheme} with all values as defaults
     *
     * @param context Context in order to resolve defaults
     * @return {@link MarkwonTheme} instance
     * @see #builderWithDefaults(Context)
     * @since 1.0.0
     */
    @NonNull
    public static MarkwonTheme create(@NonNull Context context) {
        return builderWithDefaults(context).build();
    }

    /**
     * Create an empty instance of {@link Builder} with no default values applied
     *

     * Since version 3.0.0 manual construction of {@link MarkwonTheme} is not required, instead a
     * {@link ru.noties.markwon.MarkwonPlugin#configureTheme(Builder)} should be used in order
     * to change certain theme properties
     *
     * @since 3.0.0
     */
    @SuppressWarnings("unused")
    @NonNull
    public static Builder emptyBuilder() {
        return new Builder();
    }

    /**
     * Factory method to create a {@link Builder} instance and initialize it with values
     * from supplied {@link MarkwonTheme}
     *
     * @param copyFrom {@link MarkwonTheme} to copy values from
     * @return {@link Builder} instance
     * @see #builderWithDefaults(Context)
     * @since 1.0.0
     */
    @NonNull
    public static Builder builder(@NonNull MarkwonTheme copyFrom) {
        return new Builder(copyFrom);
    }

    /**
     * Factory method to obtain a {@link Builder} instance initialized with default values taken
     * from current application theme.
     *
     * @param context Context to obtain default styling values (colors, etc)
     * @return {@link Builder} instance
     * @since 1.0.0
     */
    @NonNull
    public static Builder builderWithDefaults(@NonNull Context context) {

        final Dip dip = Dip.create(context);
        return new Builder()
                .codeBlockMargin(dip.toPx(8))
                .blockMargin(dip.toPx(24))
                .blockQuoteWidth(dip.toPx(4))
                .bulletListItemStrokeWidth(dip.toPx(1))
                .headingBreakHeight(dip.toPx(1))
                .thematicBreakHeight(dip.toPx(4));
    }

    protected static final int BLOCK_QUOTE_DEF_COLOR_ALPHA = 25;

    protected static final int CODE_DEF_BACKGROUND_COLOR_ALPHA = 25;
    protected static final float CODE_DEF_TEXT_SIZE_RATIO = .87F;

    protected static final int HEADING_DEF_BREAK_COLOR_ALPHA = 75;

    // taken from html spec (most browsers render headings like that)
    // is not exposed via protected modifier in order to disallow modification
    private static final float[] HEADING_SIZES = {
            2.F, 1.5F, 1.17F, 1.F, .83F, .67F,
    };

    protected static final int THEMATIC_BREAK_DEF_ALPHA = 25;

    protected final int linkColor;

    // used in quote, lists
    protected final int blockMargin;

    // by default it's 1/4th of `blockMargin`
    protected final int blockQuoteWidth;

    // by default it's text color with `BLOCK_QUOTE_DEF_COLOR_ALPHA` applied alpha
    protected final int blockQuoteColor;

    // by default uses text color (applied for un-ordered lists & ordered (bullets & numbers)
    protected final int listItemColor;

    // by default the stroke color of a paint object
    protected final int bulletListItemStrokeWidth;

    // width of bullet, by default min(blockMargin, height) / 2
    protected final int bulletWidth;

    // by default - main text color
    protected final int codeTextColor;

    // by default - codeTextColor
    protected final int codeBlockTextColor;

    // by default 0.1 alpha of textColor/codeTextColor
    protected final int codeBackgroundColor;

    // by default codeBackgroundColor
    protected final int codeBlockBackgroundColor;

    // by default `width` of a space char... it's fun and games, but span doesn't have access to paint in `getLeadingMargin`
    // so, we need to set this value explicitly (think of an utility method, that takes TextView/TextPaint and measures space char)
    protected final int codeBlockMargin;

    // by default Typeface.MONOSPACE
    protected final Typeface codeTypeface;

    protected final Typeface codeBlockTypeface;

    // by default a bit (how much?!) smaller than normal text
    // applied ONLY if default typeface was used, otherwise, not applied
    protected final int codeTextSize;

    protected final int codeBlockTextSize;

    // by default paint.getStrokeWidth
    protected final int headingBreakHeight;

    // by default, text color with `HEADING_DEF_BREAK_COLOR_ALPHA` applied alpha
    protected final int headingBreakColor;

    // by default, whatever typeface is set on the TextView
    // @since 1.1.0
    protected final Typeface headingTypeface;

    // by default, we use standard multipliers from the HTML spec (see HEADING_SIZES for values).
    // this library supports 6 heading sizes, so make sure the array you pass here has 6 elements.
    // @since 1.1.0
    protected final float[] headingTextSizeMultipliers;

    // by default textColor with `THEMATIC_BREAK_DEF_ALPHA` applied alpha
    protected final int thematicBreakColor;

    // by default paint.strokeWidth
    protected final int thematicBreakHeight;

    protected MarkwonTheme(@NonNull Builder builder) {
        this.linkColor = builder.linkColor;
        this.blockMargin = builder.blockMargin;
        this.blockQuoteWidth = builder.blockQuoteWidth;
        this.blockQuoteColor = builder.blockQuoteColor;
        this.listItemColor = builder.listItemColor;
        this.bulletListItemStrokeWidth = builder.bulletListItemStrokeWidth;
        this.bulletWidth = builder.bulletWidth;
        this.codeTextColor = builder.codeTextColor;
        this.codeBlockTextColor = builder.codeBlockTextColor;
        this.codeBackgroundColor = builder.codeBackgroundColor;
        this.codeBlockBackgroundColor = builder.codeBlockBackgroundColor;
        this.codeBlockMargin = builder.codeBlockMargin;
        this.codeTypeface = builder.codeTypeface;
        this.codeBlockTypeface = builder.codeBlockTypeface;
        this.codeTextSize = builder.codeTextSize;
        this.codeBlockTextSize = builder.codeBlockTextSize;
        this.headingBreakHeight = builder.headingBreakHeight;
        this.headingBreakColor = builder.headingBreakColor;
        this.headingTypeface = builder.headingTypeface;
        this.headingTextSizeMultipliers = builder.headingTextSizeMultipliers;
        this.thematicBreakColor = builder.thematicBreakColor;
        this.thematicBreakHeight = builder.thematicBreakHeight;
    }

    /**
     * @since 1.0.5
     */
    public void applyLinkStyle(@NonNull TextPaint paint) {
        paint.setUnderlineText(true);
        if (linkColor != 0) {
            paint.setColor(linkColor);
        } else {
            // if linkColor is not specified during configuration -> use default one
            paint.setColor(paint.linkColor);
        }
    }

    public void applyLinkStyle(@NonNull Paint paint) {
        paint.setUnderlineText(true);
        if (linkColor != 0) {
            // by default we will be using text color
            paint.setColor(linkColor);
        } else {
            // @since 1.0.5, if link color is specified during configuration, _try_ to use the
            // default one (if provided paint is an instance of TextPaint)
            if (paint instanceof TextPaint) {
                paint.setColor(((TextPaint) paint).linkColor);
            }
        }
    }

    public void applyBlockQuoteStyle(@NonNull Paint paint) {
        final int color;
        if (blockQuoteColor == 0) {
            color = ColorUtils.applyAlpha(paint.getColor(), BLOCK_QUOTE_DEF_COLOR_ALPHA);
        } else {
            color = blockQuoteColor;
        }
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(color);
    }

    public int getBlockMargin() {
        return blockMargin;
    }

    public int getBlockQuoteWidth() {
        final int out;
        if (blockQuoteWidth == 0) {
            out = (int) (blockMargin * .25F + .5F);
        } else {
            out = blockQuoteWidth;
        }
        return out;
    }

    public void applyListItemStyle(@NonNull Paint paint) {

        final int color;
        if (listItemColor != 0) {
            color = listItemColor;
        } else {
            color = paint.getColor();
        }
        paint.setColor(color);

        if (bulletListItemStrokeWidth != 0) {
            paint.setStrokeWidth(bulletListItemStrokeWidth);
        }
    }

    public int getBulletWidth(int height) {

        final int min = Math.min(blockMargin, height) / 2;

        final int width;
        if (bulletWidth == 0
                || bulletWidth > min) {
            width = min;
        } else {
            width = bulletWidth;
        }

        return width;
    }

    /**
     * @since 3.0.0
     */
    public void applyCodeTextStyle(@NonNull Paint paint) {

        if (codeTextColor != 0) {
            paint.setColor(codeTextColor);
        }

        if (codeTypeface != null) {

            paint.setTypeface(codeTypeface);

            if (codeTextSize > 0) {
                paint.setTextSize(codeTextSize);
            }

        } else {

            paint.setTypeface(Typeface.MONOSPACE);

            if (codeTextSize > 0) {
                paint.setTextSize(codeTextSize);
            } else {
                paint.setTextSize(paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO);
            }
        }
    }

    /**
     * @since 3.0.0
     */
    public void applyCodeBlockTextStyle(@NonNull Paint paint) {

        // apply text color, first check for block specific value,
        // then check for code (inline), else do nothing (keep original color of text)
        final int textColor = codeBlockTextColor != 0
                ? codeBlockTextColor
                : codeTextColor;

        if (textColor != 0) {
            paint.setColor(textColor);
        }

        final Typeface typeface = codeBlockTypeface != null
                ? codeBlockTypeface
                : codeTypeface;

        if (typeface != null) {

            paint.setTypeface(typeface);

            // please note that we won't be calculating textSize
            // (like we do when no Typeface is provided), if it's some specific typeface
            // we would confuse users about textSize
            final int textSize = codeBlockTextSize > 0
                    ? codeBlockTextSize
                    : codeTextSize;

            if (textSize > 0) {
                paint.setTextSize(textSize);
            }
        } else {

            // by default use monospace
            paint.setTypeface(Typeface.MONOSPACE);

            final int textSize = codeBlockTextSize > 0
                    ? codeBlockTextSize
                    : codeTextSize;

            if (textSize > 0) {
                paint.setTextSize(textSize);
            } else {
                // calculate default value
                paint.setTextSize(paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO);
            }
        }
    }

    public int getCodeBlockMargin() {
        return codeBlockMargin;
    }

    /**
     * @since 3.0.0
     */
    public int getCodeBackgroundColor(@NonNull Paint paint) {
        final int color;
        if (codeBackgroundColor != 0) {
            color = codeBackgroundColor;
        } else {
            color = ColorUtils.applyAlpha(paint.getColor(), CODE_DEF_BACKGROUND_COLOR_ALPHA);
        }
        return color;
    }

    /**
     * @since 3.0.0
     */
    public int getCodeBlockBackgroundColor(@NonNull Paint paint) {

        final int color = codeBlockBackgroundColor != 0
                ? codeBlockBackgroundColor
                : codeBackgroundColor;

        return color != 0
                ? color
                : ColorUtils.applyAlpha(paint.getColor(), CODE_DEF_BACKGROUND_COLOR_ALPHA);
    }

    public void applyHeadingTextStyle(@NonNull Paint paint, @IntRange(from = 1, to = 6) int level) {
        if (headingTypeface == null) {
            paint.setFakeBoldText(true);
        } else {
            paint.setTypeface(headingTypeface);
        }
        final float[] textSizes = headingTextSizeMultipliers != null
                ? headingTextSizeMultipliers
                : HEADING_SIZES;

        if (textSizes != null && textSizes.length >= level) {
            paint.setTextSize(paint.getTextSize() * textSizes[level - 1]);
        } else {
            throw new IllegalStateException(String.format(
                    Locale.US,
                    "Supplied heading level: %d is invalid, where configured heading sizes are: `%s`",
                    level, Arrays.toString(textSizes)));
        }
    }

    public void applyHeadingBreakStyle(@NonNull Paint paint) {
        final int color;
        if (headingBreakColor != 0) {
            color = headingBreakColor;
        } else {
            color = ColorUtils.applyAlpha(paint.getColor(), HEADING_DEF_BREAK_COLOR_ALPHA);
        }
        paint.setColor(color);
        paint.setStyle(Paint.Style.FILL);
        if (headingBreakHeight >= 0) {
            //noinspection SuspiciousNameCombination
            paint.setStrokeWidth(headingBreakHeight);
        }
    }

    public void applyThematicBreakStyle(@NonNull Paint paint) {
        final int color;
        if (thematicBreakColor != 0) {
            color = thematicBreakColor;
        } else {
            color = ColorUtils.applyAlpha(paint.getColor(), THEMATIC_BREAK_DEF_ALPHA);
        }
        paint.setColor(color);
        paint.setStyle(Paint.Style.FILL);

        if (thematicBreakHeight >= 0) {
            //noinspection SuspiciousNameCombination
            paint.setStrokeWidth(thematicBreakHeight);
        }
    }

    @SuppressWarnings("unused")
    public static class Builder {

        private int linkColor;
        private int blockMargin;
        private int blockQuoteWidth;
        private int blockQuoteColor;
        private int listItemColor;
        private int bulletListItemStrokeWidth;
        private int bulletWidth;
        private int codeTextColor;
        private int codeBlockTextColor; // @since 1.0.5
        private int codeBackgroundColor;
        private int codeBlockBackgroundColor; // @since 1.0.5
        private int codeBlockMargin;
        private Typeface codeTypeface;
        private Typeface codeBlockTypeface; // @since 3.0.0
        private int codeTextSize;
        private int codeBlockTextSize; // @since 3.0.0
        private int headingBreakHeight = -1;
        private int headingBreakColor;
        private Typeface headingTypeface;
        private float[] headingTextSizeMultipliers;
        private int thematicBreakColor;
        private int thematicBreakHeight = -1;

        Builder() {
        }

        Builder(@NonNull MarkwonTheme theme) {
            this.linkColor = theme.linkColor;
            this.blockMargin = theme.blockMargin;
            this.blockQuoteWidth = theme.blockQuoteWidth;
            this.blockQuoteColor = theme.blockQuoteColor;
            this.listItemColor = theme.listItemColor;
            this.bulletListItemStrokeWidth = theme.bulletListItemStrokeWidth;
            this.bulletWidth = theme.bulletWidth;
            this.codeTextColor = theme.codeTextColor;
            this.codeBlockTextColor = theme.codeBlockTextColor;
            this.codeBackgroundColor = theme.codeBackgroundColor;
            this.codeBlockBackgroundColor = theme.codeBlockBackgroundColor;
            this.codeBlockMargin = theme.codeBlockMargin;
            this.codeTypeface = theme.codeTypeface;
            this.codeTextSize = theme.codeTextSize;
            this.headingBreakHeight = theme.headingBreakHeight;
            this.headingBreakColor = theme.headingBreakColor;
            this.headingTypeface = theme.headingTypeface;
            this.headingTextSizeMultipliers = theme.headingTextSizeMultipliers;
            this.thematicBreakColor = theme.thematicBreakColor;
            this.thematicBreakHeight = theme.thematicBreakHeight;
        }

        @NonNull
        public Builder linkColor(@ColorInt int linkColor) {
            this.linkColor = linkColor;
            return this;
        }

        @NonNull
        public Builder blockMargin(@Px int blockMargin) {
            this.blockMargin = blockMargin;
            return this;
        }

        @NonNull
        public Builder blockQuoteWidth(@Px int blockQuoteWidth) {
            this.blockQuoteWidth = blockQuoteWidth;
            return this;
        }

        @SuppressWarnings("SameParameterValue")
        @NonNull
        public Builder blockQuoteColor(@ColorInt int blockQuoteColor) {
            this.blockQuoteColor = blockQuoteColor;
            return this;
        }

        @NonNull
        public Builder listItemColor(@ColorInt int listItemColor) {
            this.listItemColor = listItemColor;
            return this;
        }

        @NonNull
        public Builder bulletListItemStrokeWidth(@Px int bulletListItemStrokeWidth) {
            this.bulletListItemStrokeWidth = bulletListItemStrokeWidth;
            return this;
        }

        @NonNull
        public Builder bulletWidth(@Px int bulletWidth) {
            this.bulletWidth = bulletWidth;
            return this;
        }

        @NonNull
        public Builder codeTextColor(@ColorInt int codeTextColor) {
            this.codeTextColor = codeTextColor;
            return this;
        }

        /**
         * @since 1.0.5
         */
        @NonNull
        public Builder codeBlockTextColor(@ColorInt int codeBlockTextColor) {
            this.codeBlockTextColor = codeBlockTextColor;
            return this;
        }

        @SuppressWarnings({"SameParameterValue", "UnusedReturnValue"})
        @NonNull
        public Builder codeBackgroundColor(@ColorInt int codeBackgroundColor) {
            this.codeBackgroundColor = codeBackgroundColor;
            return this;
        }

        /**
         * @since 1.0.5
         */
        @NonNull
        public Builder codeBlockBackgroundColor(@ColorInt int codeBlockBackgroundColor) {
            this.codeBlockBackgroundColor = codeBlockBackgroundColor;
            return this;
        }

        @NonNull
        public Builder codeBlockMargin(@Px int codeBlockMargin) {
            this.codeBlockMargin = codeBlockMargin;
            return this;
        }

        @NonNull
        public Builder codeTypeface(@NonNull Typeface codeTypeface) {
            this.codeTypeface = codeTypeface;
            return this;
        }

        /**
         * @since 3.0.0
         */
        @NonNull
        public Builder codeBlockTypeface(@NonNull Typeface typeface) {
            this.codeBlockTypeface = typeface;
            return this;
        }

        @NonNull
        public Builder codeTextSize(@Px int codeTextSize) {
            this.codeTextSize = codeTextSize;
            return this;
        }

        /**
         * @since 3.0.0
         */
        @NonNull
        public Builder codeBlockTextSize(@Px int codeTextSize) {
            this.codeBlockTextSize = codeTextSize;
            return this;
        }

        @NonNull
        public Builder headingBreakHeight(@Px int headingBreakHeight) {
            this.headingBreakHeight = headingBreakHeight;
            return this;
        }

        @NonNull
        public Builder headingBreakColor(@ColorInt int headingBreakColor) {
            this.headingBreakColor = headingBreakColor;
            return this;
        }

        /**
         * @param headingTypeface Typeface to use for heading elements
         * @return self
         * @since 1.1.0
         */
        @NonNull
        public Builder headingTypeface(@NonNull Typeface headingTypeface) {
            this.headingTypeface = headingTypeface;
            return this;
        }

        /**
         * @param headingTextSizeMultipliers an array of multipliers values for heading elements.
         *                                   The base value for this multipliers is TextView\'s text size
         * @return self
         * @since 1.1.0
         */
        @SuppressWarnings("UnusedReturnValue")
        @NonNull
        public Builder headingTextSizeMultipliers(@Size(6) @NonNull float[] headingTextSizeMultipliers) {
            this.headingTextSizeMultipliers = headingTextSizeMultipliers;
            return this;
        }

        @NonNull
        public Builder thematicBreakColor(@ColorInt int thematicBreakColor) {
            this.thematicBreakColor = thematicBreakColor;
            return this;
        }

        @NonNull
        public Builder thematicBreakHeight(@Px int thematicBreakHeight) {
            this.thematicBreakHeight = thematicBreakHeight;
            return this;
        }

        @NonNull
        public MarkwonTheme build() {
            return new MarkwonTheme(this);
        }
    }

}


package ru.noties.markwon.html.jsoup.parser;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Locale;

import ru.noties.markwon.html.jsoup.UncheckedIOException;
import ru.noties.markwon.html.jsoup.helper.Validate;

/**
 * CharacterReader consumes tokens off a string. Used internally by jsoup. API subject to changes.
 */
public final class CharacterReader {
    static final char EOF = (char) -1;
    private static final int maxStringCacheLen = 12;
    static final int maxBufferLen = 1024 * 4; // visible for testing
    private static final int readAheadLimit = (int) (maxBufferLen * 0.75);

    private final char[] charBuf;
    private final Reader reader;
    private int bufLength;
    private int bufSplitPoint;
    private int bufPos;
    private int readerPos;
    private int bufMark;
    private final String[] stringCache = new String[128]; // holds reused strings in this doc, to lessen garbage

    public CharacterReader(Reader input, int sz) {
        Validate.notNull(input);
        Validate.isTrue(input.markSupported());
        reader = input;
        charBuf = new char[maxBufferLen];
        bufferUp();
    }

    public CharacterReader(Reader input) {
        this(input, maxBufferLen);
    }

    public CharacterReader(String input) {
        this(new StringReader(input), input.length());
    }

//    public void swapInput(@NonNull String input) {
//        reader = new StringReader(input);
//        bufLength = 0;
//        bufSplitPoint = 0;
//        bufPos = 0;
//        readerPos = 0;
//        bufferUp();
//    }

    private void bufferUp() {
        if (bufPos < bufSplitPoint)
            return;

        try {
            reader.skip(bufPos);
            reader.mark(maxBufferLen);
            final int read = reader.read(charBuf);
            reader.reset();
            if (read != -1) {
                bufLength = read;
                readerPos += bufPos;
                bufPos = 0;
                bufMark = 0;
                bufSplitPoint = bufLength > readAheadLimit ? readAheadLimit : bufLength;
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Gets the current cursor position in the content.
     *
     * @return current position
     */
    public int pos() {
        return readerPos + bufPos;
    }

    /**
     * Tests if all the content has been read.
     *
     * @return true if nothing left to read.
     */
    public boolean isEmpty() {
        bufferUp();
        return bufPos >= bufLength;
    }

    private boolean isEmptyNoBufferUp() {
        return bufPos >= bufLength;
    }

    /**
     * Get the char at the current position.
     *
     * @return char
     */
    public char current() {
        bufferUp();
        return isEmptyNoBufferUp() ? EOF : charBuf[bufPos];
    }

    char consume() {
        bufferUp();
        char val = isEmptyNoBufferUp() ? EOF : charBuf[bufPos];
        bufPos++;
        return val;
    }

    void unconsume() {
        bufPos--;
    }

    /**
     * Moves the current position by one.
     */
    public void advance() {
        bufPos++;
    }

    void mark() {
        bufMark = bufPos;
    }

    void rewindToMark() {
        bufPos = bufMark;
    }

    /**
     * Returns the number of characters between the current position and the next instance of the input char
     *
     * @param c scan target
     * @return offset between current position and next instance of target. -1 if not found.
     */
    int nextIndexOf(char c) {
        // doesn't handle scanning for surrogates
        bufferUp();
        for (int i = bufPos; i < bufLength; i++) {
            if (c == charBuf[i])
                return i - bufPos;
        }
        return -1;
    }

    /**
     * Returns the number of characters between the current position and the next instance of the input sequence
     *
     * @param seq scan target
     * @return offset between current position and next instance of target. -1 if not found.
     */
    int nextIndexOf(CharSequence seq) {
        bufferUp();
        // doesn't handle scanning for surrogates
        char startChar = seq.charAt(0);
        for (int offset = bufPos; offset < bufLength; offset++) {
            // scan to first instance of startchar:
            if (startChar != charBuf[offset])
                while (++offset < bufLength && startChar != charBuf[offset]) { /* empty */ }
            int i = offset + 1;
            int last = i + seq.length() - 1;
            if (offset < bufLength && last <= bufLength) {
                for (int j = 1; i < last && seq.charAt(j) == charBuf[i]; i++, j++) { /* empty */ }
                if (i == last) // found full sequence
                    return offset - bufPos;
            }
        }
        return -1;
    }

    /**
     * Reads characters up to the specific char.
     *
     * @param c the delimiter
     * @return the chars read
     */
    public String consumeTo(char c) {
        int offset = nextIndexOf(c);
        if (offset != -1) {
            String consumed = cacheString(charBuf, stringCache, bufPos, offset);
            bufPos += offset;
            return consumed;
        } else {
            return consumeToEnd();
        }
    }

    String consumeTo(String seq) {
        int offset = nextIndexOf(seq);
        if (offset != -1) {
            String consumed = cacheString(charBuf, stringCache, bufPos, offset);
            bufPos += offset;
            return consumed;
        } else {
            return consumeToEnd();
        }
    }

    /**
     * Read characters until the first of any delimiters is found.
     *
     * @param chars delimiters to scan for
     * @return characters read up to the matched delimiter.
     */
    public String consumeToAny(final char... chars) {
        bufferUp();
        final int start = bufPos;
        final int remaining = bufLength;
        final char[] val = charBuf;

        OUTER:
        while (bufPos < remaining) {
            for (char c : chars) {
                if (val[bufPos] == c)
                    break OUTER;
            }
            bufPos++;
        }

        return bufPos > start ? cacheString(charBuf, stringCache, start, bufPos - start) : "";
    }

    String consumeToAnySorted(final char... chars) {
        bufferUp();
        final int start = bufPos;
        final int remaining = bufLength;
        final char[] val = charBuf;

        while (bufPos < remaining) {
            if (Arrays.binarySearch(chars, val[bufPos]) >= 0)
                break;
            bufPos++;
        }

        return bufPos > start ? cacheString(charBuf, stringCache, start, bufPos - start) : "";
    }

    String consumeData() {
        // &, <, null
        bufferUp();
        final int start = bufPos;
        final int remaining = bufLength;
        final char[] val = charBuf;

        while (bufPos < remaining) {
            final char c = val[bufPos];
            if (c == '&' || c == '<' || c == TokeniserState.nullChar)
                break;
            bufPos++;
        }

        return bufPos > start ? cacheString(charBuf, stringCache, start, bufPos - start) : "";
    }

    String consumeTagName() {
        // '\t', '\n', '\r', '\f', ' ', '/', '>', nullChar
        bufferUp();
        final int start = bufPos;
        final int remaining = bufLength;
        final char[] val = charBuf;

        while (bufPos < remaining) {
            final char c = val[bufPos];
            if (c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == ' ' || c == '/' || c == '>' || c == TokeniserState.nullChar)
                break;
            bufPos++;
        }

        return bufPos > start ? cacheString(charBuf, stringCache, start, bufPos - start) : "";
    }

    String consumeToEnd() {
        bufferUp();
        String data = cacheString(charBuf, stringCache, bufPos, bufLength - bufPos);
        bufPos = bufLength;
        return data;
    }

    String consumeLetterSequence() {
        bufferUp();
        int start = bufPos;
        while (bufPos < bufLength) {
            char c = charBuf[bufPos];
            if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || Character.isLetter(c))
                bufPos++;
            else
                break;
        }

        return cacheString(charBuf, stringCache, start, bufPos - start);
    }

    String consumeLetterThenDigitSequence() {
        bufferUp();
        int start = bufPos;
        while (bufPos < bufLength) {
            char c = charBuf[bufPos];
            if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || Character.isLetter(c))
                bufPos++;
            else
                break;
        }
        while (!isEmptyNoBufferUp()) {
            char c = charBuf[bufPos];
            if (c >= '0' && c <= '9')
                bufPos++;
            else
                break;
        }

        return cacheString(charBuf, stringCache, start, bufPos - start);
    }

    String consumeHexSequence() {
        bufferUp();
        int start = bufPos;
        while (bufPos < bufLength) {
            char c = charBuf[bufPos];
            if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
                bufPos++;
            else
                break;
        }
        return cacheString(charBuf, stringCache, start, bufPos - start);
    }

    String consumeDigitSequence() {
        bufferUp();
        int start = bufPos;
        while (bufPos < bufLength) {
            char c = charBuf[bufPos];
            if (c >= '0' && c <= '9')
                bufPos++;
            else
                break;
        }
        return cacheString(charBuf, stringCache, start, bufPos - start);
    }

    boolean matches(char c) {
        return !isEmpty() && charBuf[bufPos] == c;

    }

    boolean matches(String seq) {
        bufferUp();
        int scanLength = seq.length();
        if (scanLength > bufLength - bufPos)
            return false;

        for (int offset = 0; offset < scanLength; offset++)
            if (seq.charAt(offset) != charBuf[bufPos + offset])
                return false;
        return true;
    }

    boolean matchesIgnoreCase(String seq) {
        bufferUp();
        int scanLength = seq.length();
        if (scanLength > bufLength - bufPos)
            return false;

        for (int offset = 0; offset < scanLength; offset++) {
            char upScan = Character.toUpperCase(seq.charAt(offset));
            char upTarget = Character.toUpperCase(charBuf[bufPos + offset]);
            if (upScan != upTarget)
                return false;
        }
        return true;
    }

    boolean matchesAny(char... seq) {
        if (isEmpty())
            return false;

        bufferUp();
        char c = charBuf[bufPos];
        for (char seek : seq) {
            if (seek == c)
                return true;
        }
        return false;
    }

    boolean matchesAnySorted(char[] seq) {
        bufferUp();
        return !isEmpty() && Arrays.binarySearch(seq, charBuf[bufPos]) >= 0;
    }

    boolean matchesLetter() {
        if (isEmpty())
            return false;
        char c = charBuf[bufPos];
        return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || Character.isLetter(c);
    }

    boolean matchesDigit() {
        if (isEmpty())
            return false;
        char c = charBuf[bufPos];
        return (c >= '0' && c <= '9');
    }

    boolean matchConsume(String seq) {
        bufferUp();
        if (matches(seq)) {
            bufPos += seq.length();
            return true;
        } else {
            return false;
        }
    }

    boolean matchConsumeIgnoreCase(String seq) {
        if (matchesIgnoreCase(seq)) {
            bufPos += seq.length();
            return true;
        } else {
            return false;
        }
    }

    boolean containsIgnoreCase(String seq) {
        // used to check presence of </title>, </style>. only finds consistent case.
        String loScan = seq.toLowerCase(Locale.ENGLISH);
        String hiScan = seq.toUpperCase(Locale.ENGLISH);
        return (nextIndexOf(loScan) > -1) || (nextIndexOf(hiScan) > -1);
    }

    @Override
    public String toString() {
        return new String(charBuf, bufPos, bufLength - bufPos);
    }

    /**
     * Caches short strings, as a flywheel pattern, to reduce GC load. Just for this doc, to prevent leaks.
     * <p/>
     * Simplistic, and on hash collisions just falls back to creating a new string, vs a full HashMap with Entry list.
     * That saves both having to create objects as hash keys, and running through the entry list, at the expense of
     * some more duplicates.
     */
    private static String cacheString(final char[] charBuf, final String[] stringCache, final int start, final int count) {
        // limit (no cache):
        if (count > maxStringCacheLen)
            return new String(charBuf, start, count);
        if (count < 1)
            return "";

        // calculate hash:
        int hash = 0;
        int offset = start;
        for (int i = 0; i < count; i++) {
            hash = 31 * hash + charBuf[offset++];
        }

        // get from cache
        final int index = hash & stringCache.length - 1;
        String cached = stringCache[index];

        if (cached == null) { // miss, add
            cached = new String(charBuf, start, count);
            stringCache[index] = cached;
        } else { // hashcode hit, check equality
            if (rangeEquals(charBuf, start, count, cached)) { // hit
                return cached;
            } else { // hashcode conflict
                cached = new String(charBuf, start, count);
                stringCache[index] = cached; // update the cache, as recently used strings are more likely to show up again
            }
        }
        return cached;
    }

    /**
     * Check if the value of the provided range equals the string.
     */
    static boolean rangeEquals(final char[] charBuf, final int start, int count, final String cached) {
        if (count == cached.length()) {
            int i = start;
            int j = 0;
            while (count-- != 0) {
                if (charBuf[i++] != cached.charAt(j++))
                    return false;
            }
            return true;
        }
        return false;
    }

    // just used for testing
    boolean rangeEquals(final int start, final int count, final String cached) {
        return rangeEquals(charBuf, start, count, cached);
    }
}


package ru.noties.markwon.image;

import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.text.Spanned;
import android.text.style.DynamicDrawableSpan;
import android.view.View;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import ru.noties.markwon.renderer.R;

public abstract class AsyncDrawableScheduler {

    public static void schedule(@NonNull final TextView textView) {

        final List<AsyncDrawable> list = extract(textView);
        if (list.size() > 0) {

            if (textView.getTag(R.id.markwon_drawables_scheduler) == null) {
                final View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() {
                    @Override
                    public void onViewAttachedToWindow(View v) {

                    }

                    @Override
                    public void onViewDetachedFromWindow(View v) {
                        unschedule(textView);
                        v.removeOnAttachStateChangeListener(this);
                        v.setTag(R.id.markwon_drawables_scheduler, null);
                    }
                };
                textView.addOnAttachStateChangeListener(listener);
                textView.setTag(R.id.markwon_drawables_scheduler, listener);
            }

            for (AsyncDrawable drawable : list) {
                drawable.setCallback2(new DrawableCallbackImpl(textView, drawable.getBounds()));
            }
        }
    }

    // must be called when text manually changed in TextView
    public static void unschedule(@NonNull TextView view) {
        for (AsyncDrawable drawable : extract(view)) {
            drawable.setCallback2(null);
        }
    }

    private static List<AsyncDrawable> extract(@NonNull TextView view) {

        final List<AsyncDrawable> list;

        final CharSequence cs = view.getText();
        final int length = cs != null
                ? cs.length()
                : 0;

        if (length == 0 || !(cs instanceof Spanned)) {
            //noinspection unchecked
            list = Collections.EMPTY_LIST;
        } else {

            final List<AsyncDrawable> drawables = new ArrayList<>(2);

            final Spanned spanned = (Spanned) cs;
            final AsyncDrawableSpan[] asyncDrawableSpans = spanned.getSpans(0, length, AsyncDrawableSpan.class);
            if (asyncDrawableSpans != null
                    && asyncDrawableSpans.length > 0) {
                for (AsyncDrawableSpan span : asyncDrawableSpans) {
                    drawables.add(span.getDrawable());
                }
            }

            final DynamicDrawableSpan[] dynamicDrawableSpans = spanned.getSpans(0, length, DynamicDrawableSpan.class);
            if (dynamicDrawableSpans != null
                    && dynamicDrawableSpans.length > 0) {
                for (DynamicDrawableSpan span : dynamicDrawableSpans) {
                    final Drawable d = span.getDrawable();
                    if (d != null
                            && d instanceof AsyncDrawable) {
                        drawables.add((AsyncDrawable) d);
                    }
                }
            }

            if (drawables.size() == 0) {
                //noinspection unchecked
                list = Collections.EMPTY_LIST;
            } else {
                list = drawables;
            }
        }

        return list;
    }

    private AsyncDrawableScheduler() {
    }

    private static class DrawableCallbackImpl implements Drawable.Callback {

        private final TextView view;
        private Rect previousBounds;

        DrawableCallbackImpl(TextView view, Rect initialBounds) {
            this.view = view;
            this.previousBounds = new Rect(initialBounds);
        }

        @Override
        public void invalidateDrawable(@NonNull final Drawable who) {

            if (Looper.myLooper() != Looper.getMainLooper()) {
                view.post(new Runnable() {
                    @Override
                    public void run() {
                        invalidateDrawable(who);
                    }
                });
                return;
            }

            final Rect rect = who.getBounds();

            // okay... the thing is IF we do not change bounds size, normal invalidate would do
            // but if the size has changed, then we need to update the whole layout...

            if (!previousBounds.equals(rect)) {
                // the only method that seems to work when bounds have changed
                view.setText(view.getText());
                previousBounds = new Rect(rect);
            } else {

                view.postInvalidate();
            }
        }

        @Override
        public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
            final long delay = when - SystemClock.uptimeMillis();
            view.postDelayed(what, delay);
        }

        @Override
        public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
            view.removeCallbacks(what);
        }
    }
}


package ru.noties.markwon.ext.tables;

import android.content.Context;
import android.support.annotation.NonNull;
import android.text.Spanned;
import android.widget.TextView;

import org.commonmark.ext.gfm.tables.TableBody;
import org.commonmark.ext.gfm.tables.TableCell;
import org.commonmark.ext.gfm.tables.TableHead;
import org.commonmark.ext.gfm.tables.TableRow;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.SpannableBuilder;

/**
 * @since 3.0.0
 */
public class TablePlugin extends AbstractMarkwonPlugin {

    public interface ThemeConfigure {
        void configureTheme(@NonNull TableTheme.Builder builder);
    }

    /**
     * Factory method to create a {@link TablePlugin} with default {@link TableTheme} instance
     * (obtained via {@link TableTheme#create(Context)} method)
     *
     * @see #create(TableTheme)
     * @see #create(ThemeConfigure)
     */
    @NonNull
    public static TablePlugin create(@NonNull Context context) {
        return new TablePlugin(TableTheme.create(context));
    }

    @NonNull
    public static TablePlugin create(@NonNull TableTheme tableTheme) {
        return new TablePlugin(tableTheme);
    }

    @NonNull
    public static TablePlugin create(@NonNull ThemeConfigure themeConfigure) {
        final TableTheme.Builder builder = new TableTheme.Builder();
        themeConfigure.configureTheme(builder);
        return new TablePlugin(builder.build());
    }

    private final TableTheme theme;
    private final TableVisitor visitor;

    @SuppressWarnings("WeakerAccess")
    TablePlugin(@NonNull TableTheme tableTheme) {
        this.theme = tableTheme;
        this.visitor = new TableVisitor(tableTheme);
    }

    @NonNull
    public TableTheme theme() {
        return theme;
    }

    @Override
    public void configureParser(@NonNull Parser.Builder builder) {
        builder.extensions(Collections.singleton(TablesExtension.create()));
    }

    @Override
    public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
        visitor.configure(builder);
    }

    @Override
    public void beforeRender(@NonNull Node node) {
        // clear before rendering (as visitor has some internal mutable state)
        visitor.clear();
    }

    @Override
    public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
        TableRowsScheduler.unschedule(textView);
    }

    @Override
    public void afterSetText(@NonNull TextView textView) {
        TableRowsScheduler.schedule(textView);
    }

    private static class TableVisitor {

        private final TableTheme tableTheme;

        private List<TableRowSpan.Cell> pendingTableRow;
        private boolean tableRowIsHeader;
        private int tableRows;

        TableVisitor(@NonNull TableTheme tableTheme) {
            this.tableTheme = tableTheme;
        }

        void clear() {
            pendingTableRow = null;
            tableRowIsHeader = false;
            tableRows = 0;
        }

        void configure(@NonNull MarkwonVisitor.Builder builder) {
            builder
                    .on(TableBody.class, new MarkwonVisitor.NodeVisitor<TableBody>() {
                        @Override
                        public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableBody tableBody) {

                            visitor.visitChildren(tableBody);
                            tableRows = 0;

                            if (visitor.hasNext(tableBody)) {
                                visitor.ensureNewLine();
                                visitor.forceNewLine();
                            }
                        }
                    })
                    .on(TableRow.class, new MarkwonVisitor.NodeVisitor<TableRow>() {
                        @Override
                        public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableRow tableRow) {
                            visitRow(visitor, tableRow);
                        }
                    })
                    .on(TableHead.class, new MarkwonVisitor.NodeVisitor<TableHead>() {
                        @Override
                        public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableHead tableHead) {
                            visitRow(visitor, tableHead);
                        }
                    })
                    .on(TableCell.class, new MarkwonVisitor.NodeVisitor<TableCell>() {
                        @Override
                        public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableCell tableCell) {

                            final int length = visitor.length();

                            visitor.visitChildren(tableCell);

                            if (pendingTableRow == null) {
                                pendingTableRow = new ArrayList<>(2);
                            }

                            pendingTableRow.add(new TableRowSpan.Cell(
                                    tableCellAlignment(tableCell.getAlignment()),
                                    visitor.builder().removeFromEnd(length)
                            ));

                            tableRowIsHeader = tableCell.isHeader();
                        }
                    });
        }

        private void visitRow(@NonNull MarkwonVisitor visitor, @NonNull Node node) {

            final int length = visitor.length();

            visitor.visitChildren(node);

            if (pendingTableRow != null) {

                final SpannableBuilder builder = visitor.builder();

                // @since 2.0.0
                // we cannot rely on hasNext(TableHead) as it's not reliable
                // we must apply new line manually and then exclude it from tableRow span
                final boolean addNewLine;
                {
                    final int builderLength = builder.length();
                    addNewLine = builderLength > 0
                            && '\n' != builder.charAt(builderLength - 1);
                }

                if (addNewLine) {
                    visitor.forceNewLine();
                }

                // @since 1.0.4 Replace table char with non-breakable space
                // we need this because if table is at the end of the text, then it will be
                // trimmed from the final result
                builder.append('\u00a0');

                final Object span = new TableRowSpan(
                        tableTheme,
                        pendingTableRow,
                        tableRowIsHeader,
                        tableRows % 2 == 1);

                tableRows = tableRowIsHeader
                        ? 0
                        : tableRows + 1;

                visitor.setSpans(addNewLine ? length + 1 : length, span);

                pendingTableRow = null;
            }
        }

        @TableRowSpan.Alignment
        private static int tableCellAlignment(TableCell.Alignment alignment) {
            final int out;
            if (alignment != null) {
                switch (alignment) {
                    case CENTER:
                        out = TableRowSpan.ALIGN_CENTER;
                        break;
                    case RIGHT:
                        out = TableRowSpan.ALIGN_RIGHT;
                        break;
                    default:
                        out = TableRowSpan.ALIGN_LEFT;
                        break;
                }
            } else {
                out = TableRowSpan.ALIGN_LEFT;
            }
            return out;
        }
    }
}


package ru.noties.markwon.html.jsoup.helper;

/**
 * Simple validation methods. Designed for jsoup internal use
 */
public final class Validate {

    private Validate() {}

    /**
     * Validates that the object is not null
     * @param obj object to test
     */
    public static void notNull(Object obj) {
        if (obj == null)
            throw new IllegalArgumentException("Object must not be null");
    }

    /**
     * Validates that the object is not null
     * @param obj object to test
     * @param msg message to output if validation fails
     */
    public static void notNull(Object obj, String msg) {
        if (obj == null)
            throw new IllegalArgumentException(msg);
    }

    /**
     * Validates that the value is true
     * @param val object to test
     */
    public static void isTrue(boolean val) {
        if (!val)
            throw new IllegalArgumentException("Must be true");
    }

    /**
     * Validates that the value is true
     * @param val object to test
     * @param msg message to output if validation fails
     */
    public static void isTrue(boolean val, String msg) {
        if (!val)
            throw new IllegalArgumentException(msg);
    }

    /**
     * Validates that the value is false
     * @param val object to test
     */
    public static void isFalse(boolean val) {
        if (val)
            throw new IllegalArgumentException("Must be false");
    }

    /**
     * Validates that the value is false
     * @param val object to test
     * @param msg message to output if validation fails
     */
    public static void isFalse(boolean val, String msg) {
        if (val)
            throw new IllegalArgumentException(msg);
    }

    /**
     * Validates that the array contains no null elements
     * @param objects the array to test
     */
    public static void noNullElements(Object[] objects) {
        noNullElements(objects, "Array must not contain any null objects");
    }

    /**
     * Validates that the array contains no null elements
     * @param objects the array to test
     * @param msg message to output if validation fails
     */
    public static void noNullElements(Object[] objects, String msg) {
        for (Object obj : objects)
            if (obj == null)
                throw new IllegalArgumentException(msg);
    }

    /**
     * Validates that the string is not empty
     * @param string the string to test
     */
    public static void notEmpty(String string) {
        if (string == null || string.length() == 0)
            throw new IllegalArgumentException("String must not be empty");
    }

    /**
     * Validates that the string is not empty
     * @param string the string to test
     * @param msg message to output if validation fails
     */
    public static void notEmpty(String string, String msg) {
        if (string == null || string.length() == 0)
            throw new IllegalArgumentException(msg);
    }

    /**
     Cause a failure.
     @param msg message to output.
     */
    public static void fail(String msg) {
        throw new IllegalArgumentException(msg);
    }
}


package ru.noties.markwon;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.SpannableStringBuilder;
import android.text.Spanned;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;

/**
 * This class is used to _revert_ order of applied spans. Original SpannableStringBuilder
 * is using an array to store all the information about spans. So, a span that is added first
 * will be drawn first, which leads to subtle bugs (spans receive wrong `x` values when
 * requested to draw itself)
 *

 * since 2.0.0 implements Appendable and CharSequence
 *
 * @since 1.0.1
 */
@SuppressWarnings({"WeakerAccess", "unused"})
public class SpannableBuilder implements Appendable, CharSequence {

    /**
     * @since 2.0.0
     */
    public static void setSpans(@NonNull SpannableBuilder builder, @Nullable Object spans, int start, int end) {
        if (spans != null) {

            // setting a span for an invalid position can lead to silent fail (no exception,
            // but execution is stopped)
            if (!isPositionValid(builder.length(), start, end)) {
                return;
            }

            // @since 3.0.1 we introduce another method that recursively applies spans
            // allowing array of arrays (and more)
            setSpansInternal(builder, spans, start, end);
        }
    }

    // @since 2.0.1 package-private visibility for testing
    @VisibleForTesting
    static boolean isPositionValid(int length, int start, int end) {
        return end > start
                && start >= 0
                && end <= length;
    }

    private final StringBuilder builder;

    // actually we might be just using ArrayList
    private final Deque spans = new ArrayDeque<>(8);

    public SpannableBuilder() {
        this("");
    }

    public SpannableBuilder(@NonNull CharSequence cs) {
        this.builder = new StringBuilder(cs);
        copySpans(0, cs);
    }

    /**
     * Additional method that takes a String, which is proven to NOT contain any spans
     *
     * @param text String to append
     * @return this instance
     */
    @NonNull
    public SpannableBuilder append(@NonNull String text) {
        builder.append(text);
        return this;
    }

    @NonNull
    @Override
    public SpannableBuilder append(char c) {
        builder.append(c);
        return this;
    }

    @NonNull
    @Override
    public SpannableBuilder append(@NonNull CharSequence cs) {

        copySpans(length(), cs);

        builder.append(cs);

        return this;
    }

    /**
     * @since 2.0.0 to follow Appendable interface
     */
    @NonNull
    @Override
    public SpannableBuilder append(CharSequence csq, int start, int end) {

        final CharSequence cs = csq.subSequence(start, end);
        copySpans(length(), cs);

        builder.append(cs);

        return this;
    }

    @NonNull
    public SpannableBuilder append(@NonNull CharSequence cs, @NonNull Object span) {
        final int length = length();
        append(cs);
        setSpan(span, length);
        return this;
    }

    @NonNull
    public SpannableBuilder append(@NonNull CharSequence cs, @NonNull Object span, int flags) {
        final int length = length();
        append(cs);
        setSpan(span, length, length(), flags);
        return this;
    }

    @NonNull
    public SpannableBuilder setSpan(@NonNull Object span, int start) {
        return setSpan(span, start, length());
    }

    @NonNull
    public SpannableBuilder setSpan(@NonNull Object span, int start, int end) {
        return setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    @NonNull
    public SpannableBuilder setSpan(@NonNull Object span, int start, int end, int flags) {
        spans.push(new Span(span, start, end, flags));
        return this;
    }

    @Override
    public int length() {
        return builder.length();
    }

    @Override
    public char charAt(int index) {
        return builder.charAt(index);
    }

    /**
     * @since 2.0.0 to follow CharSequence interface
     */
    @Override
    public CharSequence subSequence(int start, int end) {

        final CharSequence out;

        // @since 2.0.1 we copy spans to resulting subSequence
        final List spans = getSpans(start, end);
        if (spans.isEmpty()) {
            out = builder.subSequence(start, end);
        } else {

            // we should not be SpannableStringBuilderReversed here
            final SpannableStringBuilder builder = new SpannableStringBuilder(this.builder.subSequence(start, end));

            final int length = builder.length();

            int s;
            int e;

            for (Span span : spans) {

                // we should limit start/end to resulting subSequence length
                //
                // for example, originally it was 5-7 and range 5-7 requested
                // span should have 0-2
                //
                // if a span was fully including resulting subSequence it's start and
                // end must be within 0..length bounds
                s = Math.max(0, span.start - start);
                e = Math.max(length, s + (span.end - span.start));

                builder.setSpan(
                        span.what,
                        s,
                        e,
                        span.flags
                );
            }
            out = builder;
        }

        return out;
    }

    /**
     * This method will return all {@link Span} spans that overlap specified range,
     * so if for example a 1..9 range is specified some spans might have 0..6 or 0..10 start/end ranges.
     * <<<<<<>>>>>> master:markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java
     *
     * @since 2.0.1
     */
    @NonNull
    public List getSpans(int start, int end) {

        final int length = length();

        if (!isPositionValid(length, start, end)) {
            // we might as well throw here
            return Collections.emptyList();
        }

        // all requested
        if (start == 0
                && length == end) {
            // but also copy (do not allow external modification)
            final List list = new ArrayList<>(spans);
            Collections.reverse(list);
            return Collections.unmodifiableList(list);
        }

        final List list = new ArrayList<>(0);

        final Iterator iterator = spans.descendingIterator();
        Span span;

        while (iterator.hasNext()) {
            span = iterator.next();
            // we must execute 2 checks: if overlap with specified range or fully include it
            // if span.start is >= range.start -> check if it's before range.end
            // if span.end is <= end -> check if it's after range.start
            if (
                    (span.start >= start && span.start < end)
                            || (span.end <= end && span.end > start)
                            || (span.start < start && span.end > end)) {
                list.add(span);
            }
        }

        return Collections.unmodifiableList(list);
    }

    public char lastChar() {
        return builder.charAt(length() - 1);
    }

    @NonNull
    public CharSequence removeFromEnd(int start) {

        // this method is not intended to be used by clients
        // it's a workaround to support tables

        final int end = length();

        // as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String
        final SpannableStringBuilderReversed impl = new SpannableStringBuilderReversed(builder.subSequence(start, end));

        final Iterator iterator = spans.iterator();

        Span span;

        while (iterator.hasNext() && ((span = iterator.next())) != null) {
            if (span.start >= start && span.end <= end) {
                impl.setSpan(span.what, span.start - start, span.end - start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                iterator.remove();
            }
        }

        builder.replace(start, end, "");

        return impl;
    }

    @Override
    @NonNull
    public String toString() {
        return builder.toString();
    }

    @NonNull
    public CharSequence text() {
        // @since 2.0.0 redirects this call to `#spannableStringBuilder()`
        return spannableStringBuilder();
    }

    /**
     * Simple method to create a SpannableStringBuilder, which is created anyway. Unlike {@link #text()}
     * method which returns the same SpannableStringBuilder there is no need to cast the resulting
     * CharSequence and makes the thing more explicit
     *
     * @since 2.0.0
     */
    @NonNull
    public SpannableStringBuilder spannableStringBuilder() {

        // okay, in order to not allow external modification and keep our spans order
        // we should not return our builder
        //
        // plus, if this method was called -> all spans would be applied, which potentially
        // breaks the order that we intend to use
        // so, we will defensively copy builder

        // as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String

        final SpannableStringBuilderReversed reversed = new SpannableStringBuilderReversed(builder);

        // NB, as e are using Deque -> iteration will be started with last element
        // so, spans will be appearing in the for loop in reverse order
        for (Span span : spans) {
            reversed.setSpan(span.what, span.start, span.end, span.flags);
        }

        return reversed;
    }

    /**
     * @since 3.0.0
     */
    public void clear() {
        builder.setLength(0);
        spans.clear();
    }

    private void copySpans(final int index, @Nullable CharSequence cs) {

        // we must identify already reversed Spanned...
        // and (!) iterate backwards when adding (to preserve order)

        if (cs instanceof Spanned) {

            final Spanned spanned = (Spanned) cs;
            final boolean reversed = spanned instanceof SpannableStringBuilderReversed;

            final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
            final int length = spans != null
                    ? spans.length
                    : 0;

            if (length > 0) {
                if (reversed) {
                    Object o;
                    for (int i = length - 1; i >= 0; i--) {
                        o = spans[i];
                        setSpan(
                                o,
                                index + spanned.getSpanStart(o),
                                index + spanned.getSpanEnd(o),
                                spanned.getSpanFlags(o)
                        );
                    }
                } else {
                    Object o;
                    for (int i = 0; i < length; i++) {
                        o = spans[i];
                        setSpan(
                                o,
                                index + spanned.getSpanStart(o),
                                index + spanned.getSpanEnd(o),
                                spanned.getSpanFlags(o)
                        );
                    }
                }
            }
        }
    }

    /**
     * @since 2.0.1 made public in order to be returned from `getSpans` method, initially added in 1.0.1
     */
    public static class Span {

        public final Object what;
        public int start;
        public int end;
        public final int flags;

        Span(@NonNull Object what, int start, int end, int flags) {
            this.what = what;
            this.start = start;
            this.end = end;
            this.flags = flags;
        }
    }

    /**
     * @since 2.0.1 made inner class of {@link SpannableBuilder}, initially added in 1.0.1
     */
    static class SpannableStringBuilderReversed extends SpannableStringBuilder {
        SpannableStringBuilderReversed(CharSequence text) {
            super(text);
        }
    }

    /**
     * @since 3.0.1
     */
    private static void setSpansInternal(
            @NonNull SpannableBuilder builder,
            @Nullable Object spans,
            int start,
            int end) {
        if (spans != null) {
            if (spans.getClass().isArray()) {
                for (Object o : ((Object[]) spans)) {
                    // @since 3.0.1 recursively apply spans (allow array of arrays)
                    setSpansInternal(builder, o, start, end);
                }
            } else {
                builder.setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }
}