diff --git a/markwon-span-ext/.gitignore b/markwon-span-ext/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/markwon-span-ext/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/markwon-span-ext/build.gradle b/markwon-span-ext/build.gradle
new file mode 100644
index 00000000..60a27933
--- /dev/null
+++ b/markwon-span-ext/build.gradle
@@ -0,0 +1,50 @@
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 31
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+
+ api project(':markwon-core')
+ api deps['glide']
+
+
+ deps.with {
+ // add a compileOnly dependency, so if this artifact is present
+ // we will try to obtain a SpanFactory for a Strikethrough node and use
+ // it to be consistent with markdown (please note that we do not use markwon plugin
+ // for that in case if different implementation is used)
+ compileOnly it['commonmark-strikethrough']
+
+ testImplementation it['ix-java']
+ }
+
+ deps.test.with {
+ testImplementation it['junit']
+ testImplementation it['robolectric']
+ }
+}
\ No newline at end of file
diff --git a/markwon-span-ext/src/androidTest/java/io/noties/markwon/span/ext/ExampleInstrumentedTest.java b/markwon-span-ext/src/androidTest/java/io/noties/markwon/span/ext/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..89e367cf
--- /dev/null
+++ b/markwon-span-ext/src/androidTest/java/io/noties/markwon/span/ext/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package io.noties.markwon.span.ext;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("io.noties.markwon.span.ext.test", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/markwon-span-ext/src/main/AndroidManifest.xml b/markwon-span-ext/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fd254356
--- /dev/null
+++ b/markwon-span-ext/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/BlockQuoteSpan.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/BlockQuoteSpan.java
new file mode 100755
index 00000000..40940762
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/BlockQuoteSpan.java
@@ -0,0 +1,82 @@
+package io.noties.markwon.span.ext;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.Layout;
+import android.text.Spanned;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.LineHeightSpan;
+import io.noties.markwon.utils.ColorUtils;
+
+public class BlockQuoteSpan implements LeadingMarginSpan, LineHeightSpan {
+ protected int BLOCK_QUOTE_DEF_COLOR_ALPHA = 100;
+ protected static final int BLOCK_QUOTE_WIDTH = 10;
+ public static int BLOCK_QUOTE_MARGIN = 100;
+ protected int BLOCK_COLOR = Color.LTGRAY;
+ private final int VERTICAL_SPACING = 20;
+ private final Rect rect = ObjectsPool.rect();
+ private final Paint paint = ObjectsPool.paint();
+
+ public BlockQuoteSpan() {
+
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ return BLOCK_QUOTE_MARGIN;
+ }
+
+ @Override
+ public void drawLeadingMargin(
+ Canvas c,
+ Paint p,
+ int x,
+ int dir,
+ int top,
+ int baseline,
+ int bottom,
+ CharSequence text,
+ int start,
+ int end,
+ boolean first,
+ Layout layout) {
+
+ final int width = BLOCK_QUOTE_WIDTH;
+
+ paint.set(p);
+
+ applyBlockQuoteStyle(paint);
+
+ rect.set(x, top, x + dir * width, bottom);
+
+ c.drawRect(rect, paint);
+ }
+
+ public void applyBlockQuoteStyle(Paint paint) {
+ final int color = ColorUtils.applyAlpha(BLOCK_COLOR, BLOCK_QUOTE_DEF_COLOR_ALPHA);
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(color);
+ }
+
+ @Override
+ public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
+ final Spanned txt = (Spanned)text;
+ final int spanEnd = txt.getSpanEnd(this);
+ final int spanStart = txt.getSpanStart(this);
+
+ // add top spacing to first line
+ if (start == spanStart) {
+ fm.ascent -= VERTICAL_SPACING;
+ fm.top -= VERTICAL_SPACING;
+ }
+
+ // add bottom spacing to last line
+ if (Math.abs(spanEnd - end) <= 1) {
+ fm.descent += VERTICAL_SPACING;
+ fm.bottom += VERTICAL_SPACING;
+ }
+ }
+}
+
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/BulletSpan.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/BulletSpan.java
new file mode 100755
index 00000000..150ab427
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/BulletSpan.java
@@ -0,0 +1,188 @@
+package io.noties.markwon.span.ext;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Parcel;
+import android.text.Layout;
+import android.text.ParcelableSpan;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.MetricAffectingSpan;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+
+
+public class BulletSpan implements LeadingMarginSpan, ParcelableSpan {
+ // Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices.
+ private static final int STANDARD_BULLET_RADIUS = 6;
+ public static final int STANDARD_GAP_WIDTH = 2;
+ private static final int STANDARD_COLOR = 0;
+ public static final int BULLET_SPAN = 8;
+ @Px
+ private final int mGapWidth;
+ @Px
+ private final int mBulletRadius;
+ private Path mBulletPath = null;
+ @ColorInt
+ private final int mColor;
+ private final boolean mWantColor;
+
+ /**
+ * Creates a {@link android.text.style.BulletSpan} with the default values.
+ */
+ public BulletSpan() {
+ this(STANDARD_GAP_WIDTH, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
+ }
+
+ /**
+ * Creates a {@link android.text.style.BulletSpan} based on a gap width
+ *
+ * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
+ */
+ public BulletSpan(int gapWidth) {
+ this(gapWidth, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
+ }
+
+ public BulletSpan(int gapWidth, @ColorInt int color) {
+ this(gapWidth, color, true, STANDARD_BULLET_RADIUS);
+ }
+
+ public BulletSpan(int gapWidth, @ColorInt int color, @IntRange(from = 0) int bulletRadius) {
+ this(gapWidth, color, true, bulletRadius);
+ }
+
+ private BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor,
+ @IntRange(from = 0) int bulletRadius) {
+ mGapWidth = gapWidth;
+ mBulletRadius = bulletRadius;
+ mColor = color;
+ mWantColor = wantColor;
+ }
+
+ public BulletSpan(@NonNull Parcel src) {
+ mGapWidth = src.readInt();
+ mWantColor = src.readInt() != 0;
+ mColor = src.readInt();
+ mBulletRadius = src.readInt();
+ }
+
+ @Override
+ public int getSpanTypeId() {
+ return getSpanTypeIdInternal();
+ }
+
+
+ public int getSpanTypeIdInternal() {
+ return BULLET_SPAN;
+ }
+
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ writeToParcelInternal(dest, flags);
+ }
+
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mGapWidth);
+ dest.writeInt(mWantColor ? 1 : 0);
+ dest.writeInt(mColor);
+ dest.writeInt(mBulletRadius);
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ return 2 * mBulletRadius + mGapWidth;
+ }
+
+ /**
+ * Get the distance, in pixels, between the bullet point and the paragraph.
+ *
+ * @return the distance, in pixels, between the bullet point and the paragraph.
+ */
+ public int getGapWidth() {
+ return mGapWidth;
+ }
+
+ /**
+ * Get the radius, in pixels, of the bullet point.
+ *
+ * @return the radius, in pixels, of the bullet point.
+ */
+ public int getBulletRadius() {
+ return mBulletRadius;
+ }
+
+ /**
+ * Get the bullet point color.
+ *
+ * @return the bullet point color
+ */
+ public int getColor() {
+ return mColor;
+ }
+
+ @Override
+ public void drawLeadingMargin(@NonNull Canvas canvas, @NonNull Paint paint, int x, int dir,
+ int top, int baseline, int bottom,
+ @NonNull CharSequence text, int start, int end,
+ boolean first, @Nullable Layout layout) {
+ if (((Spanned) text).getSpanStart(this) == start) {
+ Paint.Style style = paint.getStyle();
+ int oldcolor = 0;
+
+ if (mWantColor) {
+ oldcolor = paint.getColor();
+ paint.setColor(mColor);
+ }
+
+ paint.setStyle(Paint.Style.FILL);
+
+ if (layout != null) {
+ // "bottom" position might include extra space as a result of line spacing
+ // configuration. Subtract extra space in order to show bullet in the vertical
+ // center of characters.
+ final int line = layout.getLineForOffset(start);
+
+ bottom = bottom - 0;//layout.getLineExtra(line);
+ }
+
+ final float yPosition = (top + bottom) / 2f;
+ final float xPosition = x + dir * mBulletRadius;
+
+ if (canvas.isHardwareAccelerated()) {
+ if (mBulletPath == null) {
+ mBulletPath = new Path();
+ mBulletPath.addCircle(0.0f, 0.0f, mBulletRadius, Path.Direction.CW);
+ }
+
+ canvas.save();
+ canvas.translate(xPosition, yPosition);
+ canvas.drawPath(mBulletPath, paint);
+ canvas.restore();
+ } else {
+ canvas.drawCircle(xPosition, yPosition, mBulletRadius, paint);
+ }
+
+ if (mWantColor) {
+ paint.setColor(oldcolor);
+ }
+
+ paint.setStyle(style);
+ }
+ }
+}
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CWDrawableSpan.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CWDrawableSpan.java
new file mode 100755
index 00000000..125afe0a
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CWDrawableSpan.java
@@ -0,0 +1,31 @@
+package io.noties.markwon.span.ext;
+
+import android.graphics.drawable.Drawable;
+import android.text.style.DynamicDrawableSpan;
+
+public class CWDrawableSpan extends DynamicDrawableSpan {
+ private Drawable drawable;
+ private int requiredWidth = 0;
+ private int requiredHeight = 0;
+
+ public CWDrawableSpan(Drawable drawable){
+ this.drawable = drawable;
+ }
+
+ public CWDrawableSpan(Drawable drawable, int requiredWidth, int requiredHeight){
+ this.requiredWidth = requiredWidth;
+ this.requiredHeight = requiredHeight;
+ this.drawable = drawable;
+ }
+
+ @Override
+ public Drawable getDrawable() {
+// drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ if (requiredHeight != 0 && requiredWidth != 0) {
+ drawable.setBounds(0, 0, requiredWidth, requiredHeight);
+ } else {
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ }
+ return drawable;
+ }
+}
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CenteredImageSpan.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CenteredImageSpan.java
new file mode 100755
index 00000000..8ed6513f
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CenteredImageSpan.java
@@ -0,0 +1,55 @@
+package io.noties.markwon.span.ext;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.style.ImageSpan;
+
+import java.lang.ref.WeakReference;
+
+public class CenteredImageSpan extends ImageSpan {
+ private WeakReference mDrawableRef;
+
+ public CenteredImageSpan(Context context, final int drawableRes) {
+ super(context, drawableRes);
+ }
+
+ public CenteredImageSpan(Drawable drawable) {
+ super(drawable);
+ }
+
+ @Override
+ public int getSize(Paint paint, CharSequence text, int start, int end,
+ Paint.FontMetricsInt fontMetricsInt) {
+ Drawable drawable = getDrawable();
+ Rect rect = drawable.getBounds();
+ if (fontMetricsInt != null) {
+ Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
+ int fontHeight = fmPaint.descent - fmPaint.ascent;
+ int drHeight = rect.bottom - rect.top;
+ int centerY = fmPaint.ascent + fontHeight / 2;
+
+ fontMetricsInt.ascent = centerY - drHeight / 2;
+ fontMetricsInt.top = fontMetricsInt.ascent;
+ fontMetricsInt.bottom = centerY + drHeight / 2;
+ fontMetricsInt.descent = fontMetricsInt.bottom;
+ }
+ return rect.right;
+ }
+ @Override
+ public void draw(Canvas canvas, CharSequence text, int start, int end,
+ float x, int top, int y, int bottom, Paint paint) {
+
+ Drawable drawable = getDrawable();
+ canvas.save();
+ Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
+ int fontHeight = fmPaint.descent - fmPaint.ascent;
+ int centerY = y + fmPaint.descent - fontHeight / 2;
+ int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2;
+ canvas.translate(x, transY);
+ drawable.draw(canvas);
+ canvas.restore();
+ }
+}
\ No newline at end of file
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CodeBlockSpan.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CodeBlockSpan.java
new file mode 100755
index 00000000..28cdc0cb
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/CodeBlockSpan.java
@@ -0,0 +1,126 @@
+package io.noties.markwon.span.ext;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.text.Layout;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.LineHeightSpan;
+import android.text.style.MetricAffectingSpan;
+
+public class CodeBlockSpan extends MetricAffectingSpan implements LeadingMarginSpan, LineHeightSpan {
+ protected static final float CODE_DEF_TEXT_SIZE_RATIO = 1.0F;
+ protected static final int DEFAULT_LEADING_MARGIN = 20;
+ protected int BACKGROUND_COLOR;
+ protected int TEXT_COLOR;
+
+ protected static final int CORNER_RADIUS = 15;
+ private final int VERTICAL_SPACING = 40;
+
+ private final Rect rect = ObjectsPool.rect();
+ private final Paint paint = ObjectsPool.paint();
+
+ public CodeBlockSpan(String theme) {
+ if(theme.equalsIgnoreCase("dark")){
+ BACKGROUND_COLOR = Color.argb(255, 25, 26, 27);
+ TEXT_COLOR = Color.WHITE;
+ }else{
+ BACKGROUND_COLOR = Color.argb(255, 246, 246, 246);
+ TEXT_COLOR = Color.BLACK;
+ }
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint p) {
+ apply(p);
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds);
+ }
+
+ private void apply(TextPaint p) {
+ final int textColor = TEXT_COLOR;
+
+ if (textColor != 0) {
+ paint.setColor(textColor);
+ }
+
+ paint.setTypeface(Typeface.MONOSPACE);
+
+ final int textSize = 0;
+
+ if (textSize > 0) {
+ paint.setTextSize(textSize);
+ } else {
+ // calculate default value
+ paint.setTextSize(paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO);
+ }
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ return DEFAULT_LEADING_MARGIN;
+ }
+
+ @Override
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
+
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(BACKGROUND_COLOR);
+
+ final int left;
+ final int right;
+ if (dir > 0) {
+ left = x;
+ right = c.getWidth();
+ } else {
+ left = x - c.getWidth();
+ right = x;
+ }
+
+ rect.set(left, top, right, bottom);
+
+ final Spanned txt = (Spanned)text;
+ final int spanEnd = txt.getSpanEnd(this);
+ final int spanStart = txt.getSpanStart(this);
+
+ // draw rounded corner background
+ if (start == spanStart) {
+ c.drawRoundRect(new RectF(rect), CORNER_RADIUS, CORNER_RADIUS, paint);
+ c.drawRect(new Rect(left, top + CORNER_RADIUS, right, bottom), paint);
+ }
+ else if (Math.abs(spanEnd - end) <= 1) {
+ c.drawRoundRect(new RectF(rect), CORNER_RADIUS, CORNER_RADIUS, paint);
+ c.drawRect(new Rect(left, top, right, bottom - CORNER_RADIUS), paint);
+ }
+ else {
+ c.drawRect(rect, paint);
+ }
+ }
+
+ @Override
+ public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
+ final Spanned txt = (Spanned)text;
+ final int spanEnd = txt.getSpanEnd(this);
+ final int spanStart = txt.getSpanStart(this);
+
+ // add top spacing to first line
+ if (start == spanStart) {
+ fm.ascent -= VERTICAL_SPACING;
+ fm.top -= VERTICAL_SPACING;
+ }
+
+ // add bottom spacing to last line
+ if (Math.abs(spanEnd - end) <= 1) {
+ fm.descent += VERTICAL_SPACING;
+ fm.bottom += VERTICAL_SPACING;
+ }
+ }
+}
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/GifDrawableSpan.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/GifDrawableSpan.java
new file mode 100755
index 00000000..817d157c
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/GifDrawableSpan.java
@@ -0,0 +1,33 @@
+package io.noties.markwon.span.ext;
+
+import android.graphics.drawable.Drawable;
+import android.text.style.DynamicDrawableSpan;
+
+import com.bumptech.glide.load.resource.gif.GifDrawable;
+
+public class GifDrawableSpan extends DynamicDrawableSpan {
+ private GifDrawable drawable;
+ private int requiredWidth = 0;
+ private int requiredHeight = 0;
+
+ public GifDrawableSpan(GifDrawable drawable){
+ this.drawable = drawable;
+ }
+
+ public GifDrawableSpan(GifDrawable drawable, int requiredWidth, int requiredHeight){
+ this.requiredWidth = requiredWidth;
+ this.requiredHeight = requiredHeight;
+ this.drawable = drawable;
+ }
+
+ @Override
+ public Drawable getDrawable() {
+// drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ if (requiredHeight != 0 && requiredWidth != 0) {
+ drawable.setBounds(0, 0, requiredWidth, requiredHeight);
+ } else {
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ }
+ return drawable;
+ }
+}
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/HashtagSpan.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/HashtagSpan.java
new file mode 100644
index 00000000..eef67043
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/HashtagSpan.java
@@ -0,0 +1,48 @@
+package io.noties.markwon.span.ext;
+
+import android.text.TextPaint;
+import android.text.style.URLSpan;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import io.noties.markwon.LinkResolver;
+
+public class HashtagSpan extends URLSpan {
+ private final String link;
+ private final LinkResolver resolver;
+ private final int linkColor;
+
+ public HashtagSpan(
+ @NonNull String link,
+ @NonNull int linkColor,
+ @NonNull LinkResolver resolver) {
+ super(link);
+ this.link = link;
+ this.linkColor = linkColor;
+ this.resolver = resolver;
+ }
+
+ @Override
+ public void onClick(View widget) {
+ resolver.resolve(widget, link);
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint ds) {
+ applyLinkStyle(ds);
+ }
+
+ private void applyLinkStyle(TextPaint paint){
+ paint.setUnderlineText(true);
+ paint.setColor(linkColor);
+ }
+
+ /**
+ * @since 4.2.0
+ */
+ @NonNull
+ public String getLink() {
+ return link;
+ }
+}
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/NumberedSpan.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/NumberedSpan.java
new file mode 100644
index 00000000..c2d9cd59
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/NumberedSpan.java
@@ -0,0 +1,58 @@
+package io.noties.markwon.span.ext;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.Layout;
+import android.text.Spanned;
+import android.text.style.LeadingMarginSpan;
+
+class NumberedSpan implements LeadingMarginSpan {
+ private final int NUMBER_GAP = 14;
+ private final int mIndex;
+ private final String mTheme;
+ private final Paint paint = ObjectsPool.paint();
+
+ NumberedSpan(int index, String theme) {
+ mIndex = index;
+ mTheme = theme;
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ final String numText = mIndex + ".";
+ final Rect textBounds = new Rect();
+ paint.getTextBounds(numText, 0, numText.length(), textBounds);
+ return textBounds.width() + NUMBER_GAP;
+ }
+
+ @Override
+ public void drawLeadingMargin(Canvas canvas, Paint paint, int x, int dir, int top, int baseline,
+ int bottom, CharSequence text, int start, int end, boolean first,
+ Layout layout) {
+ // add number to first line
+ if (((Spanned)text).getSpanStart(this) == start) {
+ // save previous paint values
+ final Paint.Style prevStyle = paint.getStyle();
+ final int prevColor = paint.getColor();
+
+ // draw number
+ paint.setStyle(Paint.Style.FILL);
+ if(mTheme.equalsIgnoreCase("dark")){
+ paint.setColor(Color.WHITE);
+ }else{
+ paint.setColor(Color.BLACK);
+ }
+ final String numText = mIndex + ".";
+ final Rect textBounds = new Rect();
+ paint.getTextBounds(numText, 0, numText.length(), textBounds);
+ final float yVal = (top + bottom + textBounds.height()) / 2f;
+ canvas.drawText(numText, 0, numText.length(), x, yVal, paint);
+
+ // reset modified paint values
+ paint.setStyle(prevStyle);
+ paint.setColor(prevColor);
+ }
+ }
+}
diff --git a/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/ObjectsPool.java b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/ObjectsPool.java
new file mode 100755
index 00000000..ce7945d8
--- /dev/null
+++ b/markwon-span-ext/src/main/java/io/noties/markwon/span/ext/ObjectsPool.java
@@ -0,0 +1,33 @@
+package io.noties.markwon.span.ext;
+
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+abstract class ObjectsPool {
+
+ // maybe it's premature optimization, but as all the drawing is done in one thread
+ // and we apply needed values before actual drawing it's (I assume) safe to reuse some frequently used objects
+
+ // if one of the spans need some really specific handling for Paint object (like colorFilters, masks, etc)
+ // it should instantiate own instance of it
+
+ private static final Rect RECT = new Rect();
+ private static final RectF RECT_F = new RectF();
+ private static final Paint PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ static Rect rect() {
+ return RECT;
+ }
+
+ static RectF rectF() {
+ return RECT_F;
+ }
+
+ static Paint paint() {
+ return PAINT;
+ }
+
+ private ObjectsPool() {
+ }
+}
diff --git a/markwon-span-ext/src/test/java/io/noties/markwon/span/ext/ExampleUnitTest.java b/markwon-span-ext/src/test/java/io/noties/markwon/span/ext/ExampleUnitTest.java
new file mode 100644
index 00000000..8aa28c02
--- /dev/null
+++ b/markwon-span-ext/src/test/java/io/noties/markwon/span/ext/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package io.noties.markwon.span.ext;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file