diff --git a/library-view/README.md b/library-view/README.md
index 71f032db..5213be4e 100644
--- a/library-view/README.md
+++ b/library-view/README.md
@@ -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
+
+```
+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.
\ No newline at end of file
diff --git a/library-view/build.gradle b/library-view/build.gradle
index fb709db9..bc4a3c3b 100644
--- a/library-view/build.gradle
+++ b/library-view/build.gradle
@@ -15,7 +15,7 @@ android {
dependencies {
api project(':library')
- compileOnly SUPPORT_APP_COMPAT
+ implementation SUPPORT_APP_COMPAT
}
afterEvaluate {
diff --git a/library-view/src/main/java/ru/noties/markwon/view/MarkwonView.java b/library-view/src/main/java/ru/noties/markwon/view/MarkwonView.java
index 19ab09b0..3f2130af 100644
--- a/library-view/src/main/java/ru/noties/markwon/view/MarkwonView.java
+++ b/library-view/src/main/java/ru/noties/markwon/view/MarkwonView.java
@@ -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
diff --git a/library-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java b/library-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java
index da5c1934..9619b995 100644
--- a/library-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java
+++ b/library-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java
@@ -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
diff --git a/library-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java b/library-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java
index c8c813f6..5d5391b0 100644
--- a/library-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java
+++ b/library-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java
@@ -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,34 +49,48 @@ 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) {
+ 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);
- if (attributeSet != null) {
- final TypedArray array = context.obtainStyledAttributes(attributeSet, R.styleable.MarkwonView);
- try {
-
- final String configurationProvider = array.getString(R.styleable.MarkwonView_mv_configurationProvider);
- final ConfigurationProvider provider;
- if (!TextUtils.isEmpty(configurationProvider)) {
- provider = MarkwonViewHelper.obtainProvider(configurationProvider);
- } else {
- provider = null;
- }
- if (provider != null) {
- setConfigurationProvider(provider);
- }
-
- final String markdown = array.getString(R.styleable.MarkwonView_mv_markdown);
- if (!TextUtils.isEmpty(markdown)) {
- setMarkdown(markdown);
- }
- } finally {
- array.recycle();
+ styles.put(relativeIndex, array.getResourceId(relativeIndex, 0));
}
+
+ final String configurationProvider = array.getString(R.styleable.MarkwonView_mv_configurationProvider);
+ final ConfigurationProvider provider;
+ if (!TextUtils.isEmpty(configurationProvider)) {
+ provider = MarkwonViewHelper.obtainProvider(configurationProvider);
+ } else {
+ provider = null;
+ }
+ if (provider != null) {
+ setConfigurationProvider(provider);
+ }
+
+ final String markdown = array.getString(R.styleable.MarkwonView_mv_markdown);
+ if (!TextUtils.isEmpty(markdown)) {
+ setMarkdown(markdown);
+ }
+ } finally {
+ array.recycle();
}
}
@@ -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 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