commit
a26c13c93a
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,5 +1,18 @@
|
||||
# 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
|
||||
* 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])
|
||||
<br>Thanks to [@drakeet]
|
||||
* `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201])<br>Thanks to [@drakeet]
|
||||
* `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" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ColorBlendView">
|
||||
<attr name="cbv_foreground" format="color" />
|
||||
<attr name="cbv_background" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
|
@ -123,12 +123,13 @@ public class TablePlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
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);
|
||||
}
|
||||
})
|
||||
|
@ -5,6 +5,7 @@ import android.graphics.Canvas;
|
||||
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.lang.annotation.RetentionPolicy;
|
||||
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 class TableRowSpan extends ReplacementSpan {
|
||||
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 class TableRowSpan extends ReplacementSpan {
|
||||
|
||||
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 class TableRowSpan extends ReplacementSpan {
|
||||
// @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);
|
||||
|
@ -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.Dip;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class TableTheme {
|
||||
|
||||
@NonNull
|
||||
@ -101,7 +102,8 @@ public class TableTheme {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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']
|
||||
|
@ -28,6 +28,7 @@
|
||||
<activity android:name=".simpleext.SimpleExtActivity" />
|
||||
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
||||
<activity android:name=".precomputed.PrecomputedActivity" />
|
||||
<activity android:name=".precomputed.PrecomputedFutureActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".editor.EditorActivity"
|
||||
@ -38,6 +39,7 @@
|
||||
<activity android:name=".tasklist.TaskListActivity" />
|
||||
<activity android:name=".images.ImagesActivity" />
|
||||
<activity android:name=".notification.NotificationActivity" />
|
||||
<activity android:name=".table.TableActivity" />
|
||||
|
||||
</application>
|
||||
|
||||
|
@ -30,8 +30,10 @@ import io.noties.markwon.sample.inlineparser.InlineParserActivity;
|
||||
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 @@ public class MainActivity extends Activity {
|
||||
activity = PrecomputedActivity.class;
|
||||
break;
|
||||
|
||||
case PRECOMPUTED_FUTURE_TEXT:
|
||||
activity = PrecomputedFutureActivity.class;
|
||||
break;
|
||||
|
||||
case EDITOR:
|
||||
activity = EditorActivity.class;
|
||||
break;
|
||||
@ -147,6 +153,10 @@ public class MainActivity extends Activity {
|
||||
activity = NotificationActivity.class;
|
||||
break;
|
||||
|
||||
case TABLE:
|
||||
activity = TableActivity.class;
|
||||
break;
|
||||
|
||||
default:
|
||||
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_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;
|
||||
|
||||
|
@ -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.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 java.util.Collections;
|
||||
|
||||
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.MarkwonVisitor;
|
||||
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 class BasicPluginsActivity extends ActivityWithMenuOptions {
|
||||
.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 class BasicPluginsActivity extends ActivityWithMenuOptions {
|
||||
}
|
||||
|
||||
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 @@ final Markwon markwon = Markwon.builder(this)
|
||||
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() {
|
||||
final String lorem = getString(R.string.lorem);
|
||||
final String md = "" +
|
||||
@ -472,32 +390,46 @@ final Markwon markwon = Markwon.builder(this)
|
||||
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);
|
||||
|
@ -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("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 @@ public class EditorActivity extends ActivityWithMenuOptions {
|
||||
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
|
||||
|
||||
|
@ -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_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_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_table"># \# Table\n\nUsage of tables in a `TextView`</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user