commit
a26c13c93a
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 4.3.1
|
||||||
|
* Fix DexGuard optimization issue ([#216])<br>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`<br>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
|
# 4.3.0
|
||||||
* add `MarkwonInlineParserPlugin` in `inline-parser` module
|
* add `MarkwonInlineParserPlugin` in `inline-parser` module
|
||||||
* `JLatexMathPlugin` now supports inline LaTeX structures via `MarkwonInlineParserPlugin`
|
* `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])
|
* `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
|
* 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])
|
* non-empty bounds for `AsyncDrawable` when no dimensions are not yet available ([#189])
|
||||||
* `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201])
|
* `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201])<br>Thanks to [@drakeet]
|
||||||
<br>Thanks to [@drakeet]
|
|
||||||
* `MarkwonVisitor.BlockHandler` and `BlockHandlerDef` implementation to control how blocks insert new lines after them
|
* `MarkwonVisitor.BlockHandler` and `BlockHandlerDef` implementation to control how blocks insert new lines after them
|
||||||
|
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
app/src/debug/res/layout/debug_color_blend.xml
Normal file
34
app/src/debug/res/layout/debug_color_blend.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#f00">
|
||||||
|
|
||||||
|
<io.noties.markwon.debug.ColorBlendView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dip"
|
||||||
|
app:cbv_background="#fff"
|
||||||
|
app:cbv_foreground="#f0f"/>
|
||||||
|
|
||||||
|
<io.noties.markwon.debug.ColorBlendView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dip"
|
||||||
|
app:cbv_background="#000"
|
||||||
|
app:cbv_foreground="#f0f"/>
|
||||||
|
|
||||||
|
<io.noties.markwon.debug.ColorBlendView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dip"
|
||||||
|
app:cbv_background="#fff"
|
||||||
|
app:cbv_foreground="#00f"/>
|
||||||
|
|
||||||
|
<io.noties.markwon.debug.ColorBlendView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dip"
|
||||||
|
app:cbv_background="#000"
|
||||||
|
app:cbv_foreground="#00f"/>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -8,4 +8,9 @@
|
|||||||
<attr name="fcdv_checked" format="boolean" />
|
<attr name="fcdv_checked" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="ColorBlendView">
|
||||||
|
<attr name="cbv_foreground" format="color" />
|
||||||
|
<attr name="cbv_background" format="color" />
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -66,6 +66,7 @@ ext {
|
|||||||
'x-annotations' : 'androidx.annotation:annotation:1.1.0',
|
'x-annotations' : 'androidx.annotation:annotation:1.1.0',
|
||||||
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
|
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
|
||||||
'x-core' : 'androidx.core:core:1.0.2',
|
'x-core' : 'androidx.core:core:1.0.2',
|
||||||
|
'x-appcompat' : 'androidx.appcompat:appcompat:1.1.0',
|
||||||
'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion",
|
'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion",
|
||||||
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
|
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
|
||||||
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",
|
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",
|
||||||
|
@ -8,7 +8,7 @@ android.enableJetifier=true
|
|||||||
android.enableBuildCache=true
|
android.enableBuildCache=true
|
||||||
android.buildCacheDir=build/pre-dex-cache
|
android.buildCacheDir=build/pre-dex-cache
|
||||||
|
|
||||||
VERSION_NAME=4.3.0
|
VERSION_NAME=4.3.1
|
||||||
|
|
||||||
GROUP=io.noties.markwon
|
GROUP=io.noties.markwon
|
||||||
POM_DESCRIPTION=Markwon markdown for Android
|
POM_DESCRIPTION=Markwon markdown for Android
|
||||||
|
@ -22,6 +22,7 @@ dependencies {
|
|||||||
// @since 4.1.0 to allow PrecomputedTextSetterCompat
|
// @since 4.1.0 to allow PrecomputedTextSetterCompat
|
||||||
// note that this dependency must be added on a client side explicitly
|
// note that this dependency must be added on a client side explicitly
|
||||||
compileOnly it['x-core']
|
compileOnly it['x-core']
|
||||||
|
compileOnly it['x-appcompat']
|
||||||
}
|
}
|
||||||
|
|
||||||
deps['test'].with {
|
deps['test'].with {
|
||||||
|
@ -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<PrecomputedTextCompat> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,33 @@
|
|||||||
package io.noties.markwon.utils;
|
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 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);
|
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() {
|
private ColorUtils() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,12 +123,13 @@ public class TablePlugin extends AbstractMarkwonPlugin {
|
|||||||
|
|
||||||
visitor.blockStart(tableBlock);
|
visitor.blockStart(tableBlock);
|
||||||
|
|
||||||
|
final int length = visitor.length();
|
||||||
|
|
||||||
visitor.visitChildren(tableBlock);
|
visitor.visitChildren(tableBlock);
|
||||||
|
|
||||||
// if (visitor.hasNext(tableBlock)) {
|
// @since 4.3.1 apply table span for the full table
|
||||||
// visitor.ensureNewLine();
|
visitor.setSpans(length, new TableSpan());
|
||||||
// visitor.forceNewLine();
|
|
||||||
// }
|
|
||||||
visitor.blockEnd(tableBlock);
|
visitor.blockEnd(tableBlock);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -5,6 +5,7 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
import android.text.StaticLayout;
|
import android.text.StaticLayout;
|
||||||
import android.text.TextPaint;
|
import android.text.TextPaint;
|
||||||
import android.text.style.ReplacementSpan;
|
import android.text.style.ReplacementSpan;
|
||||||
@ -19,6 +20,8 @@ import java.lang.annotation.RetentionPolicy;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.noties.markwon.utils.LeadingMarginUtils;
|
||||||
|
|
||||||
public class TableRowSpan extends ReplacementSpan {
|
public class TableRowSpan extends ReplacementSpan {
|
||||||
|
|
||||||
public static final int ALIGN_LEFT = 0;
|
public static final int ALIGN_LEFT = 0;
|
||||||
@ -139,11 +142,17 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
int top,
|
int top,
|
||||||
int y,
|
int y,
|
||||||
int bottom,
|
int bottom,
|
||||||
@NonNull Paint paint) {
|
@NonNull Paint p) {
|
||||||
|
|
||||||
if (recreateLayouts(canvas.getWidth())) {
|
if (recreateLayouts(canvas.getWidth())) {
|
||||||
width = 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();
|
makeNewLayouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,28 +164,25 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
|
|
||||||
final int w = width / size;
|
final int w = width / size;
|
||||||
|
|
||||||
// feels like magic...
|
|
||||||
final int heightDiff = (bottom - top - height) / 4;
|
|
||||||
|
|
||||||
// @since 1.1.1
|
// @since 1.1.1
|
||||||
// draw backgrounds
|
// draw backgrounds
|
||||||
{
|
{
|
||||||
if (header) {
|
if (header) {
|
||||||
theme.applyTableHeaderRowStyle(this.paint);
|
theme.applyTableHeaderRowStyle(paint);
|
||||||
} else if (odd) {
|
} else if (odd) {
|
||||||
theme.applyTableOddRowStyle(this.paint);
|
theme.applyTableOddRowStyle(paint);
|
||||||
} else {
|
} else {
|
||||||
// even
|
// even
|
||||||
theme.applyTableEvenRowStyle(this.paint);
|
theme.applyTableEvenRowStyle(paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if present (0 is transparent)
|
// if present (0 is transparent)
|
||||||
if (this.paint.getColor() != 0) {
|
if (paint.getColor() != 0) {
|
||||||
final int save = canvas.save();
|
final int save = canvas.save();
|
||||||
try {
|
try {
|
||||||
rect.set(0, 0, width, bottom - top);
|
rect.set(0, 0, width, bottom - top);
|
||||||
canvas.translate(x, top - heightDiff);
|
canvas.translate(x, top);
|
||||||
canvas.drawRect(rect, this.paint);
|
canvas.drawRect(rect, paint);
|
||||||
} finally {
|
} finally {
|
||||||
canvas.restoreToCount(save);
|
canvas.restoreToCount(save);
|
||||||
}
|
}
|
||||||
@ -186,14 +192,49 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
// @since 1.1.1 reset after applying background color
|
// @since 1.1.1 reset after applying background color
|
||||||
// as background changes color attribute and if not specific tableBorderColor
|
// 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)
|
// is specified then after this row all borders will have color of this row (plus alpha)
|
||||||
this.paint.set(paint);
|
paint.set(p);
|
||||||
theme.applyTableBorderStyle(this.paint);
|
theme.applyTableBorderStyle(paint);
|
||||||
|
|
||||||
final int borderWidth = theme.tableBorderWidth(paint);
|
final int borderWidth = theme.tableBorderWidth(paint);
|
||||||
final boolean drawBorder = borderWidth > 0;
|
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) {
|
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;
|
StaticLayout layout;
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
@ -201,10 +242,23 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
final int save = canvas.save();
|
final int save = canvas.save();
|
||||||
try {
|
try {
|
||||||
|
|
||||||
canvas.translate(x + (i * w), top - heightDiff);
|
canvas.translate(x + (i * w), top);
|
||||||
|
|
||||||
|
// @since 4.3.1
|
||||||
if (drawBorder) {
|
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);
|
canvas.translate(padding, padding + heightDiff);
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package io.noties.markwon.ext.tables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.3.1
|
||||||
|
*/
|
||||||
|
public class TableSpan {
|
||||||
|
}
|
@ -10,6 +10,7 @@ import androidx.annotation.Px;
|
|||||||
import io.noties.markwon.utils.ColorUtils;
|
import io.noties.markwon.utils.ColorUtils;
|
||||||
import io.noties.markwon.utils.Dip;
|
import io.noties.markwon.utils.Dip;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class TableTheme {
|
public class TableTheme {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -101,7 +102,8 @@ public class TableTheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
paint.setColor(color);
|
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) {
|
public void applyTableOddRowStyle(@NonNull Paint paint) {
|
||||||
|
@ -25,7 +25,9 @@ public class StrikeHandler extends TagHandler {
|
|||||||
static {
|
static {
|
||||||
boolean hasMarkdownImplementation;
|
boolean hasMarkdownImplementation;
|
||||||
try {
|
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;
|
hasMarkdownImplementation = true;
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
hasMarkdownImplementation = false;
|
hasMarkdownImplementation = false;
|
||||||
|
@ -14,7 +14,8 @@ public abstract class GifSupport {
|
|||||||
static {
|
static {
|
||||||
boolean result;
|
boolean result;
|
||||||
try {
|
try {
|
||||||
pl.droidsonroids.gif.GifDrawable.class.getName();
|
// @since 4.3.1
|
||||||
|
Class.forName("pl.droidsonroids.gif.GifDrawable");
|
||||||
result = true;
|
result = true;
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
// @since 4.1.1 instead of printing full stacktrace of the exception,
|
// @since 4.1.1 instead of printing full stacktrace of the exception,
|
||||||
|
@ -14,7 +14,7 @@ public abstract class SvgSupport {
|
|||||||
static {
|
static {
|
||||||
boolean result;
|
boolean result;
|
||||||
try {
|
try {
|
||||||
com.caverock.androidsvg.SVG.class.getName();
|
Class.forName("com.caverock.androidsvg.SVG");
|
||||||
result = true;
|
result = true;
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
// @since 4.1.1 instead of printing full stacktrace of the exception,
|
// @since 4.1.1 instead of printing full stacktrace of the exception,
|
||||||
|
@ -54,6 +54,7 @@ dependencies {
|
|||||||
deps.with {
|
deps.with {
|
||||||
implementation it['x-recycler-view']
|
implementation it['x-recycler-view']
|
||||||
implementation it['x-core'] // for precomputedTextCompat
|
implementation it['x-core'] // for precomputedTextCompat
|
||||||
|
implementation it['x-appcompat'] // for setTextFuture
|
||||||
implementation it['okhttp']
|
implementation it['okhttp']
|
||||||
implementation it['prism4j']
|
implementation it['prism4j']
|
||||||
implementation it['debug']
|
implementation it['debug']
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
<activity android:name=".simpleext.SimpleExtActivity" />
|
<activity android:name=".simpleext.SimpleExtActivity" />
|
||||||
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
||||||
<activity android:name=".precomputed.PrecomputedActivity" />
|
<activity android:name=".precomputed.PrecomputedActivity" />
|
||||||
|
<activity android:name=".precomputed.PrecomputedFutureActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".editor.EditorActivity"
|
android:name=".editor.EditorActivity"
|
||||||
@ -38,6 +39,7 @@
|
|||||||
<activity android:name=".tasklist.TaskListActivity" />
|
<activity android:name=".tasklist.TaskListActivity" />
|
||||||
<activity android:name=".images.ImagesActivity" />
|
<activity android:name=".images.ImagesActivity" />
|
||||||
<activity android:name=".notification.NotificationActivity" />
|
<activity android:name=".notification.NotificationActivity" />
|
||||||
|
<activity android:name=".table.TableActivity" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
@ -30,8 +30,10 @@ import io.noties.markwon.sample.inlineparser.InlineParserActivity;
|
|||||||
import io.noties.markwon.sample.latex.LatexActivity;
|
import io.noties.markwon.sample.latex.LatexActivity;
|
||||||
import io.noties.markwon.sample.notification.NotificationActivity;
|
import io.noties.markwon.sample.notification.NotificationActivity;
|
||||||
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
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.recycler.RecyclerActivity;
|
||||||
import io.noties.markwon.sample.simpleext.SimpleExtActivity;
|
import io.noties.markwon.sample.simpleext.SimpleExtActivity;
|
||||||
|
import io.noties.markwon.sample.table.TableActivity;
|
||||||
import io.noties.markwon.sample.tasklist.TaskListActivity;
|
import io.noties.markwon.sample.tasklist.TaskListActivity;
|
||||||
|
|
||||||
public class MainActivity extends Activity {
|
public class MainActivity extends Activity {
|
||||||
@ -123,6 +125,10 @@ public class MainActivity extends Activity {
|
|||||||
activity = PrecomputedActivity.class;
|
activity = PrecomputedActivity.class;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case PRECOMPUTED_FUTURE_TEXT:
|
||||||
|
activity = PrecomputedFutureActivity.class;
|
||||||
|
break;
|
||||||
|
|
||||||
case EDITOR:
|
case EDITOR:
|
||||||
activity = EditorActivity.class;
|
activity = EditorActivity.class;
|
||||||
break;
|
break;
|
||||||
@ -147,6 +153,10 @@ public class MainActivity extends Activity {
|
|||||||
activity = NotificationActivity.class;
|
activity = NotificationActivity.class;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TABLE:
|
||||||
|
activity = TableActivity.class;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ public enum Sample {
|
|||||||
|
|
||||||
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
|
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
|
||||||
|
|
||||||
|
PRECOMPUTED_FUTURE_TEXT(R.string.sample_precomputed_future_text),
|
||||||
|
|
||||||
EDITOR(R.string.sample_editor),
|
EDITOR(R.string.sample_editor),
|
||||||
|
|
||||||
INLINE_PARSER(R.string.sample_inline_parser),
|
INLINE_PARSER(R.string.sample_inline_parser),
|
||||||
@ -33,7 +35,9 @@ public enum Sample {
|
|||||||
|
|
||||||
IMAGES(R.string.sample_images),
|
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;
|
private final int textResId;
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -3,11 +3,8 @@ package io.noties.markwon.sample.basicplugins;
|
|||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Spannable;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@ -23,7 +20,6 @@ import java.util.Collections;
|
|||||||
|
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.BlockHandlerDef;
|
import io.noties.markwon.BlockHandlerDef;
|
||||||
import io.noties.markwon.LinkResolverDef;
|
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
import io.noties.markwon.MarkwonConfiguration;
|
import io.noties.markwon.MarkwonConfiguration;
|
||||||
import io.noties.markwon.MarkwonSpansFactory;
|
import io.noties.markwon.MarkwonSpansFactory;
|
||||||
@ -31,7 +27,6 @@ import io.noties.markwon.MarkwonVisitor;
|
|||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||||
import io.noties.markwon.core.CoreProps;
|
import io.noties.markwon.core.CoreProps;
|
||||||
import io.noties.markwon.core.MarkwonTheme;
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
import io.noties.markwon.core.spans.HeadingSpan;
|
|
||||||
import io.noties.markwon.core.spans.LastLineSpacingSpan;
|
import io.noties.markwon.core.spans.LastLineSpacingSpan;
|
||||||
import io.noties.markwon.image.ImageItem;
|
import io.noties.markwon.image.ImageItem;
|
||||||
import io.noties.markwon.image.ImagesPlugin;
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
@ -62,7 +57,9 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
|
|||||||
.add("headingNoSpace", this::headingNoSpace)
|
.add("headingNoSpace", this::headingNoSpace)
|
||||||
.add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler)
|
.add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler)
|
||||||
.add("allBlocksNoForcedLine", this::allBlocksNoForcedLine)
|
.add("allBlocksNoForcedLine", this::allBlocksNoForcedLine)
|
||||||
.add("anchor", this::anchor);
|
.add("anchor", this::anchor)
|
||||||
|
.add("letterOrderedList", this::letterOrderedList)
|
||||||
|
.add("tableOfContents", this::tableOfContents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -323,7 +320,7 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void headingNoSpaceBlockHandler() {
|
private void headingNoSpaceBlockHandler() {
|
||||||
final Markwon markwon = Markwon.builder(this)
|
final Markwon markwon = Markwon.builder(this)
|
||||||
.usePlugin(new AbstractMarkwonPlugin() {
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
@Override
|
@Override
|
||||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
@ -384,85 +381,6 @@ final Markwon markwon = Markwon.builder(this)
|
|||||||
markwon.setMarkdown(textView, md);
|
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<String> 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() {
|
private void anchor() {
|
||||||
final String lorem = getString(R.string.lorem);
|
final String lorem = getString(R.string.lorem);
|
||||||
final String md = "" +
|
final String md = "" +
|
||||||
@ -472,32 +390,46 @@ final Markwon markwon = Markwon.builder(this)
|
|||||||
lorem;
|
lorem;
|
||||||
|
|
||||||
final Markwon markwon = Markwon.builder(this)
|
final Markwon markwon = Markwon.builder(this)
|
||||||
.usePlugin(new AbstractMarkwonPlugin() {
|
.usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
|
||||||
@Override
|
.build();
|
||||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
|
||||||
builder.linkResolver(new AnchorLinkResolver((view, top) -> scrollView.smoothScrollTo(0, top)));
|
markwon.setMarkdown(textView, md);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void letterOrderedList() {
|
||||||
public void afterSetText(@NonNull TextView textView) {
|
// bullet list nested in ordered list renders letters instead of bullets
|
||||||
final Spannable spannable = (Spannable) textView.getText();
|
final String md = "" +
|
||||||
// obtain heading spans
|
"1. Hello there!\n" +
|
||||||
final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class);
|
"1. And here is how:\n" +
|
||||||
if (spans != null) {
|
" - First\n" +
|
||||||
for (HeadingSpan span : spans) {
|
" - Second\n" +
|
||||||
final int start = spannable.getSpanStart(span);
|
" - Third\n" +
|
||||||
final int end = spannable.getSpanEnd(span);
|
" 1. And first here\n\n";
|
||||||
final int flags = spannable.getSpanFlags(span);
|
|
||||||
spannable.setSpan(
|
final Markwon markwon = Markwon.builder(this)
|
||||||
new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
|
.usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin())
|
||||||
start,
|
.build();
|
||||||
end,
|
|
||||||
flags
|
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();
|
.build();
|
||||||
|
|
||||||
markwon.setMarkdown(textView, md);
|
markwon.setMarkdown(textView, md);
|
||||||
|
@ -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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
}
|
||||||
|
}
|
@ -64,7 +64,8 @@ public class EditorActivity extends ActivityWithMenuOptions {
|
|||||||
.add("multipleEditSpans", this::multiple_edit_spans)
|
.add("multipleEditSpans", this::multiple_edit_spans)
|
||||||
.add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
|
.add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
|
||||||
.add("pluginRequire", this::plugin_require)
|
.add("pluginRequire", this::plugin_require)
|
||||||
.add("pluginNoDefaults", this::plugin_no_defaults);
|
.add("pluginNoDefaults", this::plugin_no_defaults)
|
||||||
|
.add("heading", this::heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -317,6 +318,16 @@ public class EditorActivity extends ActivityWithMenuOptions {
|
|||||||
editor, Executors.newSingleThreadExecutor(), editText));
|
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() {
|
private void initBottomBar() {
|
||||||
// all except block-quote wraps if have selection, or inserts at current cursor position
|
// all except block-quote wraps if have selection, or inserts at current cursor position
|
||||||
|
|
||||||
|
@ -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<HeadingSpan> {
|
||||||
|
|
||||||
|
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<HeadingSpan> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="16dip"
|
||||||
|
android:layout_marginRight="16dip"
|
||||||
|
android:lineSpacingExtra="2dip"
|
||||||
|
android:paddingTop="8dip"
|
||||||
|
android:paddingBottom="8dip"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="#000"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="Hello" />
|
@ -25,6 +25,8 @@
|
|||||||
|
|
||||||
<string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string>
|
<string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string>
|
||||||
|
|
||||||
|
<string name="sample_precomputed_future_text"># \# PrecomputedFutureText\n\nUsage of TextSetter and PrecomputedFutureTextSetterCompat</string>
|
||||||
|
|
||||||
<string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string>
|
<string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string>
|
||||||
|
|
||||||
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
||||||
@ -37,4 +39,5 @@
|
|||||||
|
|
||||||
<string name="sample_remote_views"># \# Notification\n\nExample usage in notifications and other remote views</string>
|
<string name="sample_remote_views"># \# Notification\n\nExample usage in notifications and other remote views</string>
|
||||||
|
|
||||||
|
<string name="sample_table"># \# Table\n\nUsage of tables in a `TextView`</string>
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user