Merge 09e45f2916ad001a53ccf6ebe92afdcb337d326f into b0e548ce500a1617e402cdc89b0bb2e038cb1c79

This commit is contained in:
Cyrus Bakhtiari-Haftlang 2018-08-17 08:17:41 +00:00 committed by GitHub
commit f928625134
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 605 additions and 318 deletions

View File

@ -30,6 +30,25 @@ These are XML attributes:
```
app:mv_markdown="string"
app:mv_configurationProvider="string"
app:mv_H1Style="reference"
app:mv_H2Style="reference"
app:mv_H3Style="reference"
app:mv_H4Style="reference"
app:mv_H5Style="reference"
app:mv_H6Style="reference"
app:mv_EmphasisStyle="reference"
app:mv_StrongEmphasisStyle="reference"
app:mv_BlockQuoteStyle="reference"
app:mv_CodeSpanStyle="reference"
app:mv_MultilineCodeSpanStyle="reference"
app:mv_OrderedListItemStyle="reference"
app:mv_BulletListItemStyle="reference"
app:mv_TaskListItemStyle="reference"
app:mv_TableRowStyle="reference"
app:mv_ParagraphStyle="reference"
app:mv_LinkStyle="reference"
```
`mv_markdown` accepts a string and represents raw markdown
@ -38,4 +57,51 @@ app:mv_configurationProvider="string"
for example: `com.example.my.package.MyConfigurationProvider` (this class must have an empty constructor
in order to be instantiated via reflection).
Please note that those views parse markdown in main thread, so their usage must be for relatively small markdown portions only
Please note that those views parse markdown in main thread, so their usage must be for relatively small markdown portions only.
An `mv_*Style` may refer to an actual style or a theme attribute that resolves to a style as well as support for `android:textAppearance` which is basically a sub-style. Currently the following attributes are supported:
* `android:textColor`
* `android:textColorLink`
* `android:textSize`
* `android:textStyle`
* `android:fontFamily`
* `fontFamily`
* `android:typeface`
* `android:textAppeance`
* `android:textColor`
* `android:textColorLink`
* `android:textSize`
* `android:textStyle`
* `android:fontFamily`
* `fontFamily`
* `android:typeface`
A theme level style may be set through the theme attribute `markwonViewStyle`.
Note that, just like `TextView`, values included in `textAppearance` are canceled by values in the root style. Also mimicking `AppCompatTextView`, `android:fontFamily` takes precedence over `fontFamily`.
## Example
```XML
<ru.noties.markwon.view.MarkwonViewCompat
style="?android:textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mv_H1Style="@style/MyH1Style"
app:mv_H2Style="?myThemesH2Style"
app:mv_H3Style="@style/MyH3Style"
app:mv_H4Style="?android:textAppearanceLarge"
app:mv_H5Style="?android:textAppearanceMediumInverse"
app:mv_H6Style="?android:textAppearanceButton"
app:mv_EmphasisStyle="@style/MyEmphasisStyleWithCustomFont"
app:mv_StrongEmphasisStyle="?myThemesStrongEmphasisStyleWithCustomFont"
app:mv_BlockQuoteStyle="@null"
app:mv_CodeSpanStyle="@style/MyCodeSpanStyle"
app:mv_MultilineCodeSpanStyle="?myThemesMultilineCodeSpan"
app:mv_OrderedListItemStyle="?android:textAppearanceListItem"
app:mv_BulletListItemStyle="?android:textAppearanceListItemSmall"
app:mv_TaskListItemStyle="?android:textAppearanceSearchResultSubtitle"
app:mv_TableRowStyle="?android:textAppearanceSearchResultTitle"
app:mv_LinkStyle="?android:textAppearanceSmallPopupMenu"
app:mv_markdown="@string/some_markdown_string_resource" />
```
In the above example, the paragraph text will get the style of the widget itself `style="?android:textAppearanceSmall"`. If a particular markup style is not specified or specified as `@null` Markwon's original spans will be applied for text with that markup. Otherwise the styling will be based on the resolved attributes. By supplying a valid `app:mv_ParagraphStyle` the default text (paragraph) will be spanned explicitly.

View File

@ -15,7 +15,7 @@ android {
dependencies {
api project(':library')
compileOnly SUPPORT_APP_COMPAT
implementation SUPPORT_APP_COMPAT
}
afterEvaluate {

View File

@ -2,8 +2,12 @@ package ru.noties.markwon.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.StyleRes;
import android.util.AttributeSet;
import android.widget.TextView;
@ -14,19 +18,36 @@ public class MarkwonView extends TextView implements IMarkwonView {
private MarkwonViewHelper helper;
public MarkwonView(Context context) {
public MarkwonView(@NonNull Context context) {
super(context);
init(context, null);
init(context, null, R.attr.markwonViewStyle, 0);
}
public MarkwonView(Context context, AttributeSet attrs) {
public MarkwonView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
init(context, attrs, R.attr.markwonViewStyle, 0);
}
private void init(Context context, AttributeSet attributeSet) {
public MarkwonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MarkwonView(Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(@NonNull Context context,
@Nullable AttributeSet attributeSet,
@AttrRes int defStyleAttr,
@StyleRes int defStyleRes) {
helper = MarkwonViewHelper.create(this);
helper.init(context, attributeSet);
helper.init(context, attributeSet, defStyleAttr, defStyleRes);
}
@Override

View File

@ -1,6 +1,7 @@
package ru.noties.markwon.view;
import android.content.Context;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.AppCompatTextView;
@ -12,19 +13,28 @@ public class MarkwonViewCompat extends AppCompatTextView implements IMarkwonView
private MarkwonViewHelper helper;
public MarkwonViewCompat(Context context) {
public MarkwonViewCompat(@NonNull Context context) {
super(context);
init(context, null);
init(context, null, R.attr.markwonViewStyle);
}
public MarkwonViewCompat(Context context, AttributeSet attrs) {
public MarkwonViewCompat(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
init(context, attrs, R.attr.markwonViewStyle);
}
private void init(Context context, AttributeSet attributeSet) {
public MarkwonViewCompat(@NonNull Context context,
@Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(@NonNull Context context,
@Nullable AttributeSet attributeSet,
@AttrRes int defStyleAttr) {
helper = MarkwonViewHelper.create(this);
helper.init(context, attributeSet);
helper.init(context, attributeSet, defStyleAttr, 0);
}
@Override

View File

@ -1,15 +1,40 @@
package ru.noties.markwon.view;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.os.Build;
import android.support.annotation.AttrRes;
import android.support.annotation.FontRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StyleRes;
import android.support.annotation.StyleableRes;
import android.support.v4.content.res.ResourcesCompat;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.MetricAffectingSpan;
import android.text.style.TextAppearanceSpan;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.widget.TextView;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import ru.noties.markwon.Markwon;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.SpannableFactoryDef;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.spans.TableRowSpan;
public class MarkwonViewHelper implements IMarkwonView {
@ -24,15 +49,30 @@ public class MarkwonViewHelper implements IMarkwonView {
private SpannableConfiguration configuration;
private String markdown;
@NonNull
private final SparseIntArray styles;
private MarkwonViewHelper(@NonNull TextView textView) {
this.textView = textView;
this.styles = new SparseIntArray();
}
public void init(Context context, AttributeSet attributeSet) {
if (attributeSet != null) {
final TypedArray array = context.obtainStyledAttributes(attributeSet, R.styleable.MarkwonView);
public void init(@NonNull Context context,
@Nullable AttributeSet attributeSet,
@AttrRes int defStyleAttr,
@StyleRes int defStyleRes) {
final TypedArray array = context.obtainStyledAttributes(
attributeSet,
R.styleable.MarkwonView,
defStyleAttr,
defStyleRes);
try {
final int count = array.getIndexCount();
for (int idx = 0; idx < count; idx++) {
@StyleableRes final int relativeIndex = array.getIndex(idx);
styles.put(relativeIndex, array.getResourceId(relativeIndex, 0));
}
final String configurationProvider = array.getString(R.styleable.MarkwonView_mv_configurationProvider);
final ConfigurationProvider provider;
@ -53,7 +93,6 @@ public class MarkwonViewHelper implements IMarkwonView {
array.recycle();
}
}
}
@Override
public void setConfigurationProvider(@NonNull ConfigurationProvider provider) {
@ -78,7 +117,10 @@ public class MarkwonViewHelper implements IMarkwonView {
if (provider != null) {
this.configuration = provider.provide(textView.getContext());
} else {
this.configuration = SpannableConfiguration.create(textView.getContext());
this.configuration = SpannableConfiguration
.builder(textView.getContext())
.factory(new StyleableSpanFactory(textView.getContext(), styles))
.build();
}
}
configuration = this.configuration;
@ -93,7 +135,7 @@ public class MarkwonViewHelper implements IMarkwonView {
}
@Nullable
public static IMarkwonView.ConfigurationProvider obtainProvider(@NonNull String className) {
private static IMarkwonView.ConfigurationProvider obtainProvider(@NonNull String className) {
try {
final Class<?> cl = Class.forName(className);
return (IMarkwonView.ConfigurationProvider) cl.newInstance();
@ -102,4 +144,331 @@ public class MarkwonViewHelper implements IMarkwonView {
return null;
}
}
/**
* A SpannableFactory that creates spans based on style attributes of MarkwonView
*/
private static final class StyleableSpanFactory implements SpannableFactory {
@NonNull
private static final int[] TEXT_APPEARANCE_ATTR = new int[]{android.R.attr.textAppearance};
private static final int NO_VALUE = 0;
@NonNull
private static final SparseIntArray HEADING_STYLE_MAP = new SparseIntArray();
static {
// Unlike library attributes, the index of styleables are stable across builds making
// them safe to declare statically.
HEADING_STYLE_MAP.put(1, R.styleable.MarkwonView_mv_H1Style);
HEADING_STYLE_MAP.put(2, R.styleable.MarkwonView_mv_H2Style);
HEADING_STYLE_MAP.put(3, R.styleable.MarkwonView_mv_H3Style);
HEADING_STYLE_MAP.put(4, R.styleable.MarkwonView_mv_H4Style);
HEADING_STYLE_MAP.put(5, R.styleable.MarkwonView_mv_H5Style);
HEADING_STYLE_MAP.put(6, R.styleable.MarkwonView_mv_H6Style);
}
@NonNull
private final Context context;
@NonNull
private final SparseIntArray styles;
@NonNull
private final SpannableFactory defaultFactory;
StyleableSpanFactory(@NonNull Context context, @NonNull SparseIntArray styles) {
this.context = context;
this.styles = styles;
this.defaultFactory = SpannableFactoryDef.create();
}
@Nullable
@Override
public Object strongEmphasis() {
final int style = styles.get(R.styleable.MarkwonView_mv_StrongEmphasisStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style);
} else {
return defaultFactory.strongEmphasis();
}
}
@Nullable
@Override
public Object emphasis() {
final int style = styles.get(R.styleable.MarkwonView_mv_EmphasisStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style);
} else {
return defaultFactory.emphasis();
}
}
@Nullable
@Override
public Object blockQuote(@NonNull SpannableTheme theme) {
final int style = styles.get(R.styleable.MarkwonView_mv_BlockQuoteStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style,
createSpanCollection(defaultFactory.blockQuote(theme)));
}
return defaultFactory.blockQuote(theme);
}
@Nullable
@Override
public Object code(@NonNull SpannableTheme theme, boolean multiline) {
final int styleAttr = multiline ? R.styleable.MarkwonView_mv_MultilineCodeSpanStyle :
R.styleable.MarkwonView_mv_CodeSpanStyle;
final int style = styles.get(styleAttr, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style,
createSpanCollection(defaultFactory.code(theme, multiline)));
}
return defaultFactory.code(theme, multiline);
}
@Nullable
@Override
public Object orderedListItem(@NonNull SpannableTheme theme, int startNumber) {
final int style = styles.get(R.styleable.MarkwonView_mv_OrderedListItemStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style,
createSpanCollection(defaultFactory.orderedListItem(theme, startNumber)));
}
return defaultFactory.orderedListItem(theme, startNumber);
}
@Nullable
@Override
public Object bulletListItem(@NonNull SpannableTheme theme, int level) {
final int style = styles.get(R.styleable.MarkwonView_mv_BulletListItemStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style,
createSpanCollection(defaultFactory.bulletListItem(theme, level)));
}
return defaultFactory.bulletListItem(theme, level);
}
@Nullable
@Override
public Object thematicBreak(@NonNull SpannableTheme theme) {
return defaultFactory.thematicBreak(theme);
}
@Nullable
@Override
public Object heading(@NonNull SpannableTheme theme, int level) {
final int style = styles.get(HEADING_STYLE_MAP.get(level), NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style);
}
return defaultFactory.heading(theme, level);
}
@Nullable
@Override
public Object strikethrough() {
return defaultFactory.strikethrough();
}
@Nullable
@Override
public Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) {
final int style = styles.get(R.styleable.MarkwonView_mv_TaskListItemStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style,
createSpanCollection(defaultFactory
.taskListItem(theme, blockIndent, isDone)));
}
return defaultFactory.taskListItem(theme, blockIndent, isDone);
}
@Nullable
@Override
public Object tableRow(@NonNull SpannableTheme theme,
@NonNull List<TableRowSpan.Cell> cells,
boolean isHeader,
boolean isOdd) {
final int style = styles.get(R.styleable.MarkwonView_mv_TableRowStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style,
createSpanCollection(defaultFactory
.tableRow(theme, cells, isHeader, isOdd)));
}
return defaultFactory.tableRow(theme, cells, isHeader, isOdd);
}
@Nullable
@Override
public Object paragraph(boolean inTightList) {
final int style = styles.get(R.styleable.MarkwonView_mv_ParagraphStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style,
createSpanCollection(defaultFactory.paragraph(inTightList)));
}
return defaultFactory.paragraph(inTightList);
}
@Nullable
@Override
public Object image(@NonNull SpannableTheme theme,
@NonNull String destination,
@NonNull AsyncDrawable.Loader loader,
@NonNull ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize,
boolean replacementTextIsLink) {
return defaultFactory.image(
theme,
destination,
loader,
imageSizeResolver,
imageSize,
replacementTextIsLink);
}
@Nullable
@Override
public Object link(@NonNull SpannableTheme theme,
@NonNull String destination,
@NonNull LinkSpan.Resolver resolver) {
final int style = styles.get(R.styleable.MarkwonView_mv_LinkStyle, NO_VALUE);
if (style != NO_VALUE) {
return createFromStyle(style,
createSpanCollection(defaultFactory.link(theme, destination, resolver)));
}
return defaultFactory.link(theme, destination, resolver);
}
@Nullable
@Override
public Object superScript(@NonNull SpannableTheme theme) {
return defaultFactory.superScript(theme);
}
@Nullable
@Override
public Object subScript(@NonNull SpannableTheme theme) {
return defaultFactory.subScript(theme);
}
@Nullable
@Override
public Object underline() {
return defaultFactory.underline();
}
@NonNull
private Object[] createFromStyle(@StyleRes int styleResource) {
return createFromStyle(styleResource, new ArrayDeque<>());
}
@NonNull
private Object[] createFromStyle(@StyleRes int styleResource,
@NonNull Deque<Object> spans) {
TypedArray a = context.obtainStyledAttributes(styleResource, TEXT_APPEARANCE_ATTR);
final int ap = a.getResourceId(0, NO_VALUE);
a.recycle();
if (ap != NO_VALUE) {
spans.addFirst(new TextAppearanceSpan(context, ap));
handleCustomFont(ap, spans);
}
spans.addFirst(new TextAppearanceSpan(context, styleResource));
handleCustomFont(styleResource, spans);
return spans.toArray();
}
private void handleCustomFont(@StyleRes int style, @NonNull Deque<Object> spans) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
// In SDK 27, custom fonts are handled in TextAppearanceSpan so no need to re-invent
// the wheel.
return;
}
final TypedArray a = context.obtainStyledAttributes(
style, R.styleable.MV_CustomFonts);
if (a.hasValue(R.styleable.MV_CustomFonts_android_fontFamily) ||
a.hasValue(R.styleable.MV_CustomFonts_fontFamily)) {
@StyleableRes int resolvedFontFamily =
a.hasValue(R.styleable.MV_CustomFonts_android_fontFamily)
? R.styleable.MV_CustomFonts_android_fontFamily
: R.styleable.MV_CustomFonts_fontFamily;
try {
@FontRes int fontResId = a.getResourceId(resolvedFontFamily, NO_VALUE);
if (fontResId != NO_VALUE) {
final Typeface typeface = ResourcesCompat.getFont(context, fontResId);
if (typeface != null) {
spans.addFirst(new CustomTypefaceSpan(typeface));
}
}
} catch (UnsupportedOperationException | Resources.NotFoundException e) {
// Expected if it is not a font resource.
}
}
a.recycle();
}
private static Deque<Object> createSpanCollection(@Nullable Object defaultSpan) {
final Deque<Object> spanCollection = new ArrayDeque<>();
if (defaultSpan instanceof Object[]) {
for (final Object span : (Object[]) defaultSpan) {
spanCollection.addLast(span);
}
} else if (defaultSpan != null) {
spanCollection.addLast(defaultSpan);
}
return spanCollection;
}
// taken from https://stackoverflow.com/a/17961854
private static class CustomTypefaceSpan extends MetricAffectingSpan {
private final Typeface typeface;
CustomTypefaceSpan(final Typeface typeface) {
this.typeface = typeface;
}
@Override
public void updateDrawState(final TextPaint drawState) {
apply(drawState);
}
@Override
public void updateMeasureState(final TextPaint paint) {
apply(paint);
}
private void apply(final Paint paint) {
final Typeface oldTypeface = paint.getTypeface();
final int oldStyle = oldTypeface != null ? oldTypeface.getStyle() : 0;
final int fakeStyle = oldStyle & ~typeface.getStyle();
if ((fakeStyle & Typeface.BOLD) != 0) {
paint.setFakeBoldText(true);
}
if ((fakeStyle & Typeface.ITALIC) != 0) {
paint.setTextSkewX(-0.25f);
}
paint.setTypeface(typeface);
}
}
}
}

View File

@ -1,9 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="markwonViewStyle" format="reference" />
<declare-styleable name="MarkwonView">
<attr name="mv_configurationProvider" format="string" />
<attr name="mv_markdown" format="string" />
<attr name="mv_H1Style" format="reference" />
<attr name="mv_H2Style" format="reference" />
<attr name="mv_H3Style" format="reference" />
<attr name="mv_H4Style" format="reference" />
<attr name="mv_H5Style" format="reference" />
<attr name="mv_H6Style" format="reference" />
<attr name="mv_EmphasisStyle" format="reference" />
<attr name="mv_StrongEmphasisStyle" format="reference" />
<attr name="mv_BlockQuoteStyle" format="reference" />
<attr name="mv_CodeSpanStyle" format="reference" />
<attr name="mv_MultilineCodeSpanStyle" format="reference" />
<attr name="mv_OrderedListItemStyle" format="reference" />
<attr name="mv_BulletListItemStyle" format="reference" />
<attr name="mv_TaskListItemStyle" format="reference" />
<attr name="mv_TableRowStyle" format="reference" />
<attr name="mv_ParagraphStyle" format="reference" />
<attr name="mv_LinkStyle" format="reference" />
</declare-styleable>
<declare-styleable name="MV_CustomFonts">
<attr name="android:fontFamily" />
<attr name="fontFamily" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,42 @@
package ru.noties.markwon;
import android.text.SpannableStringBuilder;
/**
* Copied as is from @see <a href = "https://github.com/Uncodin/bypass/blob/master/platform/android/
* library/src/in/uncod/android/bypass/ReverseSpannableStringBuilder.java">Uncodin/bypass</a>
*/
public class ReverseSpannableStringBuilder extends SpannableStringBuilder {
public ReverseSpannableStringBuilder() {
super();
}
public ReverseSpannableStringBuilder(CharSequence text, int start, int end) {
super(text, start, end);
}
@Override
public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
T[] ret = super.getSpans(queryStart, queryEnd, kind);
reverse(ret);
return ret;
}
private static void reverse(Object[] arr) {
if (arr == null) {
return;
}
int i = 0;
int j = arr.length - 1;
Object tmp;
while (j > i) {
tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
j--;
i++;
}
}
}

View File

@ -1,241 +0,0 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
/**
* 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 1.0.1
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class SpannableBuilder {
// do not implement CharSequence (or any of Spanned interfaces)
// we will be using SpannableStringBuilder anyway as a backing store
// as it has tight connection with system (implements some hidden methods, etc)
private final SpannableStringBuilder builder;
// actually we might be just using ArrayList
private final Deque<Span> spans = new ArrayDeque<>(8);
public SpannableBuilder() {
this("");
}
public SpannableBuilder(@NonNull CharSequence cs) {
this.builder = new SpannableStringBuilderImpl(cs.toString());
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
public SpannableBuilder append(char c) {
builder.append(c);
return this;
}
@NonNull
public SpannableBuilder append(@NonNull CharSequence cs) {
copySpans(length(), cs);
builder.append(cs.toString());
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;
}
public int length() {
return builder.length();
}
public char charAt(int index) {
return builder.charAt(index);
}
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 SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder.subSequence(start, end));
final Iterator<Span> 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() {
// 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 SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder);
for (Span span : spans) {
impl.setSpan(span.what, span.start, span.end, span.flags);
}
// now, let's remove trailing newLines (so small amounts of text are displayed correctly)
// @since 1.0.2
final int length = impl.length();
if (length > 0) {
int amount = 0;
for (int i = length - 1; i >= 0; i--) {
if (Character.isWhitespace(impl.charAt(i))) {
amount += 1;
} else {
break;
}
}
if (amount > 0) {
impl.replace(length - amount, length, "");
}
}
return impl;
}
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 reverse = spanned instanceof SpannedReversed;
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
final int length = spans != null
? spans.length
: 0;
if (length > 0) {
if (reverse) {
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)
);
}
}
}
}
}
static class Span {
final Object what;
int start;
int end;
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;
}
}
}

View File

@ -1,13 +0,0 @@
package ru.noties.markwon;
import android.text.SpannableStringBuilder;
/**
* @since 1.0.1
*/
class SpannableStringBuilderImpl extends SpannableStringBuilder implements SpannedReversed {
SpannableStringBuilderImpl(CharSequence text) {
super(text);
}
}

View File

@ -1,9 +0,0 @@
package ru.noties.markwon;
import android.text.Spanned;
/**
* @since 1.0.1
*/
interface SpannedReversed extends Spanned {
}

View File

@ -2,6 +2,7 @@ package ru.noties.markwon.renderer;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
@ -39,7 +40,7 @@ import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.ReverseSpannableStringBuilder;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.renderer.html.SpannableHtmlParser;
@ -52,7 +53,7 @@ import ru.noties.markwon.tasklist.TaskListItem;
public class SpannableMarkdownVisitor extends AbstractVisitor {
private final SpannableConfiguration configuration;
private final SpannableBuilder builder;
private final SpannableStringBuilder builder;
private final Deque<HtmlInlineItem> htmlInlineItems;
private final SpannableTheme theme;
@ -67,7 +68,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
public SpannableMarkdownVisitor(
@NonNull SpannableConfiguration configuration,
@NonNull SpannableBuilder builder
@NonNull SpannableStringBuilder builder
) {
this.configuration = configuration;
this.builder = builder;
@ -363,10 +364,9 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
if (pendingTableRow == null) {
pendingTableRow = new ArrayList<>(2);
}
pendingTableRow.add(new TableRowSpan.Cell(
tableCellAlignment(cell.getAlignment()),
builder.removeFromEnd(length)
removeFromEnd(length)
));
tableRowIsHeader = cell.isHeader();
@ -378,6 +378,22 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
return handled;
}
@NonNull
private CharSequence removeFromEnd(int start) {
// this method is not intended to be used by clients
// it's a workaround to support tables
final int end = builder.length();
// as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String
final SpannableStringBuilder impl = new ReverseSpannableStringBuilder(builder, start, end);
builder.delete(start, end);
return impl;
}
@Override
public void visit(Paragraph paragraph) {
final boolean inTightList = isInTightList(paragraph);
@ -504,7 +520,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
private void newLine() {
if (builder.length() > 0
&& '\n' != builder.lastChar()) {
&& '\n' != builder.charAt(builder.length() - 1)) {
builder.append('\n');
}
}

View File

@ -1,18 +1,19 @@
package ru.noties.markwon.renderer;
import android.support.annotation.NonNull;
import android.text.SpannableStringBuilder;
import org.commonmark.node.Node;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.ReverseSpannableStringBuilder;
import ru.noties.markwon.SpannableConfiguration;
public class SpannableRenderer {
@NonNull
public CharSequence render(@NonNull SpannableConfiguration configuration, @NonNull Node node) {
final SpannableBuilder builder = new SpannableBuilder();
final SpannableStringBuilder builder = new ReverseSpannableStringBuilder();
node.accept(new SpannableMarkdownVisitor(configuration, builder));
return builder.text();
return builder;
}
}

View File

@ -1,25 +1,25 @@
package ru.noties.markwon.sample.extension;
import android.support.annotation.NonNull;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.TextView;
import org.commonmark.node.CustomNode;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.renderer.SpannableMarkdownVisitor;
@SuppressWarnings("WeakerAccess")
public class IconVisitor extends SpannableMarkdownVisitor {
private final SpannableBuilder builder;
private final SpannableStringBuilder builder;
private final IconSpanProvider iconSpanProvider;
public IconVisitor(
@NonNull SpannableConfiguration configuration,
@NonNull SpannableBuilder builder,
@NonNull SpannableStringBuilder builder,
@NonNull IconSpanProvider iconSpanProvider
) {
super(configuration, builder);
@ -51,7 +51,7 @@ public class IconVisitor extends SpannableMarkdownVisitor {
final int length = builder.length();
builder.append(name);
builder.setSpan(iconSpanProvider.provide(name, color, size), length);
builder.setSpan(iconSpanProvider.provide(name, color, size), length, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.append(' ');
return true;

View File

@ -3,6 +3,7 @@ package ru.noties.markwon.sample.extension;
import android.app.Activity;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.widget.TextView;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
@ -12,7 +13,7 @@ import org.commonmark.parser.Parser;
import java.util.Arrays;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.ReverseSpannableStringBuilder;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.tasklist.TaskListExtension;
@ -45,7 +46,7 @@ public class MainActivity extends Activity {
final Node node = parser.parse(markdown);
final SpannableBuilder builder = new SpannableBuilder();
final SpannableStringBuilder builder = new ReverseSpannableStringBuilder();
// please note that here I am passing `0` as fallback it means that if markdown references
// unknown icon, it will try to load fallback one and will fail with ResourceNotFound. It's
@ -70,6 +71,6 @@ public class MainActivity extends Activity {
node.accept(visitor);
// apply
textView.setText(builder.text());
textView.setText(builder);
}
}