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
	 Dimitry
						Dimitry