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