diff --git a/CHANGELOG.md b/CHANGELOG.md
index a536f2c1..886bd169 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+# 4.3.1
+* Fix DexGuard optimization issue ([#216])
Thanks [@francescocervone]
+* module `images`: `GifSupport` and `SvgSupport` use `Class.forName` instead access to full qualified class name
+* `ext-table`: fix links in tables ([#224])
+* `ext-table`: proper borders (equal for all sides)
+* module `core`: Add `PrecomputedFutureTextSetterCompat`
Thanks [@KirkBushman]
+
+[#216]: https://github.com/noties/Markwon/pull/216
+[#224]: https://github.com/noties/Markwon/issues/224
+[@francescocervone]: https://github.com/francescocervone
+[@KirkBushman]: https://github.com/KirkBushman
+
+
# 4.3.0
* add `MarkwonInlineParserPlugin` in `inline-parser` module
* `JLatexMathPlugin` now supports inline LaTeX structures via `MarkwonInlineParserPlugin`
@@ -12,8 +25,7 @@ dependency (must be explicitly added to `Markwon` whilst configuring)
* `LinkResolverDef` defaults to `https` when a link does not have scheme information ([#75])
* add `option` abstraction for `sample` module allowing switching of multiple cases in runtime via menu
* non-empty bounds for `AsyncDrawable` when no dimensions are not yet available ([#189])
-* `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201])
-
Thanks to [@drakeet]
+* `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201])
Thanks to [@drakeet]
* `MarkwonVisitor.BlockHandler` and `BlockHandlerDef` implementation to control how blocks insert new lines after them
diff --git a/app/src/debug/java/io/noties/markwon/debug/ColorBlendView.java b/app/src/debug/java/io/noties/markwon/debug/ColorBlendView.java
new file mode 100644
index 00000000..baf5e92b
--- /dev/null
+++ b/app/src/debug/java/io/noties/markwon/debug/ColorBlendView.java
@@ -0,0 +1,59 @@
+package io.noties.markwon.debug;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import io.noties.markwon.app.R;
+import io.noties.markwon.utils.ColorUtils;
+
+public class ColorBlendView extends View {
+
+ private final Rect rect = new Rect();
+ private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private int background;
+ private int foreground;
+
+ public ColorBlendView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ if (attrs != null) {
+ final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ColorBlendView);
+ try {
+ background = array.getColor(R.styleable.ColorBlendView_cbv_background, 0);
+ foreground = array.getColor(R.styleable.ColorBlendView_cbv_foreground, 0);
+ } finally {
+ array.recycle();
+ }
+ }
+
+ paint.setStyle(Paint.Style.FILL);
+
+ setWillNotDraw(false);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ final int side = getWidth() / 11;
+
+ rect.set(0, 0, side, getHeight());
+
+ canvas.translate(getPaddingLeft(), 0F);
+
+ for (int i = 0; i < 11; i++) {
+ final float alpha = i / 10F;
+ paint.setColor(ColorUtils.blend(foreground, background, alpha));
+ canvas.drawRect(rect, paint);
+ canvas.translate(side, 0F);
+ }
+ }
+}
diff --git a/app/src/debug/res/layout/debug_color_blend.xml b/app/src/debug/res/layout/debug_color_blend.xml
new file mode 100644
index 00000000..cacc73fa
--- /dev/null
+++ b/app/src/debug/res/layout/debug_color_blend.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/debug/res/values/attrs.xml b/app/src/debug/res/values/attrs.xml
index 161f7806..0636e107 100644
--- a/app/src/debug/res/values/attrs.xml
+++ b/app/src/debug/res/values/attrs.xml
@@ -8,4 +8,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 43348c63..51aa9d6c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -66,6 +66,7 @@ ext {
'x-annotations' : 'androidx.annotation:annotation:1.1.0',
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
'x-core' : 'androidx.core:core:1.0.2',
+ 'x-appcompat' : 'androidx.appcompat:appcompat:1.1.0',
'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion",
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",
diff --git a/gradle.properties b/gradle.properties
index f74f7775..0e222c26 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -8,7 +8,7 @@ android.enableJetifier=true
android.enableBuildCache=true
android.buildCacheDir=build/pre-dex-cache
-VERSION_NAME=4.3.0
+VERSION_NAME=4.3.1
GROUP=io.noties.markwon
POM_DESCRIPTION=Markwon markdown for Android
diff --git a/markwon-core/build.gradle b/markwon-core/build.gradle
index 6ff67d77..8b2f1be6 100644
--- a/markwon-core/build.gradle
+++ b/markwon-core/build.gradle
@@ -22,6 +22,7 @@ dependencies {
// @since 4.1.0 to allow PrecomputedTextSetterCompat
// note that this dependency must be added on a client side explicitly
compileOnly it['x-core']
+ compileOnly it['x-appcompat']
}
deps['test'].with {
diff --git a/markwon-core/src/main/java/io/noties/markwon/PrecomputedFutureTextSetterCompat.java b/markwon-core/src/main/java/io/noties/markwon/PrecomputedFutureTextSetterCompat.java
new file mode 100644
index 00000000..f4601286
--- /dev/null
+++ b/markwon-core/src/main/java/io/noties/markwon/PrecomputedFutureTextSetterCompat.java
@@ -0,0 +1,65 @@
+package io.noties.markwon;
+
+import android.text.Spanned;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.text.PrecomputedTextCompat;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+
+/**
+ * Please note this class requires `androidx.core:core` artifact being explicitly added to your dependencies.
+ * This is intended to be used in a RecyclerView.
+ *
+ * @see io.noties.markwon.Markwon.TextSetter
+ * @since 4.3.1
+ */
+public class PrecomputedFutureTextSetterCompat implements Markwon.TextSetter {
+
+ /**
+ * @param executor for background execution of text pre-computation,
+ * if not provided the standard, single threaded one will be used.
+ */
+ @NonNull
+ public static PrecomputedFutureTextSetterCompat create(@Nullable Executor executor) {
+ return new PrecomputedFutureTextSetterCompat(executor);
+ }
+
+ @NonNull
+ public static PrecomputedFutureTextSetterCompat create() {
+ return new PrecomputedFutureTextSetterCompat(null);
+ }
+
+ @Nullable
+ private final Executor executor;
+
+ @SuppressWarnings("WeakerAccess")
+ PrecomputedFutureTextSetterCompat(@Nullable Executor executor) {
+ this.executor = executor;
+ }
+
+ @Override
+ public void setText(
+ @NonNull TextView textView,
+ @NonNull Spanned markdown,
+ @NonNull TextView.BufferType bufferType,
+ @NonNull Runnable onComplete) {
+ if (textView instanceof AppCompatTextView) {
+ final AppCompatTextView appCompatTextView = (AppCompatTextView) textView;
+ final Future future = PrecomputedTextCompat.getTextFuture(
+ markdown,
+ appCompatTextView.getTextMetricsParamsCompat(),
+ executor);
+ appCompatTextView.setTextFuture(future);
+ // `setTextFuture` is actually a synchronous call, so we should call onComplete now
+ onComplete.run();
+ } else {
+ throw new IllegalStateException("TextView provided is not an instance of AppCompatTextView, " +
+ "cannot call setTextFuture(), textView: " + textView);
+ }
+ }
+}
diff --git a/markwon-core/src/main/java/io/noties/markwon/utils/ColorUtils.java b/markwon-core/src/main/java/io/noties/markwon/utils/ColorUtils.java
index d2a8bc6e..d5d9703d 100644
--- a/markwon-core/src/main/java/io/noties/markwon/utils/ColorUtils.java
+++ b/markwon-core/src/main/java/io/noties/markwon/utils/ColorUtils.java
@@ -1,11 +1,33 @@
package io.noties.markwon.utils;
+import android.graphics.Color;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.FloatRange;
+import androidx.annotation.IntRange;
+
public abstract class ColorUtils {
- public static int applyAlpha(int color, int alpha) {
+ @ColorInt
+ public static int applyAlpha(
+ @ColorInt int color,
+ @IntRange(from = 0, to = 255) int alpha) {
return (color & 0x00FFFFFF) | (alpha << 24);
}
+ // blend two colors w/ specified ratio, resulting color won't have alpha channel
+ @ColorInt
+ public static int blend(
+ @ColorInt int foreground,
+ @ColorInt int background,
+ @FloatRange(from = 0.0F, to = 1.0F) float ratio) {
+ return Color.rgb(
+ (int) (((1F - ratio) * Color.red(foreground)) + (ratio * Color.red(background))),
+ (int) (((1F - ratio) * Color.green(foreground)) + (ratio * Color.green(background))),
+ (int) (((1F - ratio) * Color.blue(foreground)) + (ratio * Color.blue(background)))
+ );
+ }
+
private ColorUtils() {
}
}
diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TablePlugin.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TablePlugin.java
index 0cbce8b8..d5d0f74f 100644
--- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TablePlugin.java
+++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TablePlugin.java
@@ -123,12 +123,13 @@ public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableBlock tableBloc
visitor.blockStart(tableBlock);
+ final int length = visitor.length();
+
visitor.visitChildren(tableBlock);
-// if (visitor.hasNext(tableBlock)) {
-// visitor.ensureNewLine();
-// visitor.forceNewLine();
-// }
+ // @since 4.3.1 apply table span for the full table
+ visitor.setSpans(length, new TableSpan());
+
visitor.blockEnd(tableBlock);
}
})
diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java
index 6cbdcae4..0248c4ef 100644
--- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java
+++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java
@@ -5,6 +5,7 @@
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Layout;
+import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.style.ReplacementSpan;
@@ -19,6 +20,8 @@
import java.util.ArrayList;
import java.util.List;
+import io.noties.markwon.utils.LeadingMarginUtils;
+
public class TableRowSpan extends ReplacementSpan {
public static final int ALIGN_LEFT = 0;
@@ -139,11 +142,17 @@ public void draw(
int top,
int y,
int bottom,
- @NonNull Paint paint) {
+ @NonNull Paint p) {
if (recreateLayouts(canvas.getWidth())) {
width = canvas.getWidth();
- textPaint.set(paint);
+ // @since 4.3.1 it's important to cast to TextPaint in order to display links, etc
+ if (p instanceof TextPaint) {
+ // there must be a reason why this method receives Paint instead of TextPaint...
+ textPaint.set((TextPaint) p);
+ } else {
+ textPaint.set(p);
+ }
makeNewLayouts();
}
@@ -155,28 +164,25 @@ public void draw(
final int w = width / size;
- // feels like magic...
- final int heightDiff = (bottom - top - height) / 4;
-
// @since 1.1.1
// draw backgrounds
{
if (header) {
- theme.applyTableHeaderRowStyle(this.paint);
+ theme.applyTableHeaderRowStyle(paint);
} else if (odd) {
- theme.applyTableOddRowStyle(this.paint);
+ theme.applyTableOddRowStyle(paint);
} else {
// even
- theme.applyTableEvenRowStyle(this.paint);
+ theme.applyTableEvenRowStyle(paint);
}
// if present (0 is transparent)
- if (this.paint.getColor() != 0) {
+ if (paint.getColor() != 0) {
final int save = canvas.save();
try {
rect.set(0, 0, width, bottom - top);
- canvas.translate(x, top - heightDiff);
- canvas.drawRect(rect, this.paint);
+ canvas.translate(x, top);
+ canvas.drawRect(rect, paint);
} finally {
canvas.restoreToCount(save);
}
@@ -186,25 +192,73 @@ public void draw(
// @since 1.1.1 reset after applying background color
// as background changes color attribute and if not specific tableBorderColor
// is specified then after this row all borders will have color of this row (plus alpha)
- this.paint.set(paint);
- theme.applyTableBorderStyle(this.paint);
+ paint.set(p);
+ theme.applyTableBorderStyle(paint);
final int borderWidth = theme.tableBorderWidth(paint);
final boolean drawBorder = borderWidth > 0;
+
+ // why divided by 4 gives a more or less good result is still not clear (shouldn't it be 2?)
+ final int heightDiff = (bottom - top - height) / 4;
+
+ // required for borderTop calculation
+ final boolean isFirstTableRow;
+
+ // @since 4.3.1
if (drawBorder) {
- rect.set(0, 0, w, bottom - top);
+ boolean first = false;
+ // only if first draw the line
+ {
+ final Spanned spanned = (Spanned) text;
+ final TableSpan[] spans = spanned.getSpans(start, end, TableSpan.class);
+ if (spans != null && spans.length > 0) {
+ final TableSpan span = spans[0];
+ if (LeadingMarginUtils.selfStart(start, text, span)) {
+ first = true;
+ rect.set((int) x, top, width, top + borderWidth);
+ canvas.drawRect(rect, paint);
+ }
+ }
+ }
+
+ // draw the line at the bottom
+ rect.set((int) x, bottom - borderWidth, width, bottom);
+ canvas.drawRect(rect, paint);
+
+ isFirstTableRow = first;
+ } else {
+ isFirstTableRow = false;
}
+ final int borderWidthHalf = borderWidth / 2;
+
+ // to NOT overlap borders inset top and bottom
+ final int borderTop = isFirstTableRow ? borderWidth : 0;
+ final int borderBottom = bottom - top - borderWidth;
+
StaticLayout layout;
for (int i = 0; i < size; i++) {
layout = layouts.get(i);
final int save = canvas.save();
try {
- canvas.translate(x + (i * w), top - heightDiff);
+ canvas.translate(x + (i * w), top);
+ // @since 4.3.1
if (drawBorder) {
- canvas.drawRect(rect, this.paint);
+ // first vertical border will have full width (it cannot exceed canvas)
+ if (i == 0) {
+ rect.set(0, borderTop, borderWidth, borderBottom);
+ } else {
+ rect.set(-borderWidthHalf, borderTop, borderWidthHalf, borderBottom);
+ }
+
+ canvas.drawRect(rect, paint);
+
+ if (i == (size - 1)) {
+ rect.set(w - borderWidth, borderTop, w, borderBottom);
+ canvas.drawRect(rect, paint);
+ }
}
canvas.translate(padding, padding + heightDiff);
diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableSpan.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableSpan.java
new file mode 100644
index 00000000..4f7f1aee
--- /dev/null
+++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableSpan.java
@@ -0,0 +1,7 @@
+package io.noties.markwon.ext.tables;
+
+/**
+ * @since 4.3.1
+ */
+public class TableSpan {
+}
diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableTheme.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableTheme.java
index f4a1c5cc..1f1c4dd1 100644
--- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableTheme.java
+++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableTheme.java
@@ -10,6 +10,7 @@
import io.noties.markwon.utils.ColorUtils;
import io.noties.markwon.utils.Dip;
+@SuppressWarnings("WeakerAccess")
public class TableTheme {
@NonNull
@@ -101,7 +102,8 @@ public void applyTableBorderStyle(@NonNull Paint paint) {
}
paint.setColor(color);
- paint.setStyle(Paint.Style.STROKE);
+ // @since 4.3.1 before it was STROKE... change to FILL as we draw border differently
+ paint.setStyle(Paint.Style.FILL);
}
public void applyTableOddRowStyle(@NonNull Paint paint) {
diff --git a/markwon-html/src/main/java/io/noties/markwon/html/tag/StrikeHandler.java b/markwon-html/src/main/java/io/noties/markwon/html/tag/StrikeHandler.java
index 6c01e31f..03a0e952 100644
--- a/markwon-html/src/main/java/io/noties/markwon/html/tag/StrikeHandler.java
+++ b/markwon-html/src/main/java/io/noties/markwon/html/tag/StrikeHandler.java
@@ -25,7 +25,9 @@ public class StrikeHandler extends TagHandler {
static {
boolean hasMarkdownImplementation;
try {
- org.commonmark.ext.gfm.strikethrough.Strikethrough.class.getName();
+ // @since 4.3.1 we class Class.forName instead of trying
+ // to access the class by full qualified name (which caused issues with DexGuard)
+ Class.forName("org.commonmark.ext.gfm.strikethrough.Strikethrough");
hasMarkdownImplementation = true;
} catch (Throwable t) {
hasMarkdownImplementation = false;
diff --git a/markwon-image/src/main/java/io/noties/markwon/image/gif/GifSupport.java b/markwon-image/src/main/java/io/noties/markwon/image/gif/GifSupport.java
index 70a143fc..2e9406ba 100644
--- a/markwon-image/src/main/java/io/noties/markwon/image/gif/GifSupport.java
+++ b/markwon-image/src/main/java/io/noties/markwon/image/gif/GifSupport.java
@@ -14,7 +14,8 @@ public abstract class GifSupport {
static {
boolean result;
try {
- pl.droidsonroids.gif.GifDrawable.class.getName();
+ // @since 4.3.1
+ Class.forName("pl.droidsonroids.gif.GifDrawable");
result = true;
} catch (Throwable t) {
// @since 4.1.1 instead of printing full stacktrace of the exception,
diff --git a/markwon-image/src/main/java/io/noties/markwon/image/svg/SvgSupport.java b/markwon-image/src/main/java/io/noties/markwon/image/svg/SvgSupport.java
index 4afe2506..aea6ffb5 100644
--- a/markwon-image/src/main/java/io/noties/markwon/image/svg/SvgSupport.java
+++ b/markwon-image/src/main/java/io/noties/markwon/image/svg/SvgSupport.java
@@ -14,7 +14,7 @@ public abstract class SvgSupport {
static {
boolean result;
try {
- com.caverock.androidsvg.SVG.class.getName();
+ Class.forName("com.caverock.androidsvg.SVG");
result = true;
} catch (Throwable t) {
// @since 4.1.1 instead of printing full stacktrace of the exception,
diff --git a/sample/build.gradle b/sample/build.gradle
index 595fd54b..b87cab5e 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -54,6 +54,7 @@ dependencies {
deps.with {
implementation it['x-recycler-view']
implementation it['x-core'] // for precomputedTextCompat
+ implementation it['x-appcompat'] // for setTextFuture
implementation it['okhttp']
implementation it['prism4j']
implementation it['debug']
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index f85d8750..cda45b5d 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -28,6 +28,7 @@
+
+
diff --git a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java
index 2ac55d99..beeda582 100644
--- a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java
@@ -30,8 +30,10 @@
import io.noties.markwon.sample.latex.LatexActivity;
import io.noties.markwon.sample.notification.NotificationActivity;
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
+import io.noties.markwon.sample.precomputed.PrecomputedFutureActivity;
import io.noties.markwon.sample.recycler.RecyclerActivity;
import io.noties.markwon.sample.simpleext.SimpleExtActivity;
+import io.noties.markwon.sample.table.TableActivity;
import io.noties.markwon.sample.tasklist.TaskListActivity;
public class MainActivity extends Activity {
@@ -123,6 +125,10 @@ static Intent sampleItemIntent(@NonNull Context context, @NonNull Sample item) {
activity = PrecomputedActivity.class;
break;
+ case PRECOMPUTED_FUTURE_TEXT:
+ activity = PrecomputedFutureActivity.class;
+ break;
+
case EDITOR:
activity = EditorActivity.class;
break;
@@ -147,6 +153,10 @@ static Intent sampleItemIntent(@NonNull Context context, @NonNull Sample item) {
activity = NotificationActivity.class;
break;
+ case TABLE:
+ activity = TableActivity.class;
+ break;
+
default:
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
}
diff --git a/sample/src/main/java/io/noties/markwon/sample/Sample.java b/sample/src/main/java/io/noties/markwon/sample/Sample.java
index f18ed25b..2dfcc0be 100644
--- a/sample/src/main/java/io/noties/markwon/sample/Sample.java
+++ b/sample/src/main/java/io/noties/markwon/sample/Sample.java
@@ -23,6 +23,8 @@ public enum Sample {
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
+ PRECOMPUTED_FUTURE_TEXT(R.string.sample_precomputed_future_text),
+
EDITOR(R.string.sample_editor),
INLINE_PARSER(R.string.sample_inline_parser),
@@ -33,7 +35,9 @@ public enum Sample {
IMAGES(R.string.sample_images),
- REMOTE_VIEWS(R.string.sample_remote_views);
+ REMOTE_VIEWS(R.string.sample_remote_views),
+
+ TABLE(R.string.sample_table);
private final int textResId;
diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java
new file mode 100644
index 00000000..2471cd4f
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java
@@ -0,0 +1,97 @@
+package io.noties.markwon.sample.basicplugins;
+
+import android.text.Spannable;
+import android.text.Spanned;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import io.noties.markwon.AbstractMarkwonPlugin;
+import io.noties.markwon.LinkResolverDef;
+import io.noties.markwon.MarkwonConfiguration;
+import io.noties.markwon.core.spans.HeadingSpan;
+
+public class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
+
+ public interface ScrollTo {
+ void scrollTo(@NonNull TextView view, int top);
+ }
+
+ private final ScrollTo scrollTo;
+
+ AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) {
+ this.scrollTo = scrollTo;
+ }
+
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.linkResolver(new AnchorLinkResolver(scrollTo));
+ }
+
+ @Override
+ public void afterSetText(@NonNull TextView textView) {
+ final Spannable spannable = (Spannable) textView.getText();
+ // obtain heading spans
+ final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class);
+ if (spans != null) {
+ for (HeadingSpan span : spans) {
+ final int start = spannable.getSpanStart(span);
+ final int end = spannable.getSpanEnd(span);
+ final int flags = spannable.getSpanFlags(span);
+ spannable.setSpan(
+ new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
+ start,
+ end,
+ flags
+ );
+ }
+ }
+ }
+
+ private static class AnchorLinkResolver extends LinkResolverDef {
+
+ private final ScrollTo scrollTo;
+
+ AnchorLinkResolver(@NonNull ScrollTo scrollTo) {
+ this.scrollTo = scrollTo;
+ }
+
+ @Override
+ public void resolve(@NonNull View view, @NonNull String link) {
+ if (link.startsWith("#")) {
+ final TextView textView = (TextView) view;
+ final Spanned spanned = (Spannable) textView.getText();
+ final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class);
+ if (spans != null) {
+ final String anchor = link.substring(1);
+ for (AnchorSpan span : spans) {
+ if (anchor.equals(span.anchor)) {
+ final int start = spanned.getSpanStart(span);
+ final int line = textView.getLayout().getLineForOffset(start);
+ final int top = textView.getLayout().getLineTop(line);
+ scrollTo.scrollTo(textView, top);
+ return;
+ }
+ }
+ }
+ }
+ super.resolve(view, link);
+ }
+ }
+
+ private static class AnchorSpan {
+ final String anchor;
+
+ AnchorSpan(@NonNull String anchor) {
+ this.anchor = anchor;
+ }
+ }
+
+ @NonNull
+ public static String createAnchor(@NonNull CharSequence content) {
+ return String.valueOf(content)
+ .replaceAll("[^\\w]", "")
+ .toLowerCase();
+ }
+}
diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java
index e8bb4761..9b9ffdde 100644
--- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java
@@ -3,11 +3,8 @@
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
-import android.text.Spannable;
-import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
-import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
@@ -23,7 +20,6 @@
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef;
-import io.noties.markwon.LinkResolverDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonSpansFactory;
@@ -31,7 +27,6 @@
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme;
-import io.noties.markwon.core.spans.HeadingSpan;
import io.noties.markwon.core.spans.LastLineSpacingSpan;
import io.noties.markwon.image.ImageItem;
import io.noties.markwon.image.ImagesPlugin;
@@ -62,7 +57,9 @@ public MenuOptions menuOptions() {
.add("headingNoSpace", this::headingNoSpace)
.add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler)
.add("allBlocksNoForcedLine", this::allBlocksNoForcedLine)
- .add("anchor", this::anchor);
+ .add("anchor", this::anchor)
+ .add("letterOrderedList", this::letterOrderedList)
+ .add("tableOfContents", this::tableOfContents);
}
@Override
@@ -323,26 +320,26 @@ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
}
private void headingNoSpaceBlockHandler() {
-final Markwon markwon = Markwon.builder(this)
- .usePlugin(new AbstractMarkwonPlugin() {
- @Override
- public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
- builder.blockHandler(new BlockHandlerDef() {
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
@Override
- public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
- if (node instanceof Heading) {
- if (visitor.hasNext(node)) {
- visitor.ensureNewLine();
- // ensure new line but do not force insert one
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.blockHandler(new BlockHandlerDef() {
+ @Override
+ public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
+ if (node instanceof Heading) {
+ if (visitor.hasNext(node)) {
+ visitor.ensureNewLine();
+ // ensure new line but do not force insert one
+ }
+ } else {
+ super.blockEnd(visitor, node);
+ }
}
- } else {
- super.blockEnd(visitor, node);
- }
+ });
}
- });
- }
- })
- .build();
+ })
+ .build();
final String md = "" +
"# Title title title title title title title title title title \n\ntext text text text";
@@ -384,85 +381,6 @@ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
markwon.setMarkdown(textView, md);
}
-// public void step_6() {
-//
-// final Markwon markwon = Markwon.builder(this)
-// .usePlugin(HtmlPlugin.create())
-// .usePlugin(new AbstractMarkwonPlugin() {
-// @Override
-// public void configure(@NonNull Registry registry) {
-// registry.require(HtmlPlugin.class, plugin -> plugin.addHandler(new SimpleTagHandler() {
-// @Override
-// public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
-// return new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
-// }
-//
-// @NonNull
-// @Override
-// public Collection supportedTags() {
-// return Collections.singleton("center");
-// }
-// }));
-// }
-// })
-// .build();
-// }
-
- // text lifecycle (after/before)
- // rendering lifecycle (before/after)
- // renderProps
- // process
-
- private static class AnchorSpan {
- final String anchor;
-
- AnchorSpan(@NonNull String anchor) {
- this.anchor = anchor;
- }
- }
-
- @NonNull
- private String createAnchor(@NonNull CharSequence content) {
- return String.valueOf(content)
- .replaceAll("[^\\w]", "")
- .toLowerCase();
- }
-
- private static class AnchorLinkResolver extends LinkResolverDef {
-
- interface ScrollTo {
- void scrollTo(@NonNull View view, int top);
- }
-
- private final ScrollTo scrollTo;
-
- AnchorLinkResolver(@NonNull ScrollTo scrollTo) {
- this.scrollTo = scrollTo;
- }
-
- @Override
- public void resolve(@NonNull View view, @NonNull String link) {
- if (link.startsWith("#")) {
- final TextView textView = (TextView) view;
- final Spanned spanned = (Spannable) textView.getText();
- final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class);
- if (spans != null) {
- final String anchor = link.substring(1);
- for (AnchorSpan span: spans) {
- if (anchor.equals(span.anchor)) {
- final int start = spanned.getSpanStart(span);
- final int line = textView.getLayout().getLineForOffset(start);
- final int top = textView.getLayout().getLineTop(line);
- scrollTo.scrollTo(textView, top);
- return;
- }
- }
- }
- }
- super.resolve(view, link);
- }
- }
-
private void anchor() {
final String lorem = getString(R.string.lorem);
final String md = "" +
@@ -472,32 +390,46 @@ private void anchor() {
lorem;
final Markwon markwon = Markwon.builder(this)
- .usePlugin(new AbstractMarkwonPlugin() {
- @Override
- public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
- builder.linkResolver(new AnchorLinkResolver((view, top) -> scrollView.smoothScrollTo(0, top)));
- }
+ .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
+ .build();
- @Override
- public void afterSetText(@NonNull TextView textView) {
- final Spannable spannable = (Spannable) textView.getText();
- // obtain heading spans
- final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class);
- if (spans != null) {
- for (HeadingSpan span : spans) {
- final int start = spannable.getSpanStart(span);
- final int end = spannable.getSpanEnd(span);
- final int flags = spannable.getSpanFlags(span);
- spannable.setSpan(
- new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
- start,
- end,
- flags
- );
- }
- }
- }
- })
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void letterOrderedList() {
+ // bullet list nested in ordered list renders letters instead of bullets
+ final String md = "" +
+ "1. Hello there!\n" +
+ "1. And here is how:\n" +
+ " - First\n" +
+ " - Second\n" +
+ " - Third\n" +
+ " 1. And first here\n\n";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin())
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void tableOfContents() {
+ final String lorem = getString(R.string.lorem);
+ final String md = "" +
+ "# First\n" +
+ "" + lorem + "\n\n" +
+ "# Second\n" +
+ "" + lorem + "\n\n" +
+ "## Second level\n\n" +
+ "" + lorem + "\n\n" +
+ "### Level 3\n\n" +
+ "" + lorem + "\n\n" +
+ "# First again\n" +
+ "" + lorem + "\n\n";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new TableOfContentsPlugin())
+ .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
.build();
markwon.setMarkdown(textView, md);
diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java
new file mode 100644
index 00000000..26712de0
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java
@@ -0,0 +1,156 @@
+package io.noties.markwon.sample.basicplugins;
+
+import android.text.TextUtils;
+import android.util.SparseIntArray;
+
+import androidx.annotation.NonNull;
+
+import org.commonmark.node.BulletList;
+import org.commonmark.node.ListItem;
+import org.commonmark.node.Node;
+import org.commonmark.node.OrderedList;
+
+import io.noties.markwon.AbstractMarkwonPlugin;
+import io.noties.markwon.MarkwonSpansFactory;
+import io.noties.markwon.MarkwonVisitor;
+import io.noties.markwon.Prop;
+import io.noties.markwon.core.CoreProps;
+import io.noties.markwon.core.spans.BulletListItemSpan;
+import io.noties.markwon.core.spans.OrderedListItemSpan;
+
+public class BulletListIsOrderedWithLettersWhenNestedPlugin extends AbstractMarkwonPlugin {
+
+ private static final Prop BULLET_LETTER = Prop.of("my-bullet-letter");
+
+ // or introduce some kind of synchronization if planning to use from multiple threads,
+ // for example via ThreadLocal
+ private final SparseIntArray bulletCounter = new SparseIntArray();
+
+ @Override
+ public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
+ // clear counter after render
+ bulletCounter.clear();
+ }
+
+ @Override
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ // NB that both ordered and bullet lists are represented
+ // by ListItem (must inspect parent to detect the type)
+ builder.on(ListItem.class, (visitor, listItem) -> {
+ // mimic original behaviour (copy-pasta from CorePlugin)
+
+ final int length = visitor.length();
+
+ visitor.visitChildren(listItem);
+
+ final Node parent = listItem.getParent();
+ if (parent instanceof OrderedList) {
+
+ final int start = ((OrderedList) parent).getStartNumber();
+
+ CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED);
+ CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start);
+
+ // after we have visited the children increment start number
+ final OrderedList orderedList = (OrderedList) parent;
+ orderedList.setStartNumber(orderedList.getStartNumber() + 1);
+
+ } else {
+ CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET);
+
+ if (isBulletOrdered(parent)) {
+ // obtain current count value
+ final int count = currentBulletCountIn(parent);
+ BULLET_LETTER.set(visitor.renderProps(), createBulletLetter(count));
+ // update current count value
+ setCurrentBulletCountIn(parent, count + 1);
+ } else {
+ CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
+ // clear letter info when regular bullet list is used
+ BULLET_LETTER.clear(visitor.renderProps());
+ }
+ }
+
+ visitor.setSpansForNodeOptional(listItem, length);
+
+ if (visitor.hasNext(listItem)) {
+ visitor.ensureNewLine();
+ }
+ });
+ }
+
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ builder.setFactory(ListItem.class, (configuration, props) -> {
+ final Object spans;
+
+ if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) {
+ final String letter = BULLET_LETTER.get(props);
+ if (!TextUtils.isEmpty(letter)) {
+ // NB, we are using OrderedListItemSpan here!
+ spans = new OrderedListItemSpan(
+ configuration.theme(),
+ letter
+ );
+ } else {
+ spans = new BulletListItemSpan(
+ configuration.theme(),
+ CoreProps.BULLET_LIST_ITEM_LEVEL.require(props)
+ );
+ }
+ } else {
+
+ final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props))
+ + "." + '\u00a0';
+
+ spans = new OrderedListItemSpan(
+ configuration.theme(),
+ number
+ );
+ }
+
+ return spans;
+ });
+ }
+
+ private int currentBulletCountIn(@NonNull Node parent) {
+ return bulletCounter.get(parent.hashCode(), 0);
+ }
+
+ private void setCurrentBulletCountIn(@NonNull Node parent, int count) {
+ bulletCounter.put(parent.hashCode(), count);
+ }
+
+ @NonNull
+ private static String createBulletLetter(int count) {
+ // or lower `a`
+ // `'u00a0` is non-breakable space char
+ return ((char) ('A' + count)) + ".\u00a0";
+ }
+
+ private static int listLevel(@NonNull Node node) {
+ int level = 0;
+ Node parent = node.getParent();
+ while (parent != null) {
+ if (parent instanceof ListItem) {
+ level += 1;
+ }
+ parent = parent.getParent();
+ }
+ return level;
+ }
+
+ private static boolean isBulletOrdered(@NonNull Node node) {
+ node = node.getParent();
+ while (node != null) {
+ if (node instanceof OrderedList) {
+ return true;
+ }
+ if (node instanceof BulletList) {
+ return false;
+ }
+ node = node.getParent();
+ }
+ return false;
+ }
+}
diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java
new file mode 100644
index 00000000..c172ec7e
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java
@@ -0,0 +1,115 @@
+package io.noties.markwon.sample.basicplugins;
+
+import androidx.annotation.NonNull;
+
+import org.commonmark.node.AbstractVisitor;
+import org.commonmark.node.BulletList;
+import org.commonmark.node.CustomBlock;
+import org.commonmark.node.Heading;
+import org.commonmark.node.Link;
+import org.commonmark.node.ListItem;
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+
+import io.noties.markwon.AbstractMarkwonPlugin;
+import io.noties.markwon.MarkwonVisitor;
+import io.noties.markwon.core.SimpleBlockNodeVisitor;
+
+public class TableOfContentsPlugin extends AbstractMarkwonPlugin {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ // just to make it explicit
+ registry.require(AnchorHeadingPlugin.class);
+ }
+
+ @Override
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.on(TableOfContentsBlock.class, new SimpleBlockNodeVisitor());
+ }
+
+ @Override
+ public void beforeRender(@NonNull Node node) {
+
+ // custom block to hold TOC
+ final TableOfContentsBlock block = new TableOfContentsBlock();
+
+ // create TOC title
+ {
+ final Text text = new Text("Table of contents");
+ final Heading heading = new Heading();
+ // important one - set TOC heading level
+ heading.setLevel(1);
+ heading.appendChild(text);
+ block.appendChild(heading);
+ }
+
+ final HeadingVisitor visitor = new HeadingVisitor(block);
+ node.accept(visitor);
+
+ // make it the very first node in rendered markdown
+ node.prependChild(block);
+ }
+
+ private static class HeadingVisitor extends AbstractVisitor {
+
+ private final BulletList bulletList = new BulletList();
+ private final StringBuilder builder = new StringBuilder();
+ private boolean isInsideHeading;
+
+ HeadingVisitor(@NonNull Node node) {
+ node.appendChild(bulletList);
+ }
+
+ @Override
+ public void visit(Heading heading) {
+ this.isInsideHeading = true;
+ try {
+ // reset build from previous content
+ builder.setLength(0);
+
+ // obtain level (can additionally filter by level, to skip lower ones)
+ final int level = heading.getLevel();
+
+ // build heading title
+ visitChildren(heading);
+
+ // initial list item
+ final ListItem listItem = new ListItem();
+
+ Node parent = listItem;
+ Node node = listItem;
+
+ for (int i = 1; i < level; i++) {
+ final ListItem li = new ListItem();
+ final BulletList bulletList = new BulletList();
+ bulletList.appendChild(li);
+ parent.appendChild(bulletList);
+ parent = li;
+ node = li;
+ }
+
+ final String content = builder.toString();
+ final Link link = new Link("#" + AnchorHeadingPlugin.createAnchor(content), null);
+ final Text text = new Text(content);
+ link.appendChild(text);
+ node.appendChild(link);
+ bulletList.appendChild(listItem);
+
+
+ } finally {
+ isInsideHeading = false;
+ }
+ }
+
+ @Override
+ public void visit(Text text) {
+ // can additionally check if we are building heading (to skip all other texts)
+ if (isInsideHeading) {
+ builder.append(text.getLiteral());
+ }
+ }
+ }
+
+ private static class TableOfContentsBlock extends CustomBlock {
+ }
+}
diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
index e1181a7f..31a4370e 100644
--- a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
@@ -64,7 +64,8 @@ public MenuOptions menuOptions() {
.add("multipleEditSpans", this::multiple_edit_spans)
.add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
.add("pluginRequire", this::plugin_require)
- .add("pluginNoDefaults", this::plugin_no_defaults);
+ .add("pluginNoDefaults", this::plugin_no_defaults)
+ .add("heading", this::heading);
}
@Override
@@ -317,6 +318,16 @@ private void plugin_no_defaults() {
editor, Executors.newSingleThreadExecutor(), editText));
}
+ private void heading() {
+ final Markwon markwon = Markwon.create(this);
+ final MarkwonEditor editor = MarkwonEditor.builder(markwon)
+ .useEditHandler(new HeadingEditHandler())
+ .build();
+
+ editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
+ editor, Executors.newSingleThreadExecutor(), editText));
+ }
+
private void initBottomBar() {
// all except block-quote wraps if have selection, or inserts at current cursor position
diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/HeadingEditHandler.java b/sample/src/main/java/io/noties/markwon/sample/editor/HeadingEditHandler.java
new file mode 100644
index 00000000..f76499db
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/editor/HeadingEditHandler.java
@@ -0,0 +1,78 @@
+package io.noties.markwon.sample.editor;
+
+import android.text.Editable;
+import android.text.Spanned;
+
+import androidx.annotation.NonNull;
+
+import io.noties.markwon.Markwon;
+import io.noties.markwon.core.MarkwonTheme;
+import io.noties.markwon.core.spans.HeadingSpan;
+import io.noties.markwon.editor.EditHandler;
+import io.noties.markwon.editor.PersistedSpans;
+
+public class HeadingEditHandler implements EditHandler {
+
+ private MarkwonTheme theme;
+
+ @Override
+ public void init(@NonNull Markwon markwon) {
+ this.theme = markwon.configuration().theme();
+ }
+
+ @Override
+ public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
+ builder
+ .persistSpan(Head1.class, () -> new Head1(theme))
+ .persistSpan(Head2.class, () -> new Head2(theme));
+ }
+
+ @Override
+ public void handleMarkdownSpan(
+ @NonNull PersistedSpans persistedSpans,
+ @NonNull Editable editable,
+ @NonNull String input,
+ @NonNull HeadingSpan span,
+ int spanStart,
+ int spanTextLength
+ ) {
+ final Class> type;
+ switch (span.getLevel()) {
+ case 1: type = Head1.class; break;
+ case 2: type = Head2.class; break;
+ default:
+ type = null;
+ }
+
+ if (type != null) {
+ final int index = input.indexOf('\n', spanStart + spanTextLength);
+ final int end = index < 0
+ ? input.length()
+ : index;
+ editable.setSpan(
+ persistedSpans.get(type),
+ spanStart,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ }
+ }
+
+ @NonNull
+ @Override
+ public Class markdownSpanType() {
+ return HeadingSpan.class;
+ }
+
+ private static class Head1 extends HeadingSpan {
+ Head1(@NonNull MarkwonTheme theme) {
+ super(theme, 1);
+ }
+ }
+
+ private static class Head2 extends HeadingSpan {
+ Head2(@NonNull MarkwonTheme theme) {
+ super(theme, 2);
+ }
+ }
+}
diff --git a/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedFutureActivity.java b/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedFutureActivity.java
new file mode 100644
index 00000000..577f2406
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedFutureActivity.java
@@ -0,0 +1,95 @@
+package io.noties.markwon.sample.precomputed;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import io.noties.markwon.Markwon;
+import io.noties.markwon.PrecomputedFutureTextSetterCompat;
+import io.noties.markwon.recycler.MarkwonAdapter;
+import io.noties.markwon.sample.R;
+
+public class PrecomputedFutureActivity extends Activity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_recycler);
+
+ final Markwon markwon = Markwon.builder(this)
+ .textSetter(PrecomputedFutureTextSetterCompat.create())
+ .build();
+
+ // create MarkwonAdapter and register two blocks that will be rendered differently
+ final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_appcompat_default_entry, R.id.text)
+ .build();
+
+ final RecyclerView recyclerView = findViewById(R.id.recycler_view);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ recyclerView.setHasFixedSize(true);
+ recyclerView.setAdapter(adapter);
+
+ adapter.setMarkdown(markwon, loadReadMe(this));
+
+ // please note that we should notify updates (adapter doesn't do it implicitly)
+ adapter.notifyDataSetChanged();
+ }
+
+ @NonNull
+ private static String loadReadMe(@NonNull Context context) {
+ InputStream stream = null;
+ try {
+ stream = context.getAssets().open("README.md");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return readStream(stream);
+ }
+
+ @NonNull
+ private static String readStream(@Nullable InputStream inputStream) {
+
+ String out = null;
+
+ if (inputStream != null) {
+ BufferedReader reader = null;
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ reader = new BufferedReader(new InputStreamReader(inputStream));
+ final StringBuilder builder = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line)
+ .append('\n');
+ }
+ out = builder.toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ // no op
+ }
+ }
+ }
+ }
+
+ if (out == null) {
+ throw new RuntimeException("Cannot read stream");
+ }
+
+ return out;
+ }
+}
diff --git a/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java b/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java
new file mode 100644
index 00000000..d7153ec6
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java
@@ -0,0 +1,89 @@
+package io.noties.markwon.sample.table;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import io.noties.debug.Debug;
+import io.noties.markwon.Markwon;
+import io.noties.markwon.ext.tables.TablePlugin;
+import io.noties.markwon.ext.tables.TableTheme;
+import io.noties.markwon.linkify.LinkifyPlugin;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
+import io.noties.markwon.sample.R;
+import io.noties.markwon.utils.ColorUtils;
+import io.noties.markwon.utils.Dip;
+
+public class TableActivity extends ActivityWithMenuOptions {
+
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("customize", this::customize)
+ .add("tableAndLinkify", this::tableAndLinkify);
+ }
+
+ private TextView textView;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_text_view);
+ textView = findViewById(R.id.text_view);
+
+ tableAndLinkify();
+ }
+
+ private void customize() {
+ final String md = "" +
+ "| HEADER | HEADER | HEADER |\n" +
+ "|:----:|:----:|:----:|\n" +
+ "| 测试 | 测试 | 测试 |\n" +
+ "| 测试 | 测试 | 测测测12345试测试测试 |\n" +
+ "| 测试 | 测试 | 123445 |\n" +
+ "| 测试 | 测试 | (650) 555-1212 |\n" +
+ "| 测试 | 测试 | [link](#) |\n";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(TablePlugin.create(builder -> {
+ final Dip dip = Dip.create(this);
+ builder
+ .tableBorderWidth(dip.toPx(2))
+ .tableBorderColor(Color.YELLOW)
+ .tableCellPadding(dip.toPx(4))
+ .tableHeaderRowBackgroundColor(ColorUtils.applyAlpha(Color.RED, 80))
+ .tableEvenRowBackgroundColor(ColorUtils.applyAlpha(Color.GREEN, 80))
+ .tableOddRowBackgroundColor(ColorUtils.applyAlpha(Color.BLUE, 80));
+ }))
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void tableAndLinkify() {
+ final String md = "" +
+ "| HEADER | HEADER | HEADER |\n" +
+ "|:----:|:----:|:----:|\n" +
+ "| 测试 | 测试 | 测试 |\n" +
+ "| 测试 | 测试 | 测测测12345试测试测试 |\n" +
+ "| 测试 | 测试 | 123445 |\n" +
+ "| 测试 | 测试 | (650) 555-1212 |\n" +
+ "| 测试 | 测试 | [link](#) |\n" +
+ "\n" +
+ "测试\n" +
+ "\n" +
+ "[link link](https://link.link)";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(LinkifyPlugin.create())
+ .usePlugin(TablePlugin.create(this))
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+}
diff --git a/sample/src/main/res/layout/adapter_appcompat_default_entry.xml b/sample/src/main/res/layout/adapter_appcompat_default_entry.xml
new file mode 100644
index 00000000..3861b72a
--- /dev/null
+++ b/sample/src/main/res/layout/adapter_appcompat_default_entry.xml
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/values/strings-samples.xml b/sample/src/main/res/values/strings-samples.xml
index 0305b471..a1377ffa 100644
--- a/sample/src/main/res/values/strings-samples.xml
+++ b/sample/src/main/res/values/strings-samples.xml
@@ -25,6 +25,8 @@
# \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat
+ # \# PrecomputedFutureText\n\nUsage of TextSetter and PrecomputedFutureTextSetterCompat
+
# \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText
# \# Inline Parser\n\nUsage of custom inline parser
@@ -37,4 +39,5 @@
# \# Notification\n\nExample usage in notifications and other remote views
+ # \# Table\n\nUsage of tables in a `TextView`
\ No newline at end of file