diff --git a/README.md b/README.md index fd65991b..d4501926 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Markwon -[![Maven Central|markwon](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=maven-central%7Cmarkwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon%22) -[![Maven Central|markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=maven-central%7Cmarkwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon%22) +[![maven|markwon](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=maven%7Cmarkwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon%22) +[![maven|markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=maven%7Cmarkwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon%22) Android library for rendering markdown as system-native Spannables. Based on [commonmark-java][commonmark-java] diff --git a/library/README.md b/library/README.md new file mode 100644 index 00000000..b5c0e430 --- /dev/null +++ b/library/README.md @@ -0,0 +1,97 @@ +# Markwon + +[![maven|markwon](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=maven%7Cmarkwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon%22) + + +## Installation +```groovy +compile 'ru.noties:markwon:1.0.0' +``` + +## Intoduction + +The aim for this library is to render markdown as first class citizen on Android - Spannables. It has reasonable defaults to display markdown, but also gives ability to customize almost every detail for your liking. + +The most basic example would be: +```java +Markwon.setMarkdown(textView, "**Hello *there*!!**") +``` + +## Images + +By default this library does not render any of the images. It's done to simplify rendering of text-based markdown. But if images must be supported, then the `AsyncDrawable.Loader` can be specified whilst building a `SpannableConfiguration` instance: + +```java +final AsyncDrawable.Loader loader = new AsyncDrawable.Loader() { + @Override + public void load(@NonNull String destination, @NonNull final AsyncDrawable drawable) { + // `download` method is here for demonstration purposes, it's not included in this interface + download(destination, new Callback() { + @Override + public void onDownloaded(Drawable d) { + drawable.setResult(d); + } + }); + } + + @Override + public void cancel(@NonNull String destination) { + // cancel download here + } +}; + +// `this` here referrs to a Context instance +final SpannableConfiguration configuration = SpannableConfiguration.builder(this) + .asyncDrawableLoader(loader) + .build(); +``` + +There is also standalone artifact that supports image loading *out-of-box* (including support for **SVG** & **GIF**), but provides little to none configuration and could be somewhat not optimal. Please refer to the [README][mil-README] of the module. + + +## Tables + +Tables are supported but with some limitations. First of all: table will always take the full width of the TextView Canvas. Second: each column will have the same width (we do not calculate the weight of column) - so, a column width will be: `totalWidth / columnsNumber`. + + +## Syntax highlight +This library does not provide ready-to-be-used implementation of syntax highlight, but it can be easily added via `SyntaxHighlight` interface whilst building `SpannableConfiguration`: + +```java +final SyntaxHighlight syntaxHighlight = new SyntaxHighlight() { + @NonNull + @Override + public CharSequence highlight(@Nullable String info, @NonNull String code) { + // create Spanned of highlight here + return null; // must not return `null` here + } +}; + +final SpannableConfiguration configuration = SpannableConfiguration.builder(this) + .syntaxHighlight(syntaxHighlight) + .build(); +``` + +## Url processing +If you wish to process urls (links & images) that markdown contains, the `UrlProcessor` can be used: +```java +final UrlProcessor urlProcessor = new UrlProcessor() { + @NonNull + @Override + public String process(@NonNull String destination) { + // modify the `destination` or return as-is + return null; + } +}; + +final SpannableConfiguration configuration = SpannableConfiguration.builder(this) + .urlProcessor(urlProcessor) + .build(); +``` +The primary goal of additing this abstraction is to give ability to convert relative urls to absolute ones. If it fits your purpose, then `UrlProcessorRelativeToAbsolute` can be used: +```java +final UrlProcessor urlProcessor = new UrlProcessorRelativeToAbsolute("https://this-is-base.org"); +``` + + +[mil-README]: https://github.com/noties/Markwon/blob/master/library-image-loader/README.md diff --git a/library/src/main/java/ru/noties/markwon/DrawablesScheduler.java b/library/src/main/java/ru/noties/markwon/DrawablesScheduler.java index baacac73..fd64d9d5 100644 --- a/library/src/main/java/ru/noties/markwon/DrawablesScheduler.java +++ b/library/src/main/java/ru/noties/markwon/DrawablesScheduler.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import ru.noties.markwon.renderer.R; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawableSpan; @@ -23,18 +24,24 @@ abstract class DrawablesScheduler { final List list = extract(textView); if (list.size() > 0) { - textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - } + if (textView.getTag(R.id.markwon_drawables_scheduler) == null) { + final View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { - @Override - public void onViewDetachedFromWindow(View v) { - // we obtain a new list in case text was changed - unschedule(textView); - } - }); + } + + @Override + public void onViewDetachedFromWindow(View v) { + unschedule(textView); + v.removeOnAttachStateChangeListener(this); + v.setTag(R.id.markwon_drawables_scheduler, null); + } + }; + textView.addOnAttachStateChangeListener(listener); + textView.setTag(R.id.markwon_drawables_scheduler, listener); + } for (AsyncDrawable drawable : list) { drawable.setCallback2(new DrawableCallbackImpl(textView, drawable.getBounds())); diff --git a/library/src/main/java/ru/noties/markwon/Markwon.java b/library/src/main/java/ru/noties/markwon/Markwon.java index e76deb38..b27ab224 100644 --- a/library/src/main/java/ru/noties/markwon/Markwon.java +++ b/library/src/main/java/ru/noties/markwon/Markwon.java @@ -19,16 +19,38 @@ import ru.noties.markwon.renderer.SpannableRenderer; @SuppressWarnings("WeakerAccess") public abstract class Markwon { + /** + * Helper method to obtain a {@link Parser} with registered strike-through & table extensions + * + * @return a {@link Parser} instance that is supported by this library + * @since 1.0.0 + */ public static Parser createParser() { return new Parser.Builder() .extensions(Arrays.asList(StrikethroughExtension.create(), TablesExtension.create())) .build(); } + /** + * @see #setMarkdown(TextView, SpannableConfiguration, String) + * @since 1.0.0 + */ public static void setMarkdown(@NonNull TextView view, @NonNull String markdown) { setMarkdown(view, SpannableConfiguration.create(view.getContext()), markdown); } + /** + * Parses submitted raw markdown, converts it to CharSequence (with Spannables) + * and applies it to view + * + * @param view {@link TextView} to set markdown into + * @param configuration a {@link SpannableConfiguration} instance + * @param markdown raw markdown String (for example: {@code `**Hello**`}) + * @see #markdown(SpannableConfiguration, String) + * @see #setText(TextView, CharSequence) + * @see SpannableConfiguration + * @since 1.0.0 + */ public static void setMarkdown( @NonNull TextView view, @NonNull SpannableConfiguration configuration, @@ -38,6 +60,15 @@ public abstract class Markwon { setText(view, markdown(configuration, markdown)); } + /** + * Helper method to apply parsed markdown. Please note, that if images or tables are used + * + * @param view {@link TextView} to set markdown into + * @param text parsed markdown + * @see #scheduleDrawables(TextView) + * @see #scheduleTableRows(TextView) + * @since 1.0.0 + */ public static void setText(@NonNull TextView view, CharSequence text) { unscheduleDrawables(view); @@ -52,7 +83,14 @@ public abstract class Markwon { scheduleTableRows(view); } - // with default configuration + /** + * Returns parsed markdown with default {@link SpannableConfiguration} obtained from {@link Context} + * + * @param context {@link Context} + * @param markdown raw markdown + * @return parsed markdown + * @since 1.0.0 + */ public static CharSequence markdown(@NonNull Context context, @Nullable String markdown) { final CharSequence out; if (TextUtils.isEmpty(markdown)) { @@ -64,6 +102,15 @@ public abstract class Markwon { return out; } + /** + * Returns parsed markdown with provided {@link SpannableConfiguration} + * + * @param configuration a {@link SpannableConfiguration} + * @param markdown raw markdown + * @return parsed markdown + * @see SpannableConfiguration + * @since 1.0.0 + */ public static CharSequence markdown(@NonNull SpannableConfiguration configuration, @Nullable String markdown) { final CharSequence out; if (TextUtils.isEmpty(markdown)) { @@ -77,18 +124,64 @@ public abstract class Markwon { return out; } + /** + * This method adds support for {@link ru.noties.markwon.spans.AsyncDrawable} to be used. As + * textView seems not to support drawables that change bounds (and gives no means + * to update the layout), we create own {@link android.graphics.drawable.Drawable.Callback} + * and apply it. So, textView can display drawables, that are: async (loading from disk, network); + * dynamic (requires `invalidate`) - GIF, animations. + * Please note, that this method should be preceded with {@link #unscheduleDrawables(TextView)} + * in order to avoid keeping drawables in memory after they have been removed from layout + * + * @param view a {@link TextView} + * @see ru.noties.markwon.spans.AsyncDrawable + * @see ru.noties.markwon.spans.AsyncDrawableSpan + * @see DrawablesScheduler#schedule(TextView) + * @see DrawablesScheduler#unschedule(TextView) + * @since 1.0.0 + */ public static void scheduleDrawables(@NonNull TextView view) { DrawablesScheduler.schedule(view); } + /** + * De-references previously scheduled {@link ru.noties.markwon.spans.AsyncDrawableSpan}'s + * + * @param view a {@link TextView} + * @see #scheduleDrawables(TextView) + * @since 1.0.0 + */ public static void unscheduleDrawables(@NonNull TextView view) { DrawablesScheduler.unschedule(view); } + /** + * This method is required in order to use tables. A bit of background: + * this library uses a {@link android.text.style.ReplacementSpan} to + * render tables, but the flow is not really flexible. We are required + * to return `size` (width) of our replacement, but we are not provided + * with the total one (canvas width). In order to correctly calculate height of our + * table cell text, we must have available width first. This method gives + * ability for {@link ru.noties.markwon.spans.TableRowSpan} to invalidate + * `view` when it encounters such a situation (when available width is not known or have changed). + * Precede this call with {@link #unscheduleTableRows(TextView)} in order to + * de-reference previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s + * + * @param view a {@link TextView} + * @see #unscheduleTableRows(TextView) + * @since 1.0.0 + */ public static void scheduleTableRows(@NonNull TextView view) { TableRowsScheduler.schedule(view); } + /** + * De-references previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s + * + * @param view a {@link TextView} + * @see #scheduleTableRows(TextView) + * @since 1.0.0 + */ public static void unscheduleTableRows(@NonNull TextView view) { TableRowsScheduler.unschedule(view); } diff --git a/library/src/main/java/ru/noties/markwon/TableRowsScheduler.java b/library/src/main/java/ru/noties/markwon/TableRowsScheduler.java index ac931df1..6fc9a584 100644 --- a/library/src/main/java/ru/noties/markwon/TableRowsScheduler.java +++ b/library/src/main/java/ru/noties/markwon/TableRowsScheduler.java @@ -6,6 +6,7 @@ import android.text.TextUtils; import android.view.View; import android.widget.TextView; +import ru.noties.markwon.renderer.R; import ru.noties.markwon.spans.TableRowSpan; abstract class TableRowsScheduler { @@ -14,24 +15,32 @@ abstract class TableRowsScheduler { final Object[] spans = extract(view); if (spans != null && spans.length > 0) { - view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - } + if (view.getTag(R.id.markwon_tables_scheduler) == null) { + final View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + + } + + @Override + public void onViewDetachedFromWindow(View v) { + unschedule(view); + view.removeOnAttachStateChangeListener(this); + view.setTag(R.id.markwon_tables_scheduler, null); + } + }; + view.addOnAttachStateChangeListener(listener); + view.setTag(R.id.markwon_tables_scheduler, listener); + } - @Override - public void onViewDetachedFromWindow(View v) { - unschedule(view); - view.removeOnAttachStateChangeListener(this); - } - }); final TableRowSpan.Invalidator invalidator = new TableRowSpan.Invalidator() { @Override public void invalidate() { view.setText(view.getText()); } }; + for (Object span : spans) { ((TableRowSpan) span).invalidator(invalidator); } diff --git a/library/src/main/res/values/ids.xml b/library/src/main/res/values/ids.xml new file mode 100644 index 00000000..de911baf --- /dev/null +++ b/library/src/main/res/values/ids.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file