Merge pull request #105 from noties/v3.0.0
# 3.0.0 * Plugins, plugins, plugins * Split basic functionality blocks into standalone modules * Maven artifacts group changed to `ru.noties.markwon` (previously had been `ru.noties`) * removed `markwon`, `markwon-image-loader`, `markwon-html-pareser-api`, `markwon-html-parser-impl`, `markwon-view` modules * new module system: `core`, `ext-latex`, `ext-strikethrough`, `ext-tables`, `ext-tasklist`, `html`, `image-gif`, `image-okhttp`, `image-svg`, `recycler`, `recycler-table`, `syntax-highlight` * Add BufferType option for Markwon configuration * Fix typo in AsyncDrawable waitingForDimensions * New tests format * `Markwon.render` returns `Spanned` instance of generic `CharSequence` * LinkMovementMethod is applied implicitly if not set on a TextView explicitly * Split code and codeBlock spans and factories * Add CustomTypefaceSpan * Add NoCopySpansFactory * Add placeholder to image loading Generally speaking there are a lot of changes. Most of them are not backwards-compatible. The main point of this release is the `Plugin` system that allows more fluent configuration and opens the possibility of extending `Markwon` with 3rd party functionality in a simple and intuitive fashion. Please refer to the [documentation web-site](https://noties.github.io/Markwon) that has information on how to start migration. The shortest excerpt of this release can be expressed like this: ```java // previous v2.x.x way Markwon.setMarkdown(textView, "**Hello there!**"); ``` ```java // 3.x.x Markwon.create(context) .setMarkdown(textView, "**Hello there!**"); ``` But there is much more to it, please visit documentation web-site to get the full picture of latest changes.
@ -10,7 +10,7 @@ android:
|
||||
- tools
|
||||
|
||||
- build-tools-28.0.3
|
||||
- android-27
|
||||
- android-28
|
||||
|
||||
branches:
|
||||
except:
|
||||
|
19
README.md
@ -2,11 +2,6 @@
|
||||
|
||||
# Markwon
|
||||
|
||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22)
|
||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22)
|
||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22)
|
||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22)
|
||||
|
||||
[](https://travis-ci.org/noties/Markwon)
|
||||
|
||||
**Markwon** is a markdown library for Android. It parses markdown
|
||||
@ -32,15 +27,20 @@ features listed in [commonmark-spec] are supported
|
||||
[sample-apk]: https://github.com/noties/Markwon/releases
|
||||
|
||||
## Installation
|
||||
|
||||

|
||||

|
||||
|
||||
```groovy
|
||||
implementation "ru.noties:markwon:${markwonVersion}"
|
||||
implementation "ru.noties:markwon-image-loader:${markwonVersion}" // optional
|
||||
implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" // optional
|
||||
implementation "ru.noties:markwon-view:${markwonVersion}" // optional
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
```
|
||||
|
||||
Please visit [documentation] web-site for further reference
|
||||
|
||||
|
||||
> You can find previous version of Markwon in [2.x.x](https://github.com/noties/Markwon/tree/2.x.x) branch
|
||||
|
||||
|
||||
## Supported markdown features:
|
||||
* Emphasis (`*`, `_`)
|
||||
* Strong emphasis (`**`, `__`)
|
||||
@ -55,6 +55,7 @@ Please visit [documentation] web-site for further reference
|
||||
* Code blocks
|
||||
* Tables (*with limitations*)
|
||||
* Syntax highlight
|
||||
* LaTeX formulas
|
||||
* HTML
|
||||
* Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`)
|
||||
* Strong emphasis (`<b>`, `<strong>`)
|
||||
|
@ -11,7 +11,7 @@ android {
|
||||
targetSdkVersion config['target-sdk']
|
||||
versionCode 1
|
||||
versionName version
|
||||
setProperty("archivesBaseName", "markwon-sample-$versionName")
|
||||
setProperty("archivesBaseName", "markwon-$versionName")
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@ -28,8 +28,13 @@ android {
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation project(':markwon')
|
||||
implementation project(':markwon-image-loader')
|
||||
implementation project(':markwon-core')
|
||||
implementation project(':markwon-ext-strikethrough')
|
||||
implementation project(':markwon-ext-tables')
|
||||
implementation project(':markwon-ext-tasklist')
|
||||
implementation project(':markwon-html')
|
||||
implementation project(':markwon-image-gif')
|
||||
implementation project(':markwon-image-svg')
|
||||
implementation project(':markwon-syntax-highlight')
|
||||
|
||||
deps.with {
|
||||
|
@ -9,7 +9,7 @@ import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import ru.noties.markwon.R;
|
||||
import ru.noties.markwon.spans.TaskListDrawable;
|
||||
import ru.noties.markwon.ext.tasklist.TaskListDrawable;
|
||||
|
||||
public class DebugCheckboxDrawableView extends View {
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="ru.noties.markwon">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -10,8 +11,10 @@
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppThemeLight">
|
||||
android:theme="@style/AppThemeLight"
|
||||
tools:ignore="AllowBackup">
|
||||
|
||||
<activity android:name=".MainActivity">
|
||||
|
||||
@ -38,21 +41,6 @@
|
||||
android:host="*"
|
||||
android:scheme="https" />
|
||||
|
||||
<!--<data-->
|
||||
<!--android:host="*"-->
|
||||
<!--android:scheme="http"-->
|
||||
<!--android:mimeType="text/markdown"/>-->
|
||||
|
||||
<!--<data-->
|
||||
<!--android:host="*"-->
|
||||
<!--android:scheme="file"-->
|
||||
<!--android:mimeType="text/markdown"/>-->
|
||||
|
||||
<!--<data-->
|
||||
<!--android:host="*"-->
|
||||
<!--android:scheme="https"-->
|
||||
<!--android:mimeType="text/markdown"/>-->
|
||||
|
||||
<data android:pathPattern=".*\\.markdown" />
|
||||
<data android:pathPattern=".*\\.mdown" />
|
||||
<data android:pathPattern=".*\\.mkdn" />
|
||||
|
@ -23,8 +23,8 @@ abstract class AppBarItem {
|
||||
final TextView subtitle;
|
||||
|
||||
Renderer(@NonNull View view, @NonNull View.OnClickListener themeChangeClicked) {
|
||||
this.title = Views.findView(view, R.id.app_bar_title);
|
||||
this.subtitle = Views.findView(view, R.id.app_bar_subtitle);
|
||||
this.title = view.findViewById(R.id.app_bar_title);
|
||||
this.subtitle = view.findViewById(R.id.app_bar_subtitle);
|
||||
view.findViewById(R.id.app_bar_theme_changer)
|
||||
.setOnClickListener(themeChangeClicked);
|
||||
}
|
||||
|
@ -14,11 +14,6 @@ import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.OkHttpClient;
|
||||
import ru.noties.markwon.il.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.il.GifMediaDecoder;
|
||||
import ru.noties.markwon.il.ImageMediaDecoder;
|
||||
import ru.noties.markwon.il.SvgMediaDecoder;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.syntax.Prism4jThemeDarkula;
|
||||
import ru.noties.markwon.syntax.Prism4jThemeDefault;
|
||||
import ru.noties.prism4j.Prism4j;
|
||||
@ -72,23 +67,6 @@ class AppModule {
|
||||
return new UriProcessorImpl();
|
||||
}
|
||||
|
||||
@Provides
|
||||
AsyncDrawable.Loader asyncDrawableLoader(
|
||||
OkHttpClient client,
|
||||
ExecutorService executorService,
|
||||
Resources resources) {
|
||||
return AsyncDrawableLoader.builder()
|
||||
.client(client)
|
||||
.executorService(executorService)
|
||||
.resources(resources)
|
||||
.mediaDecoders(
|
||||
SvgMediaDecoder.create(resources),
|
||||
GifMediaDecoder.create(false),
|
||||
ImageMediaDecoder.create(resources)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Prism4j prism4j() {
|
||||
@ -104,12 +82,12 @@ class AppModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
Prism4jThemeDarkula prism4jThemeDarkula() {
|
||||
return Prism4jThemeDarkula.create();
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
GifProcessor gifProcessor() {
|
||||
return GifProcessor.create();
|
||||
return Prism4jThemeDarkula.create(0x0Fffffff);
|
||||
}
|
||||
//
|
||||
// @Singleton
|
||||
// @Provides
|
||||
// GifProcessor gifProcessor() {
|
||||
// return GifProcessor.create();
|
||||
// }
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.renderer.ImageSize;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.AsyncDrawableSpan;
|
||||
import ru.noties.markwon.spans.SpannableTheme;
|
||||
|
||||
public class GifAwareSpannableFactory extends SpannableFactoryDef {
|
||||
|
||||
private final GifPlaceholder gifPlaceholder;
|
||||
|
||||
public GifAwareSpannableFactory(@NonNull GifPlaceholder gifPlaceholder) {
|
||||
this.gifPlaceholder = gifPlaceholder;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
|
||||
return new AsyncDrawableSpan(
|
||||
theme,
|
||||
new GifAwareAsyncDrawable(
|
||||
gifPlaceholder,
|
||||
destination,
|
||||
loader,
|
||||
imageSizeResolver,
|
||||
imageSize
|
||||
),
|
||||
AsyncDrawableSpan.ALIGN_BOTTOM,
|
||||
replacementTextIsLink
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spanned;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
@ -27,9 +28,6 @@ public class MainActivity extends Activity {
|
||||
@Inject
|
||||
UriProcessor uriProcessor;
|
||||
|
||||
@Inject
|
||||
GifProcessor gifProcessor;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@ -40,9 +38,6 @@ public class MainActivity extends Activity {
|
||||
|
||||
themes.apply(this);
|
||||
|
||||
// how can we obtain SpannableConfiguration after theme was applied?
|
||||
// as we inject `themes` we won't be able to inject configuration, as it requires theme set
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
// we process additionally github urls, as if url has in path `blob`, we won't receive
|
||||
@ -58,7 +53,7 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
});
|
||||
|
||||
final TextView textView = Views.findView(this, R.id.text);
|
||||
final TextView textView = findViewById(R.id.text);
|
||||
final View progress = findViewById(R.id.progress);
|
||||
|
||||
appBarRenderer.render(appBarState());
|
||||
@ -68,11 +63,9 @@ public class MainActivity extends Activity {
|
||||
public void apply(final String text) {
|
||||
markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() {
|
||||
@Override
|
||||
public void onMarkdownReady(CharSequence markdown) {
|
||||
public void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown) {
|
||||
|
||||
Markwon.setText(textView, markdown);
|
||||
|
||||
gifProcessor.process(textView);
|
||||
markwon.setParsedMarkdown(textView, markdown);
|
||||
|
||||
Views.setVisible(progress, false);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
@ -13,24 +14,30 @@ import java.util.concurrent.Future;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import ru.noties.debug.Debug;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.SpannableTheme;
|
||||
import ru.noties.markwon.syntax.Prism4jSyntaxHighlight;
|
||||
import ru.noties.markwon.core.CorePlugin;
|
||||
import ru.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import ru.noties.markwon.ext.tables.TablePlugin;
|
||||
import ru.noties.markwon.ext.tasklist.TaskListPlugin;
|
||||
import ru.noties.markwon.gif.GifAwarePlugin;
|
||||
import ru.noties.markwon.html.HtmlPlugin;
|
||||
import ru.noties.markwon.image.ImagesPlugin;
|
||||
import ru.noties.markwon.image.gif.GifPlugin;
|
||||
import ru.noties.markwon.image.svg.SvgPlugin;
|
||||
import ru.noties.markwon.syntax.Prism4jTheme;
|
||||
import ru.noties.markwon.syntax.Prism4jThemeDarkula;
|
||||
import ru.noties.markwon.syntax.Prism4jThemeDefault;
|
||||
import ru.noties.markwon.syntax.SyntaxHighlightPlugin;
|
||||
import ru.noties.markwon.urlprocessor.UrlProcessor;
|
||||
import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
|
||||
import ru.noties.prism4j.Prism4j;
|
||||
|
||||
@ActivityScope
|
||||
public class MarkdownRenderer {
|
||||
|
||||
interface MarkdownReadyListener {
|
||||
void onMarkdownReady(CharSequence markdown);
|
||||
void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown);
|
||||
}
|
||||
|
||||
@Inject
|
||||
AsyncDrawable.Loader loader;
|
||||
|
||||
@Inject
|
||||
ExecutorService service;
|
||||
|
||||
@ -64,9 +71,17 @@ public class MarkdownRenderer {
|
||||
cancel();
|
||||
|
||||
task = service.submit(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
execute();
|
||||
} catch (Throwable t) {
|
||||
Debug.e(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void execute() {
|
||||
final UrlProcessor urlProcessor;
|
||||
if (uri == null) {
|
||||
urlProcessor = new UrlProcessorInitialReadme();
|
||||
@ -78,29 +93,28 @@ public class MarkdownRenderer {
|
||||
? prism4jThemeDefault
|
||||
: prism4JThemeDarkula;
|
||||
|
||||
final int background = isLightTheme
|
||||
? prism4jTheme.background()
|
||||
: 0x0Fffffff;
|
||||
|
||||
final GifPlaceholder gifPlaceholder = new GifPlaceholder(
|
||||
context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white),
|
||||
0x20000000
|
||||
);
|
||||
|
||||
final SpannableConfiguration configuration = SpannableConfiguration.builder(context)
|
||||
.asyncDrawableLoader(loader)
|
||||
.urlProcessor(urlProcessor)
|
||||
.syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme))
|
||||
.theme(SpannableTheme.builderWithDefaults(context)
|
||||
.codeBackgroundColor(background)
|
||||
.codeTextColor(prism4jTheme.textColor())
|
||||
.build())
|
||||
.factory(new GifAwareSpannableFactory(gifPlaceholder))
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(CorePlugin.create())
|
||||
.usePlugin(ImagesPlugin.createWithAssets(context))
|
||||
.usePlugin(SvgPlugin.create(context.getResources()))
|
||||
.usePlugin(GifPlugin.create(false))
|
||||
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
|
||||
.usePlugin(GifAwarePlugin.create(context))
|
||||
.usePlugin(TablePlugin.create(context))
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.urlProcessor(urlProcessor);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
final long start = SystemClock.uptimeMillis();
|
||||
|
||||
final CharSequence text = Markwon.markdown(configuration, markdown);
|
||||
final Spanned text = markwon.toMarkdown(markdown);
|
||||
|
||||
final long end = SystemClock.uptimeMillis();
|
||||
|
||||
@ -111,7 +125,7 @@ public class MarkdownRenderer {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isCancelled()) {
|
||||
listener.onMarkdownReady(text);
|
||||
listener.onMarkdownReady(markwon, text);
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import ru.noties.markwon.urlprocessor.UrlProcessor;
|
||||
import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
|
||||
|
||||
class UrlProcessorInitialReadme implements UrlProcessor {
|
||||
|
||||
private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/";
|
||||
|
@ -1,7 +1,5 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
@ -13,16 +11,6 @@ public abstract class Views {
|
||||
@interface NotVisible {
|
||||
}
|
||||
|
||||
public static <V extends View> V findView(@NonNull View view, @IdRes int id) {
|
||||
//noinspection unchecked
|
||||
return (V) view.findViewById(id);
|
||||
}
|
||||
|
||||
public static <V extends View> V findView(@NonNull Activity activity, @IdRes int id) {
|
||||
//noinspection unchecked
|
||||
return (V) activity.findViewById(id);
|
||||
}
|
||||
|
||||
public static void setVisible(@NonNull View view, boolean visible) {
|
||||
setVisible(view, visible, View.GONE);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon;
|
||||
package ru.noties.markwon.gif;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@ -6,9 +6,10 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import pl.droidsonroids.gif.GifDrawable;
|
||||
import ru.noties.markwon.renderer.ImageSize;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.image.ImageSize;
|
||||
import ru.noties.markwon.image.ImageSizeResolver;
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
|
||||
public class GifAwareAsyncDrawable extends AsyncDrawable {
|
||||
|
||||
@ -23,7 +24,7 @@ public class GifAwareAsyncDrawable extends AsyncDrawable {
|
||||
public GifAwareAsyncDrawable(
|
||||
@NonNull Drawable gifPlaceholder,
|
||||
@NonNull String destination,
|
||||
@NonNull Loader loader,
|
||||
@NonNull AsyncDrawableLoader loader,
|
||||
@Nullable ImageSizeResolver imageSizeResolver,
|
||||
@Nullable ImageSize imageSize) {
|
||||
super(destination, loader, imageSizeResolver, imageSize);
|
72
app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java
Normal file
@ -0,0 +1,72 @@
|
||||
package ru.noties.markwon.gif;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Image;
|
||||
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin;
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.MarkwonSpansFactory;
|
||||
import ru.noties.markwon.R;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.image.AsyncDrawableSpan;
|
||||
import ru.noties.markwon.image.ImageProps;
|
||||
import ru.noties.markwon.image.ImagesPlugin;
|
||||
import ru.noties.markwon.priority.Priority;
|
||||
|
||||
public class GifAwarePlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
public static GifAwarePlugin create(@NonNull Context context) {
|
||||
return new GifAwarePlugin(context);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final GifProcessor processor;
|
||||
|
||||
GifAwarePlugin(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.processor = GifProcessor.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
|
||||
final GifPlaceholder gifPlaceholder = new GifPlaceholder(
|
||||
context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white),
|
||||
0x20000000
|
||||
);
|
||||
|
||||
builder.setFactory(Image.class, new SpanFactory() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
return new AsyncDrawableSpan(
|
||||
configuration.theme(),
|
||||
new GifAwareAsyncDrawable(
|
||||
gifPlaceholder,
|
||||
ImageProps.DESTINATION.require(props),
|
||||
configuration.asyncDrawableLoader(),
|
||||
configuration.imageSizeResolver(),
|
||||
ImageProps.IMAGE_SIZE.get(props)
|
||||
),
|
||||
AsyncDrawableSpan.ALIGN_BOTTOM,
|
||||
ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Priority priority() {
|
||||
return Priority.after(ImagesPlugin.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
processor.process(textView);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon;
|
||||
package ru.noties.markwon.gif;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon;
|
||||
package ru.noties.markwon.gif;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@ -9,7 +9,7 @@ import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import pl.droidsonroids.gif.GifDrawable;
|
||||
import ru.noties.markwon.spans.AsyncDrawableSpan;
|
||||
import ru.noties.markwon.image.AsyncDrawableSpan;
|
||||
|
||||
public abstract class GifProcessor {
|
||||
|
||||
@ -31,6 +31,7 @@ public abstract class GifProcessor {
|
||||
// if not we apply onGifListener
|
||||
|
||||
final Spannable spannable = spannable(textView);
|
||||
|
||||
if (spannable == null) {
|
||||
return;
|
||||
}
|
||||
@ -89,6 +90,7 @@ public abstract class GifProcessor {
|
||||
// as with each `setText()` new spannable is created and keeping reference
|
||||
// to an older one won't affect textView
|
||||
final Spannable spannable = spannable(textView);
|
||||
|
||||
if (spannable == null) {
|
||||
return;
|
||||
}
|
||||
@ -113,12 +115,13 @@ public abstract class GifProcessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (gifDrawable.isPlaying()) {
|
||||
gifDrawable.pause();
|
||||
} else {
|
||||
gifDrawable.start();
|
||||
}
|
||||
widget.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
24
app/src/main/res/drawable-v26/ic_launcher_background.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M0,0h512v256h-512z"
|
||||
android:strokeAlpha="0.94117647"
|
||||
android:strokeWidth="0.40000001"
|
||||
android:fillColor="#d7d7d7"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M0,256h512v256h-512z"
|
||||
android:strokeAlpha="0.94117647"
|
||||
android:strokeWidth="0.40000001"
|
||||
android:fillColor="#eeeeee"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
24
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M0,0h512v256h-512z"
|
||||
android:strokeAlpha="0.94117647"
|
||||
android:strokeWidth="0.40000001"
|
||||
android:fillColor="#d7d7d7"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M0,256h512v256h-512z"
|
||||
android:strokeAlpha="0.94117647"
|
||||
android:strokeWidth="0.40000001"
|
||||
android:fillColor="#eeeeee"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.5 KiB |
97
art/markwon-icon-foreground.svg
Normal file
@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512.00001 512.00001"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
inkscape:export-filename="/Users/di/text4169.png"
|
||||
inkscape:export-xdpi="89.93"
|
||||
inkscape:export-ydpi="89.93"
|
||||
sodipodi:docname="markwon-icon-foreground.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.92578125"
|
||||
inkscape:cx="163.7657"
|
||||
inkscape:cy="186.451"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1442"
|
||||
inkscape:window-height="788"
|
||||
inkscape:window-x="-1"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-540.36216)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:34.96873856px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="106.24741"
|
||||
y="908.6958"
|
||||
id="text4136"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4138"
|
||||
x="106.24741"
|
||||
y="908.6958"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:314.71862793px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif Bold'">M</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:65.1031189px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
|
||||
x="109.9856"
|
||||
y="780.45221"
|
||||
id="text4140"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4142"
|
||||
x="109.9856"
|
||||
y="780.45221"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:293.75px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif';fill:#666666;">**</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:40.77807617px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
|
||||
x="109.9856"
|
||||
y="1150.7955"
|
||||
id="text4169"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4171"
|
||||
x="109.9856"
|
||||
y="1150.7955"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:293.75px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif';fill:#666666;">**</tspan></text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
97
art/sample-icon-foreground.svg
Normal file
@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512.00001 512.00001"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
inkscape:export-filename="/Users/di/text4169.png"
|
||||
inkscape:export-xdpi="89.93"
|
||||
inkscape:export-ydpi="89.93"
|
||||
sodipodi:docname="sample-icon-foreground.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.92578125"
|
||||
inkscape:cx="163.7657"
|
||||
inkscape:cy="186.451"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1442"
|
||||
inkscape:window-height="788"
|
||||
inkscape:window-x="-1"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-540.36216)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:34.96873856px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
|
||||
x="106.24741"
|
||||
y="908.6958"
|
||||
id="text4136"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4138"
|
||||
x="106.24741"
|
||||
y="908.6958"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:314.71862793px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif Bold';fill:#ffffff;">M</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:65.1031189px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#999999;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
|
||||
x="109.9856"
|
||||
y="780.45221"
|
||||
id="text4140"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4142"
|
||||
x="109.9856"
|
||||
y="780.45221"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:293.75px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif';fill:#999999;">**</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:40.77807617px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#999999;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
|
||||
x="109.9856"
|
||||
y="1150.7955"
|
||||
id="text4169"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4171"
|
||||
x="109.9856"
|
||||
y="1150.7955"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:293.75px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif';fill:#999999;">**</tspan></text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
28
build.gradle
@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,10 @@ allprojects {
|
||||
}
|
||||
version = VERSION_NAME
|
||||
group = GROUP
|
||||
|
||||
tasks.withType(Javadoc) {
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
@ -43,27 +47,30 @@ ext {
|
||||
// NB, updating build-tools or compile-sdk will require updating Travis config (.travis.yml)
|
||||
config = [
|
||||
'build-tools' : '28.0.3',
|
||||
'compile-sdk' : 27,
|
||||
'target-sdk' : 27,
|
||||
'compile-sdk' : 28,
|
||||
'target-sdk' : 28,
|
||||
'min-sdk' : 16,
|
||||
'push-aar-gradle': 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle'
|
||||
]
|
||||
|
||||
final def supportVersion = '27.1.1'
|
||||
final def supportVersion = '28.0.0'
|
||||
final def commonMarkVersion = '0.12.1'
|
||||
final def daggerVersion = '2.10'
|
||||
|
||||
deps = [
|
||||
'support-annotations' : "com.android.support:support-annotations:$supportVersion",
|
||||
'support-app-compat' : "com.android.support:appcompat-v7:$supportVersion",
|
||||
'support-recycler-view' : "com.android.support:recyclerview-v7:$supportVersion",
|
||||
'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",
|
||||
'android-svg' : 'com.caverock:androidsvg:1.2.1',
|
||||
'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.14',
|
||||
'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.0',
|
||||
'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0',
|
||||
'prism4j' : 'ru.noties:prism4j:1.1.0',
|
||||
'debug' : 'ru.noties:debug:3.0.0@jar',
|
||||
'adapt' : 'ru.noties:adapt:1.1.0',
|
||||
'dagger' : "com.google.dagger:dagger:$daggerVersion"
|
||||
]
|
||||
|
||||
@ -73,14 +80,11 @@ ext {
|
||||
]
|
||||
|
||||
deps['test'] = [
|
||||
'junit' : 'junit:junit:4.12',
|
||||
'robolectric' : 'org.robolectric:robolectric:3.8',
|
||||
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
|
||||
'jackson-yaml' : 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.0',
|
||||
'jackson-databind': 'com.fasterxml.jackson.core:jackson-databind:2.9.6',
|
||||
'gson' : 'com.google.code.gson:gson:2.8.5',
|
||||
'commons-io' : 'commons-io:commons-io:2.6',
|
||||
'mockito' : 'org.mockito:mockito-core:2.21.0'
|
||||
'junit' : 'junit:junit:4.12',
|
||||
'robolectric': 'org.robolectric:robolectric:3.8',
|
||||
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
|
||||
'commons-io' : 'commons-io:commons-io:2.6',
|
||||
'mockito' : 'org.mockito:mockito-core:2.21.0'
|
||||
]
|
||||
|
||||
registerArtifact = this.®isterArtifact
|
||||
|
4
docs/.vuepress/.artifacts.js
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
|
||||
const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"ru.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
|
||||
export { artifacts };
|
105
docs/.vuepress/components/ArtifactPicker.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="artifact-container">
|
||||
<div v-for="artifact in artifacts" class="artifact" @click="toggleSelection(artifact)">
|
||||
<div class="artifact-header">
|
||||
<input type="checkbox" v-model="selected" :value="artifact.id" :id="artifact.id">
|
||||
<strong>
|
||||
<label :for="artifact.id">{{artifact.name}}</label>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="artifact-description" v-if="artifact.description">{{artifact.description}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra-class language-gradle selected-artifacts" v-if="selected.length > 0">
|
||||
<div class="selected-artifact-script">
|
||||
<span class="token keyword">final def</span>
|
||||
<span> markwon_version = </span>
|
||||
<span class="token string">'{{latestVersion}}'</span>
|
||||
</div>
|
||||
<br>
|
||||
<div class="selected-artifact-script" v-for="artifact in selectedArtifacts">
|
||||
<span>implementation </span>
|
||||
<span class="token string">"{{artifact.group}}:{{artifact.id}}:</span>
|
||||
<span>$markwon_version</span>
|
||||
<span class="token string">"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { artifacts } from "../.artifacts.js";
|
||||
|
||||
if (!artifacts) {
|
||||
throw "Artifacts not found. Use `collectArtifacts.js` script to obtain artifacts metadata.";
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "ArtifactPicker",
|
||||
data() {
|
||||
return {
|
||||
artifacts,
|
||||
selected: ["core"],
|
||||
latestVersion: "latest_version"
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleSelection(artifact) {
|
||||
const index = this.selected.indexOf(artifact.id);
|
||||
if (index < 0) {
|
||||
this.selected.push(artifact.id);
|
||||
} else {
|
||||
this.selected.splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedArtifacts() {
|
||||
return this.artifacts.filter(a => this.selected.indexOf(a.id) >= 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.artifact-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.artifact {
|
||||
flex: 1;
|
||||
border: 1px #ccc solid;
|
||||
background-color: #fafafa;
|
||||
padding: 0.5em;
|
||||
margin: 0.2em;
|
||||
border-radius: 0.25em;
|
||||
min-width: 10em;
|
||||
max-width: 10em;
|
||||
}
|
||||
.artifact-description {
|
||||
font-size: 0.85em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.selected-artifacts {
|
||||
color: white;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
hyphens: none;
|
||||
font-size: 0.85em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.selected-artifact-script {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
105
docs/.vuepress/components/CommonmarkSandbox.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="container">
|
||||
<div class="container-item">
|
||||
<textarea @input="processMarkdown">{{markdownInput}}</textarea>
|
||||
</div>
|
||||
<div class="container-item display" v-html="markdownHtml"></div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<!-- <p v-if="permalink">
|
||||
Permalink: <span v-html="permalink"></span>
|
||||
</p> -->
|
||||
<p>
|
||||
<em>
|
||||
* Please note that this tool can be used to evaluate how commonmark
|
||||
will react to certain markdown input. There is no guarantee that results
|
||||
here and in an Android application that uses Markwon will be the same.
|
||||
Especially if raw HTML is involved.
|
||||
</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
** For a more sophisticated commonmark sandbox editor
|
||||
<a
|
||||
href="https://spec.commonmark.org/dingus/"
|
||||
>the dingus</a> can be used.
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import commonmark from "commonmark";
|
||||
|
||||
const parser = new commonmark.Parser();
|
||||
const writer = new commonmark.HtmlRenderer();
|
||||
|
||||
export default {
|
||||
name: "CommonmarkSandbox",
|
||||
data() {
|
||||
return {
|
||||
markdownInput: this.initialMarkdown()
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
initialMarkdown() {
|
||||
// const query = this.$route.query;
|
||||
// if (query) {
|
||||
// const md = query.md;
|
||||
// if (md) {
|
||||
// query.md = null;
|
||||
// return md;
|
||||
// }
|
||||
// }
|
||||
return `# Header 1\n\n*Hello* __there!__`;
|
||||
},
|
||||
processMarkdown(e) {
|
||||
this.markdownInput = e.target.value;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
markdownHtml() {
|
||||
return writer.render(parser.parse(this.markdownInput));
|
||||
},
|
||||
// permalink() {
|
||||
// if (!this.markdownInput) {
|
||||
// return null;
|
||||
// }
|
||||
// const url = `${window.location.href}?md=${encodeURIComponent(this.markdownInput)}`;
|
||||
// return `<a href="#" title="${url}" onclick="">click to copy</a>`;
|
||||
// }
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.container-item {
|
||||
flex: 4;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.container textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
resize: vertical;
|
||||
min-height: 20em;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
.display {
|
||||
flex: 5;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.footer {
|
||||
color: #666;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
13
docs/.vuepress/components/LegacyWarning.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="warning custom-block">
|
||||
<p class="custom-block-title">WARNING</p>
|
||||
<p>This is documentation for <u>legacy 2.x.x</u> versions. For the most current version <a :href="$withBase('/')">click here.</a></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LegacyWarning'
|
||||
}
|
||||
</script>
|
||||
|
@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<a :href="linkHref()" target="_blank" rel="noopener noreferrer">{{linkText()}}<OutboundLink/></a>
|
||||
<a :href="linkHref()" target="_blank" rel="noopener noreferrer">
|
||||
{{linkText()}}
|
||||
<OutboundLink/>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -9,34 +12,36 @@ var map = {
|
||||
href: "https://spec.commonmark.org/0.28/"
|
||||
},
|
||||
"commonmark-spec#inline": {
|
||||
href: "https://spec.commonmark.org/0.28/#raw-html"
|
||||
href: "https://spec.commonmark.org/0.28/#raw-html"
|
||||
},
|
||||
"commonmark-spec#block": {
|
||||
href: "https://spec.commonmark.org/0.28/#html-blocks"
|
||||
href: "https://spec.commonmark.org/0.28/#html-blocks"
|
||||
},
|
||||
"commonmark-spec#soft-break": {
|
||||
href: "https://spec.commonmark.org/0.28/#soft-line-breaks"
|
||||
href: "https://spec.commonmark.org/0.28/#soft-line-breaks"
|
||||
},
|
||||
"commonmark-dingus": {
|
||||
displayName: "commonmark dingus",
|
||||
href: "https://spec.commonmark.org/dingus/"
|
||||
displayName: "commonmark dingus",
|
||||
href: "https://spec.commonmark.org/dingus/"
|
||||
},
|
||||
"html-inlines": {
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements"
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements"
|
||||
},
|
||||
"html-blocks": {
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements"
|
||||
href:
|
||||
"https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements"
|
||||
},
|
||||
"jsoup": {
|
||||
displayName: "Jsoup",
|
||||
href: "https://github.com/jhy/jsoup/"
|
||||
jsoup: {
|
||||
displayName: "Jsoup",
|
||||
href: "https://github.com/jhy/jsoup/"
|
||||
},
|
||||
"markwon-jsoup": {
|
||||
href: "https://github.com/noties/Markwon/tree/master/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup"
|
||||
href:
|
||||
"https://github.com/noties/Markwon/tree/master/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup"
|
||||
},
|
||||
"commonmark-java": {
|
||||
href: "https://github.com/atlassian/commonmark-java/",
|
||||
displayName: "commonmark-java"
|
||||
href: "https://github.com/atlassian/commonmark-java/",
|
||||
displayName: "commonmark-java"
|
||||
}
|
||||
};
|
||||
|
||||
@ -48,7 +53,7 @@ export default {
|
||||
return this.href || map[this.name].href;
|
||||
},
|
||||
linkText: function() {
|
||||
return this.displayName || map[this.name].displayName;
|
||||
return this.displayName || map[this.name].displayName;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<a :href="mavenSearchUrl()"><img :src="shieldImgageUrl()" :alt="'' + artifact"></a>
|
||||
<a :href="mavenSearchUrl()"><img :src="shieldImgageUrl()" :alt="displayLabel"></a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MavenBadge',
|
||||
props: ['artifact'],
|
||||
props: ['artifact', 'label'],
|
||||
methods: {
|
||||
mavenSearchUrl: function() {
|
||||
return 'http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22' + this.artifact + '%22';
|
||||
return `http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20AND%20a%3A%22${this.artifact}%22`;
|
||||
},
|
||||
shieldImgageUrl: function() {
|
||||
return 'https://img.shields.io/maven-central/v/ru.noties/' + this.artifact +'.svg?label=' + this.artifact;
|
||||
return `https://img.shields.io/maven-central/v/ru.noties.markwon/${this.artifact}.svg?label=${this.displayLabel}`;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayLabel() {
|
||||
return this.label || this.artifact;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
docs/.vuepress/components/MavenBadge2xx.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<a :href="mavenSearchUrl()"><img :src="shieldImgageUrl()" :alt="displayLabel"></a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MavenBadge2xx',
|
||||
props: ['artifact', 'label'],
|
||||
methods: {
|
||||
mavenSearchUrl: function() {
|
||||
return `http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22${this.artifact}%22`;
|
||||
},
|
||||
shieldImgageUrl: function() {
|
||||
return `https://img.shields.io/maven-central/v/ru.noties/${this.artifact}.svg?label=${this.displayLabel}`;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayLabel() {
|
||||
return this.label || this.artifact;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
20
docs/.vuepress/components/MavenBadges2xx.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<MavenBadge2xx :artifact="'markwon'" />
|
||||
<MavenBadge2xx :artifact="'markwon-image-loader'" />
|
||||
<MavenBadge2xx :artifact="'markwon-syntax-highlight'"/>
|
||||
<MavenBadge2xx :artifact="'markwon-view'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MavenBadge2xx from "./MavenBadge2xx.vue";
|
||||
export default {
|
||||
name: "MavenBadges2xx",
|
||||
components: {
|
||||
MavenBadge2xx
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -1,37 +1,74 @@
|
||||
module.exports = {
|
||||
base: '/Markwon/',
|
||||
title: 'Markwon',
|
||||
description: 'Android markdown library based on commonmark specification',
|
||||
description: 'Android markdown library based on commonmark specification that renders markdown as system-native Spannables (no WebView)',
|
||||
head: [
|
||||
['link', {rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png?v=1'}],
|
||||
['link', {rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png?v=1'}],
|
||||
['link', {rel: 'icon', href: '/favicon.ico?v=1'}],
|
||||
['link', {rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png?v=1'}],
|
||||
['link', {rel: 'manifest', href: '/manifest.json?v=1'}],
|
||||
['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png?v=1' }],
|
||||
['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png?v=1' }],
|
||||
['link', { rel: 'icon', href: '/favicon.ico?v=1' }],
|
||||
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png?v=1' }],
|
||||
['link', { rel: 'manifest', href: '/manifest.json?v=1' }],
|
||||
['meta', { name: 'keywords', content: 'android,markdown,library,spannable,markwon,commonmark' }]
|
||||
],
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: 'Install', link: '/docs/install.md' },
|
||||
{ text: 'Install', link: '/docs/v3/install.md' },
|
||||
{ text: 'Changelog', link: '/CHANGELOG.md' },
|
||||
{
|
||||
text: 'API Version',
|
||||
items: [
|
||||
{ text: 'Current (3.x.x)', link: '/' },
|
||||
{ text: 'Legacy (2.x.x)', link: '/docs/v2/' }
|
||||
]
|
||||
},
|
||||
{ text: 'Sandbox', link: '/sandbox.md' },
|
||||
{ text: 'Github', link: 'https://github.com/noties/Markwon' }
|
||||
],
|
||||
sidebar: [
|
||||
'/',
|
||||
'/docs/getting-started.md',
|
||||
'/docs/configure.md',
|
||||
'/docs/theme.md',
|
||||
'/docs/factory.md',
|
||||
'/docs/image-loader.md',
|
||||
'/docs/syntax-highlight.md',
|
||||
'/docs/html.md',
|
||||
'/docs/view.md'
|
||||
],
|
||||
sidebar: {
|
||||
'/docs/v2': [
|
||||
'/docs/v2/getting-started.md',
|
||||
'/docs/v2/configure.md',
|
||||
'/docs/v2/theme.md',
|
||||
'/docs/v2/factory.md',
|
||||
'/docs/v2/image-loader.md',
|
||||
'/docs/v2/syntax-highlight.md',
|
||||
'/docs/v2/html.md',
|
||||
'/docs/v2/view.md'
|
||||
],
|
||||
'/': [
|
||||
'',
|
||||
{
|
||||
title: 'Core',
|
||||
collapsable: false,
|
||||
children: [
|
||||
'/docs/v3/core/getting-started.md',
|
||||
'/docs/v3/core/plugins.md',
|
||||
'/docs/v3/core/theme.md',
|
||||
'/docs/v3/core/images.md',
|
||||
'/docs/v3/core/configuration.md',
|
||||
'/docs/v3/core/visitor.md',
|
||||
'/docs/v3/core/spans-factory.md',
|
||||
'/docs/v3/core/html-renderer.md',
|
||||
'/docs/v3/core/core-plugin.md',
|
||||
'/docs/v3/core/movement-method-plugin.md',
|
||||
'/docs/v3/core/render-props.md'
|
||||
]
|
||||
},
|
||||
'/docs/v3/ext-latex/',
|
||||
'/docs/v3/ext-strikethrough/',
|
||||
'/docs/v3/ext-tables/',
|
||||
'/docs/v3/ext-tasklist/',
|
||||
'/docs/v3/html/',
|
||||
'/docs/v3/image/gif.md',
|
||||
'/docs/v3/image/okhttp.md',
|
||||
'/docs/v3/image/svg.md',
|
||||
'/docs/v3/recycler/',
|
||||
'/docs/v3/recycler-table/',
|
||||
'/docs/v3/syntax-highlight/',
|
||||
'/docs/v3/migration-2-3.md'
|
||||
]
|
||||
},
|
||||
sidebarDepth: 2,
|
||||
lastUpdated: true
|
||||
},
|
||||
markdown: {
|
||||
config: md => {
|
||||
md.use(require('markdown-it-task-lists'));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +1,23 @@
|
||||
$textColor = #000000
|
||||
$accentColor = #4CAF50
|
||||
|
||||
a.sidebar-link {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-sub-headers a.sidebar-link {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.sidebar-group a.sidebar-link {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
color: $textColor;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-heading.open, .sidebar-heading:hover {
|
||||
color: $accentColor;
|
||||
}
|
BIN
docs/.vuepress/public/assets/recycler-table-screenshot.png
Normal file
After Width: | Height: | Size: 77 KiB |
@ -0,0 +1,71 @@
|
||||
div[class~=language-gradle]:before {
|
||||
content:"gradle"
|
||||
}
|
||||
|
||||
div[class~=language-proguard]:before {
|
||||
content:"proguard"
|
||||
}
|
||||
|
||||
div[class~=language-groovy]:before {
|
||||
content:"gradle"
|
||||
}
|
||||
|
||||
div[class*="language-"] {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.token.comment, .token.prolog, .token.cdata {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.token.delimiter, .token.boolean, .token.keyword, .token.selector, .token.important, .token.atrule {
|
||||
color: #cc7832;
|
||||
}
|
||||
|
||||
.token.operator, .token.punctuation, .token.attr-name {
|
||||
color: #a9b7c6;
|
||||
}
|
||||
|
||||
.token.tag, .token.doctype, .token.builtin {
|
||||
color: #e8bf6a;
|
||||
}
|
||||
|
||||
.token.entity, .token.number, .token.symbol {
|
||||
color: #6897bb;
|
||||
}
|
||||
|
||||
.token.property, .token.constant, .token.variable {
|
||||
color: #9876aa;
|
||||
}
|
||||
|
||||
.token.string, .token.char {
|
||||
color: #6a8759;
|
||||
}
|
||||
|
||||
.token.annotation {
|
||||
color: #bbb438;
|
||||
}
|
||||
|
||||
.token.attr-value {
|
||||
color: #a5c261;
|
||||
}
|
||||
|
||||
.token.url {
|
||||
color: #287bde;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #ffc66d;
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
color: #364135;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: #294436;
|
||||
}
|
||||
|
||||
.token.deleted {
|
||||
color: #484a4a;
|
||||
}
|
@ -1,17 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
# 2.0.1
|
||||
# 3.0.0
|
||||
* Plugins, plugins, plugins
|
||||
* Split basic functionality blocks into standalone modules
|
||||
* Maven artifacts group changed to `ru.noties.markwon` (previously had been `ru.noties`)
|
||||
* removed `markwon`, `markwon-image-loader`, `markwon-html-pareser-api`, `markwon-html-parser-impl`, `markwon-view` modules
|
||||
* new module system: `core`, `ext-latex`, `ext-strikethrough`, `ext-tables`, `ext-tasklist`, `html`, `image-gif`, `image-okhttp`, `image-svg`, `recycler`, `recycler-table`, `syntax-highlight`
|
||||
* Add BufferType option for Markwon configuration
|
||||
* Fix typo in AsyncDrawable waitingForDimensions
|
||||
* New tests format
|
||||
* `Markwon.render` returns `Spanned` instance of generic `CharSequence`
|
||||
* LinkMovementMethod is applied implicitly if not set on a TextView explicitly
|
||||
* Split code and codeBlock spans and factories
|
||||
* Add CustomTypefaceSpan
|
||||
* Add NoCopySpansFactory
|
||||
* Add placeholder to image loading
|
||||
|
||||
Generally speaking there are a lot of changes. Most of them are not backwards-compatible.
|
||||
The main point of this release is the `Plugin` system that allows more fluent configuration
|
||||
and opens the possibility of extending `Markwon` with 3rd party functionality in a simple
|
||||
and intuitive fashion. Please refer to the [documentation web-site](https://noties.github.io/Markwon)
|
||||
that has information on how to start migration.
|
||||
|
||||
The shortest excerpt of this release can be expressed like this:
|
||||
|
||||
```java
|
||||
// previous v2.x.x way
|
||||
Markwon.setMarkdown(textView, "**Hello there!**");
|
||||
```
|
||||
|
||||
```java
|
||||
// 3.x.x
|
||||
Markwon.create(context)
|
||||
.setMarkdown(textView, "**Hello there!**");
|
||||
```
|
||||
|
||||
But there is much more to it, please visit documentation web-site
|
||||
to get the full picture of latest changes.
|
||||
|
||||
## 2.0.1
|
||||
* `SpannableMarkdownVisitor` Rename blockQuoteIndent to blockIndent
|
||||
* Fixed block new lines logic for block quote and paragraph (#82)
|
||||
* AsyncDrawable fix no dimensions bug (#81)
|
||||
* Fixed block new lines logic for block quote and paragraph (<GithubIssue id="82" />)
|
||||
* AsyncDrawable fix no dimensions bug (<GithubIssue id="81" />)
|
||||
* Update SpannableTheme to use Px instead of Dimension annotation
|
||||
* Allow TaskListSpan isDone mutation
|
||||
* Updated commonmark-java to 0.12.1
|
||||
* Add OrderedListItemSpan measure utility method (#78)
|
||||
* Add OrderedListItemSpan measure utility method (<GithubIssue id="78" />)
|
||||
* Add SpannableBuilder#getSpans method
|
||||
* Fix DataUri scheme handler in image-loader (#74)
|
||||
* Introduced a "copy" builder for SpannableThem
|
||||
Thanks @c-b-h 🙌
|
||||
* Fix DataUri scheme handler in image-loader (<GithubIssue id="74" />)
|
||||
* Introduced a "copy" builder for SpannableThem <br>Thanks <GithubUser name="c-b-h" />
|
||||
|
||||
## 2.0.0
|
||||
* Add `html-parser-api` and `html-parser-impl` modules
|
||||
|
@ -1,11 +1,12 @@
|
||||
---
|
||||
title: 'Overview'
|
||||
title: 'Introduction'
|
||||
---
|
||||
|
||||
<img :src="$withBase('./art/markwon_logo.png')" alt="Markwon Logo" width="50%">
|
||||
<img :src="$withBase('/art/markwon_logo.png')" alt="Markwon Logo" width="50%">
|
||||
|
||||
<br><br>
|
||||
<MavenBadges/>
|
||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20)
|
||||
[](https://travis-ci.org/noties/Markwon)
|
||||
|
||||
**Markwon** is a markdown library for Android. It parses markdown following
|
||||
<Link name="commonmark-spec" /> with the help of amazing <Link name="commonmark-java" /> library
|
||||
@ -20,22 +21,23 @@ but also gives all the means to tweak the appearance if desired. All markdown fe
|
||||
listed in <Link name="commonmark-spec" /> are supported (including support for **inlined/block HTML code**,
|
||||
**markdown tables**, **images** and **syntax highlight**).
|
||||
|
||||
## Supported markdown features:
|
||||
## Supported markdown features
|
||||
|
||||
* Emphasis (`*`, `_`)
|
||||
* Strong emphasis (`**`, `__`)
|
||||
* Strike-through (`~~`)
|
||||
* Headers (`#{1,6}`)
|
||||
* Links (`[]()` && `[][]`)
|
||||
* [Images](/docs/image-loader.md)
|
||||
* [Images](/docs/v3/core/images.md)
|
||||
* Thematic break (`---`, `***`, `___`)
|
||||
* Quotes & nested quotes (`>{1,}`)
|
||||
* Ordered & non-ordered lists & nested ones
|
||||
* Inline code
|
||||
* Code blocks
|
||||
* Tables (*with limitations*)
|
||||
* [Syntax highlight](/docs/syntax-highlight.md)
|
||||
* [HTML](/docs/html.md)
|
||||
* [Strike-through](/docs/v3/ext-strikethrough/) (`~~`)
|
||||
* [Tables](/docs/v3/ext-tables/) (*with limitations*)
|
||||
* [Syntax highlight](/docs/v3/syntax-highlight/)
|
||||
* [LaTeX](/docs/v3/ext-latex/) formulas
|
||||
* [HTML](/docs/v3/html/)
|
||||
* Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`)
|
||||
* Strong emphasis (`<b>`, `<strong>`)
|
||||
* SuperScript (`<sup>`)
|
||||
@ -48,11 +50,13 @@ listed in <Link name="commonmark-spec" /> are supported (including support for *
|
||||
* Blockquote (`blockquote`)
|
||||
* Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`)
|
||||
* there is support to render any HTML tag, but it will require to create a special `TagHandler`,
|
||||
more information can be found in [HTML section](/docs/html.md#custom-tag-handler)
|
||||
* Task lists:
|
||||
- [ ] Not _done_
|
||||
- [X] **Done** with `X`
|
||||
- [x] ~~and~~ **or** small `x`
|
||||
more information can be found in [HTML section](/docs/v3/core/html-renderer.md)
|
||||
* [Task lists](/docs/v3/ext-tasklist/):
|
||||
<ul style="list-style-type: none; margin: 0; padding: 0;">
|
||||
<li><input type="checkbox" disabled>Not <i>done</i></li>
|
||||
<li><input type="checkbox" disabled checked><strong>Done</strong> with <code>X</code></li>
|
||||
<li><input type="checkbox" disabled checked><del>and</del> <strong>or</strong> small <code>x</code></li>
|
||||
</ul>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@ -68,3 +72,24 @@ Screenshots are taken from sample application. It is a generic markdown viewer
|
||||
with support to display markdown content via `http`, `https` & `file` schemes
|
||||
and 2 themes included: Light & Dark. It can be downloaded from [releases](https://github.com/noties/Markwon/releases)
|
||||
:::
|
||||
|
||||
|
||||
## Awesome Markwon
|
||||
|
||||
<u>Applications using Markwon</u>:
|
||||
|
||||
* [Partico](https://partiko.app/) - Partiko is a censorship free social network.
|
||||
* [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas.
|
||||
|
||||
|
||||
<u>Extension/plugins</u>:
|
||||
|
||||
* [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background.
|
||||
|
||||
---
|
||||
|
||||
[Help to improve][awesome_link] this section by submitting your application or library
|
||||
that is using `Markwon`
|
||||
|
||||
|
||||
[awesome_link]: https://github.com/noties/Markwon/issues/new?labels=awesome&body=Please%20provide%20the%20following%3A%0A*%20Project%20name%0A*%20Project%20URL%20(repository%2C%20store%20listing%2C%20web%20page)%0A*%20Optionally%20_brand_%20image%20URL%0A%0APlease%20make%20sure%20that%20there%20is%20the%20**awesome**%20label%20selected%20for%20this%20issue.%0A%0A%F0%9F%99%8C%20
|
||||
|
50
docs/collectArtifacts.js
Normal file
@ -0,0 +1,50 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROPERTIES_FILE_NAME = 'gradle.properties';
|
||||
const PROP_GROUP = 'GROUP';
|
||||
const PROP_DESCRIPTION = 'POM_DESCRIPTION';
|
||||
const PROP_ARTIFACT_NAME = 'POM_NAME';
|
||||
const PROP_ARTIFACT_ID = 'POM_ARTIFACT_ID';
|
||||
|
||||
const readProperties = (file) => fs.readFileSync(file, { encoding: 'utf-8' }, 'string')
|
||||
.split('\n')
|
||||
// filter-out empty lines
|
||||
.filter(s => s)
|
||||
.map(s => s.split('='))
|
||||
.reduce((a, s) => {
|
||||
a[s[0]] = s[1];
|
||||
return a;
|
||||
}, {});
|
||||
|
||||
const listDirectories = (folder) => fs.readdirSync(folder)
|
||||
.map(name => path.join(folder, name))
|
||||
.filter(f => fs.lstatSync(f).isDirectory());
|
||||
|
||||
const projectDir = path.resolve(__dirname, '../');
|
||||
|
||||
const projectProperties = readProperties(path.join(projectDir, PROPERTIES_FILE_NAME));
|
||||
|
||||
const projectGroup = projectProperties[PROP_GROUP]
|
||||
|
||||
const artifacts = listDirectories(projectDir)
|
||||
.map(dir => path.join(dir, PROPERTIES_FILE_NAME))
|
||||
.filter(f => fs.existsSync(f))
|
||||
.map(readProperties)
|
||||
.map(props => {
|
||||
return {
|
||||
id: props[PROP_ARTIFACT_ID],
|
||||
name: props[PROP_ARTIFACT_NAME],
|
||||
group: projectGroup,
|
||||
description: props[PROP_DESCRIPTION]
|
||||
}
|
||||
});
|
||||
|
||||
const artifactsFile = path.join(__dirname, '.vuepress', '.artifacts.js');
|
||||
const artifactsJs = `
|
||||
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
|
||||
const artifacts = ${JSON.stringify(artifacts)};
|
||||
export { artifacts };
|
||||
`
|
||||
|
||||
fs.writeFileSync(artifactsFile, artifactsJs);
|
70
docs/docs/v2/README.md
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
title: 'Overview'
|
||||
---
|
||||
|
||||
<img :src="$withBase('/art/markwon_logo.png')" alt="Markwon Logo" width="50%">
|
||||
|
||||
<br><br>
|
||||
<MavenBadges2xx/>
|
||||
|
||||
**Markwon** is a markdown library for Android. It parses markdown following
|
||||
<Link name="commonmark-spec" /> with the help of amazing <Link name="commonmark-java" /> library
|
||||
and renders result as _Android-native_ Spannables. **No HTML** is involved
|
||||
as an intermediate step. <u>**No WebView** is required</u>. It's extremely fast,
|
||||
feature-rich and extensible.
|
||||
|
||||
It gives ability to display markdown in all TextView widgets (**TextView**,
|
||||
**Button**, **Switch**, **CheckBox**, etc), **Toasts** and all other places that accept
|
||||
**Spanned content**. Library provides reasonable defaults to display style of a markdown content
|
||||
but also gives all the means to tweak the appearance if desired. All markdown features
|
||||
listed in <Link name="commonmark-spec" /> are supported (including support for **inlined/block HTML code**,
|
||||
**markdown tables**, **images** and **syntax highlight**).
|
||||
|
||||
## Supported markdown features:
|
||||
|
||||
* Emphasis (`*`, `_`)
|
||||
* Strong emphasis (`**`, `__`)
|
||||
* Strike-through (`~~`)
|
||||
* Headers (`#{1,6}`)
|
||||
* Links (`[]()` && `[][]`)
|
||||
* [Images](/docs/v2/image-loader.md)
|
||||
* Thematic break (`---`, `***`, `___`)
|
||||
* Quotes & nested quotes (`>{1,}`)
|
||||
* Ordered & non-ordered lists & nested ones
|
||||
* Inline code
|
||||
* Code blocks
|
||||
* Tables (*with limitations*)
|
||||
* [Syntax highlight](/docs/v2/syntax-highlight.md)
|
||||
* [HTML](/docs/v2/html.md)
|
||||
* Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`)
|
||||
* Strong emphasis (`<b>`, `<strong>`)
|
||||
* SuperScript (`<sup>`)
|
||||
* SubScript (`<sub>`)
|
||||
* Underline (`<u>`, `ins`)
|
||||
* Strike-through (`<s>`, `<strike>`, `<del>`)
|
||||
* Link (`a`)
|
||||
* Lists (`ul`, `ol`)
|
||||
* Images (`img` will require configured image loader)
|
||||
* Blockquote (`blockquote`)
|
||||
* Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`)
|
||||
* there is support to render any HTML tag, but it will require to create a special `TagHandler`,
|
||||
more information can be found in [HTML section](/docs/v2/html.md#custom-tag-handler)
|
||||
* Task lists:
|
||||
- [ ] Not _done_
|
||||
- [X] **Done** with `X`
|
||||
- [x] ~~and~~ **or** small `x`
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img :src="$withBase('/art/mw_light_01.png')" alt="screenshot light #1" width="30%">
|
||||
<img :src="$withBase('/art/mw_light_02.png')" alt="screenshot light #2" width="30%">
|
||||
<img :src="$withBase('/art/mw_light_03.png')" alt="screenshot light #3" width="30%">
|
||||
<img :src="$withBase('/art/mw_dark_01.png')" alt="screenshot dark #2" width="30%">
|
||||
|
||||
By default configuration uses TextView textColor for styling, so changing textColor changes style
|
||||
|
||||
:::tip Sample application
|
||||
Screenshots are taken from sample application. It is a generic markdown viewer
|
||||
with support to display markdown content via `http`, `https` & `file` schemes
|
||||
and 2 themes included: Light & Dark. It can be downloaded from [releases](https://github.com/noties/Markwon/releases)
|
||||
:::
|
@ -24,13 +24,13 @@ values as they will be applied automatically
|
||||
If you plan on using images inside your markdown/HTML, you will have to **explicitly**
|
||||
register an implementation of `AsyncDrawable.Loader` via `#asyncDrawableLoader` builder method.
|
||||
`Markwon` comes with ready implementation for that and it can be found in
|
||||
`markwon-image-loader` module. Refer to module [documentation](/docs/image-loader.md)
|
||||
`markwon-image-loader` module. Refer to module [documentation](/docs/v2/image-loader.md)
|
||||
:::
|
||||
|
||||
## Theme
|
||||
|
||||
`SpannableTheme` controls how markdown is rendered. It has pretty extensive number of
|
||||
options that can be found [here](/docs/theme.md)
|
||||
options that can be found [here](/docs/v2/theme.md)
|
||||
|
||||
```java
|
||||
SpannableConfiguration.builder(context)
|
||||
@ -56,7 +56,7 @@ If `AsyncDrawable.Loader` is not provided explicitly, default **no-op** implemen
|
||||
|
||||
:::tip Implementation
|
||||
There are no restrictions on what implementation to use, but `Markwon` has artifact that can
|
||||
answer the most common needs of displaying SVG, GIF and other image formats. It can be found [here](/docs/image-loader.md)
|
||||
answer the most common needs of displaying SVG, GIF and other image formats. It can be found [here](/docs/v2/image-loader.md)
|
||||
:::
|
||||
|
||||
### Size resolver <Badge text="1.0.1" />
|
||||
@ -107,7 +107,7 @@ If not provided explicitly, default **no-op** implementation will be used.
|
||||
Although `SyntaxHighlight` interface was included with the very first version
|
||||
of `Markwon` there were no ready-to-use implementations. But starting with <Badge text="1.1.0" />
|
||||
`Markwon` provides one. It can be found in `markwon-syntax-highlight` artifact. Refer
|
||||
to module [documentation](/docs/syntax-highlight.md)
|
||||
to module [documentation](/docs/v2/syntax-highlight.md)
|
||||
:::
|
||||
|
||||
## Link resolver
|
||||
@ -166,7 +166,7 @@ SpannableConfiguration.builder(context)
|
||||
```
|
||||
|
||||
If not provided explicitly, default `SpannableFactoryDef` implementation will be used. It is documented
|
||||
in [this section](/docs/factory.md)
|
||||
in [this section](/docs/v2/factory.md)
|
||||
|
||||
## Soft line break <Badge text="1.1.1" />
|
||||
|
||||
@ -197,7 +197,7 @@ SpannableConfiguration.builder(context)
|
||||
|
||||
if not provided explicitly, default `MarkwonHtmlParserImpl` will be used
|
||||
**if** it can be found in classpath, otherwise default **no-op** implementation
|
||||
wiil be used. Refer to [HTML](/docs/html.md#parser) document for more information about this behavior.
|
||||
wiil be used. Refer to [HTML](/docs/v2/html.md#parser) document for more information about this behavior.
|
||||
|
||||
### Renderer
|
||||
|
||||
@ -210,7 +210,7 @@ SpannableConfiguration.builder(context)
|
||||
```
|
||||
|
||||
If not provided explicitly, default `MarkwonHtmlRenderer` implementation will be used.
|
||||
It is documented [here](/docs/html.md#renderer)
|
||||
It is documented [here](/docs/v2/html.md#renderer)
|
||||
|
||||
### HTML allow non-closed tags
|
||||
|
@ -1,10 +1,5 @@
|
||||
# Getting started
|
||||
|
||||
:::tip Installation
|
||||
Please follow [installation](/docs/install.md) instructions
|
||||
to learn how to add `Markwon` to your project
|
||||
:::
|
||||
|
||||
## Quick one
|
||||
|
||||
This is the most simple way to set markdown to a `TextView` or any of its siblings:
|
||||
@ -25,7 +20,7 @@ Toast.makeText(context, markdown, Toast.LENGTH_LONG).show();
|
||||
|
||||
## Longer one
|
||||
|
||||
When you need to customize markdown parsing/rendering you can use [SpannableConfiguration](/docs/configure.md):
|
||||
When you need to customize markdown parsing/rendering you can use [SpannableConfiguration](/docs/v2/configure.md):
|
||||
|
||||
```java
|
||||
final SpannableConfiguration configuration = SpannableConfiguration.builder(context)
|
@ -16,12 +16,12 @@ public interface Loader {
|
||||
|
||||
## AsyncDrawableLoader
|
||||
|
||||
<MavenBadge artifact="markwon-image-loader" />
|
||||
<MavenBadge2xx artifact="markwon-image-loader" />
|
||||
|
||||
`AsyncDrawableLoader` from `markwon-image-loader` artifact can be used.
|
||||
|
||||
:::tip Install
|
||||
[Learn how to add](/docs/install.md#image-loader) `markwon-image-loader` to your project
|
||||
[Learn how to add](/docs/v2/install.md#image-loader) `markwon-image-loader` to your project
|
||||
:::
|
||||
|
||||
Default instance of `AsyncDrawableLoader` can be obtain like this:
|
@ -5,7 +5,7 @@ next: /docs/getting-started.md
|
||||
|
||||
# Installation
|
||||
|
||||
<MavenBadges />
|
||||
<MavenBadges2xx />
|
||||
|
||||
In order to start using `Markwon` add this to your dependencies block
|
||||
in your projects `build.gradle`:
|
||||
@ -39,7 +39,7 @@ Provides implementation of `AsyncDrawable.Loader` and comes with support for:
|
||||
* GIF
|
||||
* Other image formats
|
||||
|
||||
Please refer to documentation for [image loader](/docs/image-loader.md) module
|
||||
Please refer to documentation for [image loader](/docs/v2/image-loader.md) module
|
||||
|
||||
### Syntax highlight
|
||||
|
||||
@ -49,7 +49,7 @@ implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}"
|
||||
|
||||
Provides implementation of `SyntaxHighlight` and allows various syntax highlighting
|
||||
in your markdown based Android applications. Comes with 2 ready-to-be-used themes: `light` and `dark`.
|
||||
Please refer to documentation for [syntax highlight](/docs/syntax-highlight.md) module
|
||||
Please refer to documentation for [syntax highlight](/docs/v2/syntax-highlight.md) module
|
||||
|
||||
### View
|
||||
|
||||
@ -59,7 +59,7 @@ implementation "ru.noties:markwon-view:${markwonVersion}"
|
||||
|
||||
Provides 2 widgets to display markdown: `MarkwonView` and `MarkwonViewCompat` (subclasses
|
||||
of `TextView` and `AppCompatTextView` respectively).
|
||||
Please refer to documentation for [view](/docs/view.md) module
|
||||
Please refer to documentation for [view](/docs/v2/view.md) module
|
||||
|
||||
## Proguard
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Syntax highlight
|
||||
|
||||
<MavenBadge artifact="markwon-syntax-highlight" />
|
||||
<MavenBadge2xx artifact="markwon-syntax-highlight" />
|
||||
|
||||
This is a simple module to add **syntax highlight** functionality to your markdown rendered with `Markwon` library. It is based on [Prism4j](https://github.com/noties/Prism4j) so lead there to understand how to configure `Prism4j` instance.
|
||||
|
@ -1,10 +1,19 @@
|
||||
# Theme
|
||||
|
||||
Here is the list of properties that can be configured via `SpannableTheme#builder` factory
|
||||
method. If you wish to control what is out of this list, you can use [SpannableFactory](/docs/factory.md)
|
||||
Here is the list of properties that can be configured via `SpannableTheme`. If you wish to control what
|
||||
is out of this list, you can use [SpannableFactory](/docs/v2/factory.md)
|
||||
abstraction which lets you to gather full control of Spans that are used to display markdown.
|
||||
|
||||
* factory methods
|
||||
* `SpannableTheme#create(Context)` - creates a **default** instance of `SpannableBuilder (with _defaults_ registered)
|
||||
* `SpannableTheme#builder` - creates **empty** builder with **no defaults registered**
|
||||
* `SpannableTheme#builderWithDefaults(Context)` - create a **default** instance of builder (with default values registered)
|
||||
|
||||
:::warning
|
||||
`SpannbleTheme#builder` method has an unfortunate naming. It should've been `emptyBuilder`
|
||||
or `builderNoDefaults` because `#builder` method returns a builder with <strong>no default
|
||||
theme values registered</strong>. To create a builder **with** default values registered
|
||||
use `SpannableBuilder#builderWithDefaults(Context)`
|
||||
:::
|
||||
|
||||
## Link color
|
||||
|
||||
@ -107,7 +116,7 @@ The color of background of code block text
|
||||
|
||||
Leading margin for the block code content
|
||||
|
||||
<ThemeProperty name="codeMultilineMargin" type="@Px int" defaults="Width of the space character" />
|
||||
<ThemeProperty name="codeMultilineMargin" type="@Px int" defaults="8dip" />
|
||||
|
||||
### Code typeface
|
||||
|
@ -1,6 +1,6 @@
|
||||
# MarkwonView
|
||||
|
||||
<MavenBadge artifact="markwon-view" />
|
||||
<MavenBadge2xx artifact="markwon-view" />
|
||||
|
||||
This is simple library containing 2 views that are able to display markdown:
|
||||
* MarkwonView - extends `android.view.TextView`
|
||||
@ -27,7 +27,8 @@ public interface IMarkwonView {
|
||||
|
||||
Both views support layout-preview in Android Studio (with some exceptions, for example, bold span is not rendered due to some limitations of layout preview).
|
||||
These are XML attributes:
|
||||
```
|
||||
|
||||
```xml
|
||||
app:mv_markdown="string"
|
||||
app:mv_configurationProvider="string"
|
||||
```
|
181
docs/docs/v3/core/configuration.md
Normal file
@ -0,0 +1,181 @@
|
||||
# Configuration
|
||||
|
||||
`MarkwonConfiguration` class holds common Markwon functionality.
|
||||
These are _configurable_ properties:
|
||||
* `SyntaxHighlight`
|
||||
* `LinkSpan.Resolver`
|
||||
* `UrlProcessor`
|
||||
* `ImageSizeResolver`
|
||||
* `MarkwonHtmlParser`
|
||||
|
||||
:::tip
|
||||
Additionally `MarkwonConfiguration` holds:
|
||||
* `MarkwonTheme`
|
||||
* `AsyncDrawableLoader`
|
||||
* `MarkwonHtmlRenderer`
|
||||
* `MarkwonSpansFactory`
|
||||
|
||||
Please note that these values can be retrieved from `MarkwonConfiguration`
|
||||
instance, but their _configuration_ must be done by a `Plugin` by overriding
|
||||
one of the methods:
|
||||
* `Plugin#configureTheme`
|
||||
* `Plugin#configureImages`
|
||||
* `Plugin#configureHtmlRenderer`
|
||||
* `Plugin#configureSpansFactory`
|
||||
:::
|
||||
|
||||
## SyntaxHighlight
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.syntaxHighlight(new SyntaxHighlightNoOp());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::tip
|
||||
Use [syntax-highlight](/docs/v3/syntax-highlight/) to add syntax highlighting
|
||||
to your application
|
||||
:::
|
||||
|
||||
## LinkSpan.Resolver
|
||||
|
||||
React to a link click event. By default `LinkResolverDef` is used,
|
||||
which tries to start an Activity given the `link` argument. If no
|
||||
Activity can handle `link` `LinkResolverDef` silently ignores click event
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.linkResolver(new LinkSpan.Resolver() {
|
||||
@Override
|
||||
public void resolve(View view, @NonNull String link) {
|
||||
// react to link click here
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::tip
|
||||
Please note that `Markwon` will apply `LinkMovementMethod` to a resulting TextView
|
||||
if there is none registered. if you wish to register own instance of a `MovementMethod`
|
||||
apply it directly to a TextView or use [MovementMethodPlugin](/docs/v3/core/movement-method-plugin.md)
|
||||
:::
|
||||
|
||||
## UrlProcessor
|
||||
|
||||
Process URLs in your markdown (for links and images). If not provided explicitly,
|
||||
default **no-op** implementation will be used, which does not modify URLs (keeping them as-is).
|
||||
|
||||
`Markwon` provides 2 implementations of `UrlProcessor`:
|
||||
* `UrlProcessorRelativeToAbsolute`
|
||||
* `UrlProcessorAndroidAssets`
|
||||
|
||||
### UrlProcessorRelativeToAbsolute
|
||||
|
||||
`UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is
|
||||
defined like this: `` and `UrlProcessorRelativeToAbsolute`
|
||||
is created with `https://github.com/noties/Markwon/raw/master/` as the base:
|
||||
`new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`,
|
||||
then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG`
|
||||
as the destination.
|
||||
|
||||
### UrlProcessorAndroidAssets
|
||||
|
||||
`UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder.
|
||||
So an image: `` will have `file:///android_asset/art/image.JPG` as the
|
||||
destination.
|
||||
|
||||
:::tip
|
||||
Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information,
|
||||
so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png`
|
||||
will be kept as-is.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
In order to display an image from assets you still need to register `ImagesPlugin#createWithAssets(Context)`
|
||||
plugin in resulting `Markwon` instance. As `UrlProcessorAndroidAssets` only
|
||||
_processes_ URLs and doesn't take any part in displaying an image.
|
||||
:::
|
||||
|
||||
|
||||
## ImageSizeResolver
|
||||
|
||||
`ImageSizeResolver` controls the size of an image to be displayed. Currently it
|
||||
handles only HTML images (specified via `img` tag).
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.imageSizeResolver(new ImageSizeResolver() {
|
||||
@NonNull
|
||||
@Override
|
||||
public Rect resolveImageSize(
|
||||
@Nullable ImageSize imageSize,
|
||||
@NonNull Rect imageBounds,
|
||||
int canvasWidth,
|
||||
float textSize) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
If not provided explicitly, default `ImageSizeResolverDef` implementation will be used.
|
||||
It handles 3 dimension units:
|
||||
* `%` (percent, relative to Canvas width)
|
||||
* `em` (relative to text size)
|
||||
* `px` (absolute size, every dimension that is not `%` or `em` is considered to be _absolute_)
|
||||
|
||||
```html
|
||||
<img width="100%">
|
||||
<img width="2em" height="10px">
|
||||
<img style="{width: 100%; height: 8em;}">
|
||||
```
|
||||
|
||||
`ImageSizeResolverDef` keeps the ratio of original image if one of the dimensions is missing.
|
||||
|
||||
:::warning Height%
|
||||
There is no support for `%` units for `height` dimension. This is due to the fact that
|
||||
height of an TextView in which markdown is displayed is non-stable and changes with time
|
||||
(for example when image is loaded and applied to a TextView it will _increase_ TextView's height),
|
||||
so we will have no point-of-reference from which to _calculate_ image height.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
`ImageSizeResolverDef` also takes care for an image to **not** exceed
|
||||
canvas width. If an image has greater width than a TextView Canvas, then
|
||||
image will be _scaled-down_ to fit the canvas. Please note that this rule
|
||||
applies only if image has no absolute sizes (for example width is specified
|
||||
in pixels).
|
||||
:::
|
||||
|
||||
## MarkwonHtmlParser
|
||||
|
||||
Specify which HTML parser to use. Default implementation is **no-op**.
|
||||
|
||||
:::warning
|
||||
One must explicitly use [HtmlPlugin](/docs/v3/html/) in order to display
|
||||
HTML content in markdown. Without specified HTML parser **no HTML content
|
||||
will be rendered**.
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
```
|
||||
|
||||
Please note that adding `HtmlPlugin` will take care of initializing parser,
|
||||
so after `HtmlPlugin` is used, no additional configuration steps are required.
|
||||
:::
|
105
docs/docs/v3/core/core-plugin.md
Normal file
@ -0,0 +1,105 @@
|
||||
# Core plugin <Badge text="3.0.0" />
|
||||
|
||||
Since <Badge text="3.0.0" /> with introduction of _plugins_, Markwon
|
||||
**core** functionality was moved to a dedicated plugin.
|
||||
|
||||
```java
|
||||
CorePlugin.create();
|
||||
```
|
||||
|
||||
## Node visitors
|
||||
|
||||
`CorePlugin` registers these `commonmark-java` node visitors:
|
||||
* `Text`
|
||||
* `StrongEmphasis`
|
||||
* `Emphasis`
|
||||
* `BlockQuote`
|
||||
* `Code`
|
||||
* `FencedCodeBlock`
|
||||
* `IndentedCodeBlock`
|
||||
* `BulletList`
|
||||
* `OrderedList`
|
||||
* `ListItem`
|
||||
* `ThematicBreak`
|
||||
* `Heading`
|
||||
* `SoftLineBreak`
|
||||
* `HardLineBreak`
|
||||
* `Paragraph`
|
||||
* `Link`
|
||||
|
||||
## Span factories
|
||||
|
||||
`CorePlugin` adds these `SpanFactory`s:
|
||||
* `StrongEmphasis`
|
||||
* `Emphasis`
|
||||
* `BlockQuote`
|
||||
* `Code`
|
||||
* `FencedCodeBlock`
|
||||
* `IndentedCodeBlock`
|
||||
* `ListItem`
|
||||
* `Heading`
|
||||
* `Link`
|
||||
* `ThematicBreak`
|
||||
|
||||
|
||||
:::tip
|
||||
By default `CorePlugin` does not register a `Paragraph` `SpanFactory` but
|
||||
this can be done in your custom plugin:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.setFactory(Paragraph.class, (configuration, props) ->
|
||||
new ForegroundColorSpan(Color.RED));
|
||||
}
|
||||
})
|
||||
```
|
||||
:::
|
||||
|
||||
## Props
|
||||
These props are exported by `CorePlugin` and can be found in `CoreProps`:
|
||||
* `Prop<ListItemType> LIST_ITEM_TYPE` (BULLET | ORDERED)
|
||||
* `Prop<Integer> BULLET_LIST_ITEM_LEVEL`
|
||||
* `Prop<Integer> ORDERED_LIST_ITEM_NUMBER`
|
||||
* `Prop<Integer> HEADING_LEVEL`
|
||||
* `Prop<String> LINK_DESTINATION`
|
||||
* `Prop<Boolean> PARAGRAPH_IS_IN_TIGHT_LIST`
|
||||
|
||||
:::warning List item type
|
||||
Before <Badge text="3.0.0" /> `Markwon` had 2 distinct lists (bullet and ordered).
|
||||
Since <Badge text="3.0.0" /> a single `SpanFactory` is used, which internally checks
|
||||
for `Prop<ListItemType> LIST_ITEM_TYPE`.
|
||||
Beware of this if you would like to override only one of the list types. This is
|
||||
done to correspond to `commonmark-java` implementation.
|
||||
:::
|
||||
|
||||
More information about props can be found [here](/docs/v3/core/render-props.md)
|
||||
|
||||
---
|
||||
|
||||
:::tip Soft line break
|
||||
Since <Badge text="3.0.0" /> Markwon core does not give an option to
|
||||
insert a new line when there is a soft line break in markdown. Instead a
|
||||
custom plugin can be used:
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(SoftLineBreak.class, (visitor, softLineBreak) ->
|
||||
visitor.forceNewLine());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Please note that `CorePlugin` will implicitly set a `LinkMovementMethod` on a TextView
|
||||
if one is not present. If you wish to customize a MovementMethod that is used, apply
|
||||
one manually to a TextView (before applying markdown) or use the [MovementMethodPlugin](/docs/v3/core/movement-method-plugin.md)
|
||||
which accepts a MovementMethod as an argument.
|
||||
:::
|
65
docs/docs/v3/core/getting-started.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Getting started
|
||||
|
||||
:::tip Installation
|
||||
Please follow [installation](/docs/v3/install.md) instructions
|
||||
to learn how to add `Markwon` to your project
|
||||
:::
|
||||
|
||||
## Quick one
|
||||
|
||||
This is the most simple way to set markdown to a `TextView` or any of its siblings:
|
||||
|
||||
```java
|
||||
// obtain an instance of Markwon
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
|
||||
// set markdown
|
||||
markwon.setMarkdown(textView, "**Hello there!**");
|
||||
```
|
||||
|
||||
The most simple way to obtain markdown to be applied _somewhere_ else:
|
||||
|
||||
```java
|
||||
// obtain an instance of Markwon
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
|
||||
// parse markdown and create styled text
|
||||
final Spanned markdown = markwon.toMarkdown("**Hello there!**");
|
||||
|
||||
// use it
|
||||
Toast.makeText(context, markdown, Toast.LENGTH_LONG).show();
|
||||
```
|
||||
|
||||
:::warning 3.x.x migration
|
||||
Starting with <Badge text="3.0.0" /> version Markwon no longer relies on static
|
||||
utility methods. To learn more about migrating existing applications
|
||||
refer to [migration](/docs/v3/migration-2-3.md) section.
|
||||
:::
|
||||
|
||||
## Longer one
|
||||
|
||||
With explicit `parse` and `render` methods:
|
||||
|
||||
```java
|
||||
// obtain an instance of Markwon
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
|
||||
// parse markdown to commonmark-java Node
|
||||
final Node node = markwon.parse("Are **you** still there?");
|
||||
|
||||
// create styled text from parsed Node
|
||||
final Spanned markdown = markwon.render(node);
|
||||
|
||||
// use it on a TextView
|
||||
markwon.setParsedMarkdown(textView, markdown);
|
||||
|
||||
// or a Toast
|
||||
Toast.makeText(context, markdown, Toast.LENGTH_LONG).show();
|
||||
```
|
||||
|
||||
## No magic one
|
||||
|
||||
This section is kept due to historical reasons. Starting with version <Badge text="3.0.0" />
|
||||
the amount of magic is reduced. To leverage your `Markwon` usage a concept of `Plugin`
|
||||
is introduced which helps to extend default behavior in a simple and _no-breaking-the-flow_ manner.
|
||||
Head to the [next section](/docs/v3/core/plugins.md) to know more.
|
101
docs/docs/v3/core/html-renderer.md
Normal file
@ -0,0 +1,101 @@
|
||||
# HTML Renderer
|
||||
|
||||
Starting with <Badge text="3.0.0" /> `MarkwonHtmlRenderer` controls how HTML
|
||||
is rendered:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) {
|
||||
builder.setHandler("a", new MyTagHandler());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
:::danger
|
||||
Customizing `MarkwonHtmlRenderer` is not enough to include HTML content in your application.
|
||||
You must explicitly include [markwon-html](/docs/v3/html/) artifact (includes HtmlParser)
|
||||
to your project and register `HtmlPlugin`:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
```
|
||||
:::
|
||||
|
||||
For example, to create an `<a>` HTML tag handler:
|
||||
|
||||
```java
|
||||
builder.setHandler("a", new SimpleTagHandler() {
|
||||
@Override
|
||||
public Object getSpans(
|
||||
@NonNull MarkwonConfiguration configuration,
|
||||
@NonNull RenderProps renderProps,
|
||||
@NonNull HtmlTag tag) {
|
||||
return new LinkSpan(
|
||||
configuration.theme(),
|
||||
tag.attributes().get("href"),
|
||||
configuration.linkResolver());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`SimpleTagHandler` can be used for simple cases when a tag does not require any special
|
||||
handling (like visiting it's children)
|
||||
|
||||
:::tip
|
||||
One can return `null` a single span or an array of spans from `getSpans` method
|
||||
:::
|
||||
|
||||
For a more advanced usage `TagHandler` can be used directly:
|
||||
|
||||
```java
|
||||
builder.setHandler("a", new TagHandler() {
|
||||
@Override
|
||||
public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) {
|
||||
|
||||
// obtain default spanFactory for Link node
|
||||
final SpanFactory factory = visitor.configuration().spansFactory().get(Link.class);
|
||||
|
||||
if (factory != null) {
|
||||
|
||||
// set destination property
|
||||
CoreProps.LINK_DESTINATION.set(
|
||||
visitor.renderProps(),
|
||||
tag.attributes().get("href"));
|
||||
|
||||
// Obtain spans from the factory
|
||||
final Object spans = factory.getSpans(
|
||||
visitor.configuration(),
|
||||
visitor.renderProps());
|
||||
|
||||
// apply spans to SpannableBuilder
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
spans,
|
||||
tag.start(),
|
||||
tag.end());
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
:::tip
|
||||
Sometimes HTML content might include tags that are not closed (although
|
||||
they are required to be by the spec, for example a `div`).
|
||||
Markwon by default disallows such tags and ignores them. Still,
|
||||
there is an option to allow them _explicitly_ via builder method:
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) {
|
||||
builder.allowNonClosedTags(true);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
Please note that if `allowNonClosedTags=true` then all non-closed tags will be closed
|
||||
at the end of a document.
|
||||
:::
|
205
docs/docs/v3/core/images.md
Normal file
@ -0,0 +1,205 @@
|
||||
# Images
|
||||
|
||||
Starting with <Badge text="3.0.0" /> `Markwon` comes with `ImagesPlugin`
|
||||
which supports `http(s)`, `file` and `data` schemes and default media
|
||||
decoder (for simple images, no [SVG](/docs/v3/image/svg.md) or [GIF](/docs/v3/image/gif.md) which
|
||||
are defined in standalone modules).
|
||||
|
||||
## ImagesPlugin
|
||||
|
||||
`ImagePlugin` takes care of _obtaining_ image resource, decoding it and displaying it in a `TextView`.
|
||||
|
||||
:::warning
|
||||
Although `core` artifact contains `ImagesPlugin` one must
|
||||
still **explicitly** register the `ImagesPlugin` on resulting `Markwon`
|
||||
instance.
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
```
|
||||
:::
|
||||
|
||||
There are 2 factory methods to obtain `ImagesPlugin`:
|
||||
* `ImagesPlugin#create(Context)`
|
||||
* `ImagesPlugin#createWithAssets(Context)`
|
||||
|
||||
The first one `#create(Context)` configures:
|
||||
* `FileSchemeHandler` that allows obtaining images from `file://` uris
|
||||
* `DataUriSchemeHandler` that allows _inlining_ images with `data:`
|
||||
scheme (`data:image/svg+xml;base64,MTIz`)
|
||||
* `NetworkSchemeHandler` that allows obtaining images from `http://` and `https://` uris
|
||||
(internally it uses `HttpURLConnection`)
|
||||
* `ImageMediaDecoder` which _tries_ to decode all encountered images as regular ones (png, jpg, etc)
|
||||
|
||||
The second one `#createWithAssets(Context)` does the same but also adds support
|
||||
for images that reside in `assets` folder of your application and
|
||||
referenced by `file:///android_asset/{path}` uri.
|
||||
|
||||
`ImagesPlugin` also _prepares_ a TextView to display images. Due to asynchronous
|
||||
nature of image loading, there must be a way to invalidate resulting Spanned
|
||||
content after an image is loaded.
|
||||
|
||||
:::warning
|
||||
Images come with few limitations. For of all, they work with a **TextView only**.
|
||||
This is due to the fact that there is no way to invalidate a `Spanned` content
|
||||
by itself (without context in which it is displayed). So, if `Markwon` is used,
|
||||
for example, to display a `Toast` with an image:
|
||||
|
||||
```java
|
||||
final Spanned spanned = markwon.toMarkdown("Hello ");
|
||||
Toast.makeText(context, spanned, Toast.LENGTH_LONG).show();
|
||||
```
|
||||
|
||||
Image _probably_ won't be displayed. As a workaround for `Toast` a custom `View`
|
||||
can be used:
|
||||
|
||||
```java
|
||||
final Spanned spanned = markwon.toMarkdown("Hello ");
|
||||
|
||||
final View view = createToastView();
|
||||
final TextView textView = view.findViewById(R.id.text_view);
|
||||
markwon.setParsedMarkdown(textView, spanned);
|
||||
|
||||
final Toast toast = new Toast(context);
|
||||
toast.setView(view);
|
||||
// other Toast configurations
|
||||
toast.show();
|
||||
```
|
||||
:::
|
||||
|
||||
## SchemeHandler
|
||||
|
||||
To add support for different schemes (or customize provided) a `SchemeHandler` must be used.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create(context))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
// example only, Markwon doesn't come with a ftp scheme handler
|
||||
builder.addSchemeHandler("ftp", new FtpSchemeHandler());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
It's a class to _convert_ an URI into an `InputStream`:
|
||||
|
||||
```java
|
||||
public abstract class SchemeHandler {
|
||||
|
||||
@Nullable
|
||||
public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri);
|
||||
}
|
||||
```
|
||||
|
||||
`ImageItem` is a holder class for resulting `InputStream` and (optional)
|
||||
content type:
|
||||
|
||||
```java
|
||||
public class ImageItem {
|
||||
|
||||
private final String contentType;
|
||||
private final InputStream inputStream;
|
||||
|
||||
/* rest omitted */
|
||||
}
|
||||
```
|
||||
|
||||
Based on `contentType` returned a corresponding `MediaDecoder` will be matched.
|
||||
If no `MediaDecoder` can handle given `contentType` then a default media decoder will
|
||||
be used.
|
||||
|
||||
## MediaDecoder
|
||||
|
||||
By default `core` artifact comes with _default image decoder_ only. It's called
|
||||
`ImageMediaDecoder` and it can decode all the formats that `BitmapFactory#decodeStream(InputStream)`
|
||||
can.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(ImagesPlugin.create(this))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
builder.addMediaDecoder("text/plain", new TextPlainMediaDecoder());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
```
|
||||
|
||||
`MediaDecoder` is a class to turn `InputStream` into a `Drawable`:
|
||||
|
||||
```java
|
||||
public abstract class MediaDecoder {
|
||||
|
||||
@Nullable
|
||||
public abstract Drawable decode(@NonNull InputStream inputStream);
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you want to display GIF or SVG images also, you can use [image-gif](/docs/v3/image/gif.md)
|
||||
and [image-svg](/docs/v3/image/svg.md) modules.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
If you are using [html](/docs/v3/html/) you do not have to additionally setup
|
||||
images displayed via `<img>` tag, as `HtmlPlugin` automatically uses configured
|
||||
image loader. But images referenced in HTML come with additional support for
|
||||
sizes, which is not supported natively by markdown, allowing absolute or relative sizes:
|
||||
|
||||
```html
|
||||
<img src="./assets/my-image" width="100%">
|
||||
```
|
||||
:::
|
||||
|
||||
## Placeholder drawable <Badge text="3.0.0" />
|
||||
|
||||
It's possible to provide a custom placeholder for an image (whilst it's loading).
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create(context))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
builder.placeholderDrawableProvider(new AsyncDrawableLoader.DrawableProvider() {
|
||||
@Override
|
||||
public Drawable provide() {
|
||||
// your custom placeholder drawable
|
||||
return new PlaceholderDrawable();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Error drawable <Badge text="3.0.0" />
|
||||
|
||||
To fallback in case of error whilst loading an image, an `error drawable` can be used:
|
||||
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create(context))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
builder.errorDrawableProvider(new AsyncDrawableLoader.DrawableProvider() {
|
||||
@Override
|
||||
public Drawable provide() {
|
||||
// your custom error drawable
|
||||
return new MyErrorDrawable();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
:::warning
|
||||
Before `3.0.0` `AsyncDrawableLoader` accepted a simple `Drawable` as error drawable
|
||||
argument. Starting `3.0.0` it accepts a `DrawableProvider` instead.
|
||||
:::
|
17
docs/docs/v3/core/movement-method-plugin.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Movement method plugin
|
||||
|
||||
`MovementMethodPlugin` can be used to apply a `MovementMethod` to a TextView
|
||||
(important if you have links inside your markdown). By default `CorePlugin`
|
||||
will set a `LinkMovementMethod` on a TextView if one is missing. If you have
|
||||
specific needs for a `MovementMethod` and `LinkMovementMethod` doesn't answer
|
||||
your needs use `MovementMethodPlugin`:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(MovementMethodPlugin.create(ScrollingMovementMethod.getInstance()))
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you are having trouble with system `LinkMovementMethod` as an alternative
|
||||
[BetterLinkMovementMethod](https://github.com/saket/Better-Link-Movement-Method) library can be used.
|
||||
:::
|
467
docs/docs/v3/core/plugins.md
Normal file
@ -0,0 +1,467 @@
|
||||
# Plugins <Badge text="3.0.0" />
|
||||
|
||||
Since <Badge text="3.0.0" /> `MarkwonPlugin` takes the key role in
|
||||
processing and rendering markdown. Even **core** functionaly is abstracted
|
||||
into a `CorePlugin`. So it's still possible to use `Markwon` with a completely
|
||||
own set of plugins.
|
||||
|
||||
To register a plugin `Markwon.Builder` must be used:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(CorePlugin.create())
|
||||
.build();
|
||||
```
|
||||
|
||||
All the process of transforming _raw_ markdown into a styled text (Spanned)
|
||||
will go through plugins. A plugin can:
|
||||
|
||||
* [configure commonmark-java `Parser`](#parser)
|
||||
* [configure `MarkwonTheme`](#markwontheme)
|
||||
* [configure `AsyncDrawableLoader` (used to display images in markdown)](#images)
|
||||
* [configure `MarkwonConfiguration`](#configuration)
|
||||
* [configure `MarkwonVisitor` (extensible commonmark-java Node visitor)](#visitor)
|
||||
* [configure `MarkwonSpansFactory` (factory to hold spans information for each Node)](#spans-factory)
|
||||
* [configure `MarkwonHtmlRenderer` (utility to properly display HTML in markdown)](#html-renderer)
|
||||
|
||||
---
|
||||
|
||||
* [declare a dependency on another plugin (will be used as a runtime validator)](#priority)
|
||||
|
||||
---
|
||||
|
||||
* [process raw input markdown before parsing it](#process-markdown)
|
||||
* [inspect/modify commonmark-java Node after it's been parsed, but before rendering](#inspect-modify-node)
|
||||
* [inspect commonmark-java Node after it's been rendered](#inspect-node-after-render)
|
||||
* [prepare TextView to display markdown _before_ markdown is applied to a TextView](#prepare-textview)
|
||||
* [post-process TextView _after_ markdown was applied](#textview-after-markdown-applied)
|
||||
|
||||
:::tip
|
||||
if you need to override only few methods of `MarkwonPlugin` (since it is an interface),
|
||||
`AbstractMarkwonPlugin` can be used.
|
||||
:::
|
||||
|
||||
## Parser
|
||||
|
||||
For example, let's register a new commonmark-java Parser extension:
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(CorePlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
// no need to call `super.configureParser(builder)`
|
||||
builder.extensions(Collections.singleton(StrikethroughExtension.create()));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
There are no limitations on what to do with commonmark-java Parser. For more info
|
||||
_what_ can be done please refer to <Link name="commonmark-java" displayName="commonmark-java documentation" />.
|
||||
|
||||
## MarkwonTheme
|
||||
|
||||
Starting <Badge text="3.0.0" /> `MarkwonTheme` represents _core_ theme. Aka theme for
|
||||
things core module knows of. For example it doesn't know anything about `strikethrough`
|
||||
or `tables` (as they belong to different modules).
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
builder
|
||||
.codeTextColor(Color.BLACK)
|
||||
.codeBackgroundColor(Color.GREEN);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::warning
|
||||
`CorePlugin` has special handling - it will be **implicitly** added
|
||||
if a plugin declares dependency on it. This is why in previous example we haven't
|
||||
added CorePlugin _explicitly_ as `AbstractMarkwonPlugin` declares a dependency on it.
|
||||
If it's not desireable override `AbstractMarkwonPlugin#priority` method to specify own rules.
|
||||
:::
|
||||
|
||||
More information about `MarkwonTheme` can be found [here](/docs/v3/core/theme.md).
|
||||
|
||||
|
||||
## Images
|
||||
|
||||
Since <Badge text="3.0.0" /> core images functionality moved to the `core` module.
|
||||
Now `Markwon` comes bundled with support for regular images (no `SVG` or `GIF`, they
|
||||
defined in standalone modules now). And 3(4) schemes supported by default:
|
||||
* http (+https; using system built-in `HttpURLConnection`)
|
||||
* file (including Android assets)
|
||||
* data (image inline, `data:image/svg+xml;base64,!@#$%^&*(`)
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
// sorry, these are not bundled with the library
|
||||
builder
|
||||
.addSchemeHandler("ftp", new FtpSchemeHandler("root", ""))
|
||||
.addMediaDecoder("text/plain", new AnsiiMediaDecoder());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::warning
|
||||
Although `ImagesPlugin` is bundled with the `core` artifact, it is **not** used by default
|
||||
and one must **explicitly** add it:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create(context));
|
||||
```
|
||||
|
||||
Without explicit usage of `ImagesPlugin` all image configuration will be ignored (no-op'ed)
|
||||
:::
|
||||
|
||||
More information about dealing with images can be found [here](/docs/v3/core/images.md)
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
`MarkwonConfiguration` is a set of common tools that are used by different parts
|
||||
of `Markwon`. It allows configurations of these:
|
||||
|
||||
* `SyntaxHighlight` (highlighting code blocks)
|
||||
* `LinkResolver` (opens links in markdown)
|
||||
* `UrlProcessor` (process URLs in markdown for both links and images)
|
||||
* `MarkwonHtmlParser` (HTML parser)
|
||||
* `ImageSizeResolver` (resolve image sizes, like `fit-to-canvas`, etc)
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
// MarkwonHtmlParserImpl is defined in `markwon-html` artifact
|
||||
builder.htmlParser(MarkwonHtmlParserImpl.create());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
More information about `MarkwonConfiguration` can be found [here](/docs/v3/core/configuration.md)
|
||||
|
||||
|
||||
## Visitor
|
||||
|
||||
`MarkwonVisitor` <Badge text="3.0.0" /> is commonmark-java Visitor that allows
|
||||
configuration of how each Node is visited. There is no longer need to create
|
||||
own subclass of Visitor and override required methods (like in `2.x.x` versions).
|
||||
`MarkwonVisitor` also allows registration of Nodes, that `core` module knows
|
||||
nothing about (instead of relying on `visit(CustomNode)` method)).
|
||||
|
||||
For example, let's add `strikethrough` Node visitor:
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder
|
||||
.on(Strikethrough.class, new MarkwonVisitor.NodeVisitor<Strikethrough>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Strikethrough strikethrough) {
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(strikethrough);
|
||||
visitor.setSpansForNodeOptional(strikethrough, length);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::tip
|
||||
`MarkwonVisitor` also allows _overriding_ already registered nodes. For example,
|
||||
we can disable `Heading` Node rendering:
|
||||
|
||||
```java
|
||||
builder.on(Heading.class, null);
|
||||
```
|
||||
|
||||
Please note that `Priority` plays nicely here to ensure that your
|
||||
custom Node override/disable happens _after_ some plugin defines it.
|
||||
:::
|
||||
|
||||
More information about `MarkwonVisitor` can be found [here](/docs/v3/core/visitor.md)
|
||||
|
||||
|
||||
## Spans Factory
|
||||
|
||||
`MarkwonSpansFactory` <Badge text="3.0.0" /> is an abstract factory (factory that produces other factories)
|
||||
for spans that `Markwon` uses. It controls what spans to use for certain Nodes.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
// override emphasis factory to make all emphasis nodes underlined
|
||||
builder.setFactory(Emphasis.class, new SpanFactory() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
return new UnderlineSpan();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::tip
|
||||
`SpanFactory` allows to return an _array_ of spans to apply multiple spans
|
||||
for a Node:
|
||||
|
||||
```java
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
// make underlined and set text color to red
|
||||
return new Object[]{
|
||||
new UnderlineSpan(),
|
||||
new ForegroundColorSpan(Color.RED)
|
||||
};
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
More information about spans factory can be found [here](/docs/v3/core/spans-factory.md)
|
||||
|
||||
|
||||
## HTML Renderer
|
||||
|
||||
`MarkwonHtmlRenderer` controls how HTML is rendered in markdown.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) {
|
||||
// <center> tag handling (deprecated but valid in our case)
|
||||
// can be any tag name, there is no connection with _real_ HTML tags,
|
||||
// <just-try-to-not-go-crazy-and-remember-about-portability>
|
||||
builder.addHandler("center", new SimpleTagHandler() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
|
||||
return new AlignmentSpan() {
|
||||
@Override
|
||||
public Layout.Alignment getAlignment() {
|
||||
return Layout.Alignment.ALIGN_CENTER;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::danger
|
||||
Although `MarkwonHtmlRenderer` is bundled with `core` artifact, actual
|
||||
HTML parser is placed in a standalone artifact and must be added to your
|
||||
project **explicitly** and then registered via `Markwon.Builder#usePlugin(HtmlPlugin.create())`.
|
||||
If not done so, no HTML will be parsed nor rendered.
|
||||
:::
|
||||
|
||||
More information about HTML rendering can be found [here](/docs/v3/core/html-renderer.md)
|
||||
|
||||
|
||||
## Priority
|
||||
|
||||
`Priority` is an abstraction to _state_ dependency connection between plugins. It is
|
||||
also used as a runtime graph validator. If a plugin defines a dependency on other, but
|
||||
_other_ is not in resulting `Markwon` instance, then a runtime exception will be thrown.
|
||||
`Priority` is also defines the order in which plugins will be placed. So, if a plugin `A`
|
||||
states a plugin `B` as a dependency, then plugin `A` will come **after** plugin `B`.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@NonNull
|
||||
@Override
|
||||
public Priority priority() {
|
||||
return Priority.after(CorePlugin.class);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::warning
|
||||
Please note that `AbstractMarkwonPlugin` _implicitly_ defines `CorePlugin`
|
||||
as a dependency (`return Priority.after(CorePlugin.class);`). This will
|
||||
also add `CorePlugin` to a `Markwon` instance, because it will be added
|
||||
_implicitly_ if a plugin defines it as a dependency.
|
||||
:::
|
||||
|
||||
Use one of the factory methods to create a `Priority` instance:
|
||||
|
||||
```java
|
||||
// none
|
||||
Priority.none();
|
||||
|
||||
// single dependency
|
||||
Priority.after(CorePlugin.class);
|
||||
|
||||
// 2 dependencies
|
||||
Priority.after(CorePlugin.class, ImagesPlugin.class);
|
||||
|
||||
// for a number >2, use #builder
|
||||
Priority.builder()
|
||||
.after(CorePlugin.class)
|
||||
.after(ImagesPlugin.class)
|
||||
.after(StrikethroughPlugin.class)
|
||||
.build();
|
||||
```
|
||||
|
||||
## Process markdown
|
||||
|
||||
A plugin can be used to _pre-process_ input markdown (this will be called before _parsing_):
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@NonNull
|
||||
@Override
|
||||
public String processMarkdown(@NonNull String markdown) {
|
||||
return markdown.replaceAll("foo", "bar");
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
## Inspect/modify Node
|
||||
|
||||
A plugin can inspect/modify commonmark-java Node _before_ it's being rendered.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void beforeRender(@NonNull Node node) {
|
||||
|
||||
// for example inspect it with custom visitor
|
||||
node.accept(new MyVisitor());
|
||||
|
||||
// or modify (you know what you are doing, right?)
|
||||
node.appendChild(new Text("Appended"));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
## Inspect Node after render
|
||||
|
||||
A plugin can inspect commonmark-java Node after it's been rendered.
|
||||
Modifying Node at this point makes not much sense (it's already been
|
||||
rendered and all modifications won't change anything). But this method can be used,
|
||||
for example, to clean-up some internal state (after rendering). Generally
|
||||
speaking, a plugin must be stateless, but if it cannot, then this method is
|
||||
the best place to clean-up.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
|
||||
cleanUp();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
## Prepare TextView
|
||||
|
||||
A plugin can _prepare_ a TextView before markdown is applied. For example `images`
|
||||
unschedules all previously scheduled `AsyncDrawableSpans` (if any) here. This way
|
||||
when new markdown (and set of Spannables) arrives, previous set won't be kept in
|
||||
memory and could be garbage-collected.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||
// clean-up previous
|
||||
AsyncDrawableScheduler.unschedule(textView);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
## TextView after markdown applied
|
||||
|
||||
A plugin will receive a callback _after_ markdown is applied to a TextView.
|
||||
For example `images` uses this callback to schedule new set of Spannables.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
AsyncDrawableScheduler.schedule(textView);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
:::tip
|
||||
Please note that unlike `#beforeSetText`, `#afterSetText` won't receive
|
||||
`Spanned` markdown. This happens because at this point spans must be
|
||||
queried directly from a TextView.
|
||||
:::
|
||||
|
||||
## What happens underneath
|
||||
|
||||
Here is what happens inside `Markwon` when `setMarkdown` method is called:
|
||||
|
||||
```java
|
||||
// `Markwon#create` implicitly uses CorePlugin
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(CorePlugin.create())
|
||||
.build();
|
||||
|
||||
// warning: pseudo-code
|
||||
|
||||
// 0. each plugin will be called to _pre-process_ raw input markdown
|
||||
rawInput = plugins.reduce(rawInput, (input, plugin) -> plugin.processMarkdown(input));
|
||||
|
||||
// 1. after input is processed it's being parsed to a Node
|
||||
node = parser.parse(rawInput);
|
||||
|
||||
// 2. each plugin will be able to inspect or manipulate resulting Node
|
||||
// before rendering
|
||||
plugins.forEach(plugin -> plugin.beforeRender(node));
|
||||
|
||||
// 3. node is being visited by a visitor
|
||||
node.accept(visitor);
|
||||
|
||||
// 4. each plugin will be called after node is being visited (aka rendered)
|
||||
plugins.forEach(plugin -> plugin.afterRender(node, visitor));
|
||||
|
||||
// 5. styled markdown ready at this point
|
||||
final Spanned markdown = visitor.markdown();
|
||||
|
||||
// NB, points 6-8 are applied **only** if markdown is set to a TextView
|
||||
|
||||
// 6. each plugin will be called before styled markdown is applied to a TextView
|
||||
plugins.forEach(plugin -> plugin.beforeSetText(textView, markdown));
|
||||
|
||||
// 7. markdown is applied to a TextView
|
||||
textView.setText(markdown);
|
||||
|
||||
// 8. each plugin will be called after markdown is applied to a TextView
|
||||
plugins.forEach(plugin -> plugin.afterSetText(textView));
|
||||
```
|
75
docs/docs/v3/core/render-props.md
Normal file
@ -0,0 +1,75 @@
|
||||
# RenderProps <Badge text="3.0.0" />
|
||||
|
||||
`RenderProps` encapsulates passing arguments from a node visitor to a node renderer.
|
||||
Without hardcoding arguments into an API method calls.
|
||||
|
||||
`RenderProps` is the state collection for `Props` that are set by a node visitor and
|
||||
retrieved by a node renderer.
|
||||
|
||||
```java
|
||||
public class Prop<T> {
|
||||
|
||||
@NonNull
|
||||
public static <T> Prop<T> of(@NonNull String name) {
|
||||
return new Prop<>(name);
|
||||
}
|
||||
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
For example `CorePlugin` defines a _Heading level_ prop (inside `CoreProps` class):
|
||||
|
||||
```java
|
||||
public static final Prop<Integer> HEADING_LEVEL = Prop.of("heading-level");
|
||||
```
|
||||
|
||||
Then CorePlugin registers a `Heading` node visitor and applies heading value:
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Heading.class, new MarkwonVisitor.NodeVisitor<Heading>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
|
||||
|
||||
/* Heading node handling logic */
|
||||
|
||||
// set heading level
|
||||
CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
|
||||
|
||||
// a helper method to apply span(s) for a node
|
||||
// (internally obtains a SpanFactory for Heading or silently ignores
|
||||
// this call if no factory for a Heading is registered)
|
||||
visitor.setSpansForNodeOptional(heading, start);
|
||||
|
||||
/* Heading node handling logic */
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
And finally `HeadingSpanFactory` (which is also registered by `CorePlugin`):
|
||||
|
||||
```java
|
||||
public class HeadingSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
return new HeadingSpan(
|
||||
configuration.theme(),
|
||||
CoreProps.HEADING_LEVEL.require(props)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
`Prop<T>` has these methods:
|
||||
|
||||
* `@Nullable T get(RenderProps)` - returns value stored in RenderProps or `null` if none is present
|
||||
* `@NonNull T get(RenderProps, @NonNull T defValue)` - returns value stored in RenderProps or default value (this method always return non-null value)
|
||||
* `@NonNull T require(RenderProps)` - returns value stored in RenderProps or _throws an exception_ if none is present
|
||||
* `void set(RenderProps, @Nullable T value)` - updates value stored in RenderProps, passing `null` as value is the same as calling `clear`
|
||||
* `void clear(RenderProps)` - clears value stored in RenderProps
|
61
docs/docs/v3/core/spans-factory.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Spans Factory
|
||||
|
||||
Starting with <Badge text="3.0.0" /> `MarkwonSpansFactory` controls what spans are displayed
|
||||
for markdown nodes.
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
// passing null as second argument will remove previously added
|
||||
// factory for the Link node
|
||||
builder.setFactory(Link.class, null);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## SpanFactory
|
||||
|
||||
In order to create a _generic_ interface for all possible Nodes, a `SpanFactory`
|
||||
was added:
|
||||
|
||||
```java
|
||||
builder.setFactory(Link.class, new SpanFactory() {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
All possible arguments are passed via [RenderProps](/docs/v3/core/render-props.md):
|
||||
|
||||
```java
|
||||
builder.setFactory(Link.class, new SpanFactory() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
final String href = CoreProps.LINK_DESTINATION.require(props);
|
||||
return new LinkSpan(configuration.theme(), href, configuration.linkResolver());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`SpanFactory` allows returning `null` for a certain span (no span will be applied).
|
||||
Or an array of spans:
|
||||
|
||||
```java
|
||||
builder.setFactory(Link.class, new SpanFactory() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
return new Object[]{
|
||||
new LinkSpan(
|
||||
configuration.theme(),
|
||||
CoreProps.LINK_DESTINATION.require(props),
|
||||
configuration.linkResolver()),
|
||||
new ForegroundColorSpan(Color.RED)
|
||||
};
|
||||
}
|
||||
});
|
||||
```
|
187
docs/docs/v3/core/theme.md
Normal file
@ -0,0 +1,187 @@
|
||||
# Theme
|
||||
|
||||
Here is the list of properties that can be configured via `MarkwonTheme.Builder` class.
|
||||
|
||||
:::tip
|
||||
Starting with <Badge text="3.0.0" /> there is no need to manually construct a `MarkwonTheme`.
|
||||
Instead a `Plugin` should be used:
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
builder
|
||||
.codeTextColor(Color.BLACK)
|
||||
.codeBackgroundColor(Color.GREEN);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
:::
|
||||
|
||||
## Link color
|
||||
|
||||
Controls the color of a [link](#)
|
||||
|
||||
<ThemeProperty name="linkColor" type="@ColorInt int" defaults="Default link color of a context where markdown is displayed <sup>*</sup>" />
|
||||
|
||||
<sup>*</sup> `TextPaint#linkColor` will be used to determine linkColor of a context
|
||||
|
||||
## Block margin
|
||||
|
||||
Starting margin before text content for the:
|
||||
* lists
|
||||
* blockquotes
|
||||
* task lists
|
||||
|
||||
<ThemeProperty name="blockMargin" type="@Px int" defaults="24dp" />
|
||||
|
||||
## Block quote
|
||||
|
||||
Customizations for the `blockquote` stripe
|
||||
|
||||
> Quote
|
||||
|
||||
### Stripe width
|
||||
|
||||
Width of a blockquote stripe
|
||||
|
||||
<ThemeProperty name="blockQuoteWidth" type="@Px int" defaults="1/4 of the <a href='#block-margin'>block margin</a>" />
|
||||
|
||||
### Stripe color
|
||||
|
||||
Color of a blockquote stripe
|
||||
|
||||
<ThemeProperty name="blockQuoteColor" type="@ColorInt int" defaults="textColor with <code>25</code> (0-255) alpha value" />
|
||||
|
||||
## List
|
||||
|
||||
### List item color
|
||||
|
||||
Controls the color of a list item. For ordered list: leading number,
|
||||
for unordered list: bullet.
|
||||
|
||||
* UL
|
||||
1. OL
|
||||
|
||||
<ThemeProperty name="listItemColor" type="@ColorInt int" defaults="Text color" />
|
||||
|
||||
### Bullet item stroke width
|
||||
|
||||
Border width of a bullet list item (level 2)
|
||||
|
||||
* First
|
||||
* * Second
|
||||
* * * Third
|
||||
|
||||
<ThemeProperty name="bulletListItemStrokeWidth" type="@Px int" defaults="Stroke width of TextPaint" />
|
||||
|
||||
### Bullet width
|
||||
|
||||
The width of the bullet item
|
||||
|
||||
* First
|
||||
* Second
|
||||
* Third
|
||||
|
||||
<ThemeProperty name="bulletWidth" type="@Px int" defaults="min(<a href='#block-margin'>blockMargin</a>, lineHeight) / 2" />
|
||||
|
||||
## Code
|
||||
|
||||
### Inline code text color
|
||||
|
||||
The color of the `code` content
|
||||
|
||||
<ThemeProperty name="codeTextColor" type="@ColorInt int" defaults="Content text color" />
|
||||
|
||||
### Inline code background color
|
||||
|
||||
The color of `background` of a code content
|
||||
|
||||
<ThemeProperty name="codeBackgroundColor" type="@ColorInt int" defaults="<a href='#inline-code-text-color'>inline code text color</a> with 25 (0-255) alpha" />
|
||||
|
||||
### Block code text color
|
||||
|
||||
```
|
||||
The color of code block text
|
||||
```
|
||||
|
||||
<ThemeProperty name="codeBlockTextColor" type="@ColorInt int" defaults="<a href='#inline-code-text-color'>inline code text color</a>" />
|
||||
|
||||
### Block code background color
|
||||
|
||||
```
|
||||
The color of background of code block text
|
||||
```
|
||||
|
||||
<ThemeProperty name="codeBlockBackgroundColor" type="@ColorInt int" defaults="<a href='#inline-code-background-color'>inline code background color</a>" />
|
||||
|
||||
### Block code leading margin
|
||||
|
||||
Leading margin for the block code content
|
||||
|
||||
<ThemeProperty name="codeMultilineMargin" type="@Px int" defaults="8dip" />
|
||||
|
||||
### Code typeface
|
||||
|
||||
Typeface of code content
|
||||
|
||||
<ThemeProperty name="codeTypeface" type="android.graphics.Typeface" defaults="Typeface.MONOSPACE" />
|
||||
|
||||
### Block code typeface <Badge text="3.0.0" />
|
||||
|
||||
Typeface of block code content
|
||||
|
||||
<ThemeProperty name="codeBlockTypeface" type="android.graphics.Typeface" defaults="<code>codeTypeface</code> if set or Typeface.MONOSPACE" />
|
||||
|
||||
### Code text size
|
||||
|
||||
Text size of code content
|
||||
|
||||
<ThemeProperty name="codeTextSize" type="@Px int" defaults="(Content text size) * 0.87 if no custom <a href='#code-typeface'>Typeface</a> was set, otherwise (content text size)" />
|
||||
|
||||
### Block code text size <Badge text="3.0.0" />
|
||||
|
||||
Text size of block code content
|
||||
|
||||
<ThemeProperty name="codeBlockTextSize" type="@Px int" defaults="<code>codeTextSize</code> if set or (content text size) * 0.87 if no custom <a href='#code-typeface'>Typeface</a> was set, otherwise (content text size)" />
|
||||
|
||||
## Heading
|
||||
|
||||
### Break height
|
||||
|
||||
The height of a brake under H1 & H2
|
||||
|
||||
<ThemeProperty name="headingBreakHeight" type="@Px int" defaults="Stroke width of context TextPaint" />
|
||||
|
||||
### Break color
|
||||
|
||||
The color of a brake under H1 & H2
|
||||
|
||||
<ThemeProperty name="headingBreakColor" type="@ColorInt int" defaults="(text color) with 75 (0-255) alpha" />
|
||||
|
||||
### Typeface <Badge text="1.1.0" />
|
||||
|
||||
The typeface of heading elements
|
||||
|
||||
<ThemeProperty name="headingTypeface" type="android.graphics.Typeface" defaults="default text Typeface" />
|
||||
|
||||
### Text size <Badge text="1.1.0" />
|
||||
|
||||
Array of heading text sizes _ratio_ that is applied to text size
|
||||
|
||||
<ThemeProperty name="headingTextSizeMultipliers" type="float[]" defaults="<code>{2.F, 1.5F, 1.17F, 1.F, .83F, .67F}</code> (HTML spec)" />
|
||||
|
||||
## Thematic break
|
||||
|
||||
### Color
|
||||
|
||||
Color of a thematic break
|
||||
|
||||
<ThemeProperty name="thematicBreakColor" type="@ColorInt int" defaults="(text color) with 25 (0-255) alpha" />
|
||||
|
||||
### Height
|
||||
|
||||
Height of a thematic break
|
||||
|
||||
<ThemeProperty name="thematicBreakHeight" type="@Px int" defaults="Stroke width of context TextPaint" />
|
73
docs/docs/v3/core/visitor.md
Normal file
@ -0,0 +1,73 @@
|
||||
# Visitor
|
||||
|
||||
Starting with <Badge text="3.0.0" /> _visiting_ of parsed markdown
|
||||
nodes does not require creating own instance of commonmark-java `Visitor`,
|
||||
instead a composable/configurable `MarkwonVisitor` is used.
|
||||
|
||||
## Visitor.Builder
|
||||
There is no need to create own instance of `MarkwonVisitor.Builder` as
|
||||
it is done by `Markwon` itself. One still can configure it as one wishes:
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(contex)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor<SoftLineBreak>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) {
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
`MarkwonVisitor` encapsulates most of the functionality of rendering parsed markdown.
|
||||
|
||||
It holds rendering configuration:
|
||||
* `MarkwonVisitor#configuration` - getter for current [MarkwonConfiguration](/docs/v3/core/configuration.md)
|
||||
* `MarkwonVisitor#renderProps` - getter for current [RenderProps](/docs/v3/core/render-props.md)
|
||||
* `MarkwonVisitor#builder` - getter for current `SpannableBuilder`
|
||||
|
||||
It contains also a number of utility functions:
|
||||
* `visitChildren(Node)` - will visit all children of supplied Node
|
||||
* `hasNext(Node)` - utility function to check if supplied Node has a Node after it (useful for white-space management, so there should be no blank new line after last BlockNode)
|
||||
* `ensureNewLine` - will insert a new line at current `SpannableBuilder` position only if current (last) character is not a new-line
|
||||
* `forceNewLine` - will insert a new line character without any condition checking
|
||||
* `length` - helper function to call `visitor.builder().length()`, returns current length of `SpannableBuilder`
|
||||
* `clear` - will clear state for `RenderProps` and `SpannableBuilder`, this is done by `Markwon` automatically after each render call
|
||||
|
||||
And some utility functions to control the spans:
|
||||
* `setSpans(int start, Object spans)` - will apply supplied `spans` on `SpannableBuilder` starting at `start` position and ending at `SpannableBuilder#length`. `spans` can be `null` (no spans will be applied) or an array of spans (each span of this array will be applied)
|
||||
* `setSpansForNodeOptional(N node, int start)` - helper method to set spans for specified `node` (internally obtains `SpanFactory` for that node and uses it to apply spans)
|
||||
* `setSpansForNode(N node, int start)` - almost the same as `setSpansForNodeOptional` but instead of silently ignoring call if none `SpanFactory` is registered, this method will throw an exception.
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Heading.class, new MarkwonVisitor.NodeVisitor<Heading>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
|
||||
|
||||
// or just `visitor.length()`
|
||||
final int start = visitor.builder().length();
|
||||
|
||||
visitor.visitChildren(heading);
|
||||
|
||||
// or just `visitor.setSpansForNodeOptional(heading, start)`
|
||||
final SpanFactory factory = visitor.configuration().spansFactory().get(heading.getClass());
|
||||
if (factory != null) {
|
||||
visitor.setSpans(start, factory.getSpans(visitor.configuration(), visitor.renderProps()));
|
||||
}
|
||||
|
||||
if (visitor.hasNext(heading)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
46
docs/docs/v3/ext-latex/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# LaTeX extension
|
||||
|
||||
<MavenBadge :artifact="'ext-latex'" />
|
||||
|
||||
This is an extension that will help you display LaTeX formulas in your markdown.
|
||||
Syntax is pretty simple: pre-fix and post-fix your latex with `$$` (double dollar sign).
|
||||
`$$` should be the first characters in a line.
|
||||
|
||||
```markdown
|
||||
$$
|
||||
\\text{A long division \\longdiv{12345}{13}
|
||||
$$
|
||||
```
|
||||
|
||||
```markdown
|
||||
$$\\text{A long division \\longdiv{12345}{13}$$
|
||||
```
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.use(ImagesPlugin.create(context))
|
||||
.use(JLatexMathPlugin.create(textSize))
|
||||
.build();
|
||||
```
|
||||
|
||||
This extension uses [jlatexmath-android](https://github.com/noties/jlatexmath-android) artifact to create LaTeX drawable. Then it
|
||||
registers special `latex` image scheme handler and uses `AsyncDrawableLoader` to display
|
||||
final result
|
||||
|
||||
## Config
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create(context))
|
||||
.usePlugin(JLatexMathPlugin.create(textSize, new BuilderConfigure() {
|
||||
@Override
|
||||
public void configureBuilder(@NonNull Builder builder) {
|
||||
builder
|
||||
.background(backgroundDrawable)
|
||||
.align(JLatexMathDrawable.ALIGN_CENTER)
|
||||
.fitCanvas(true)
|
||||
.padding(paddingPx);
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
```
|
29
docs/docs/v3/ext-strikethrough/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Strikethrough extension
|
||||
|
||||
<MavenBadge :artifact="'ext-strikethrough'" />
|
||||
|
||||
This module adds `strikethrough` functionality to `Markwon` via `StrikethroughPlugin`:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
```
|
||||
|
||||
This plugin registers `SpanFactory` for `Strikethrough` node, so it's possible to customize Strikethrough Span that is used in rendering:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.setFactory(Strikethrough.class, new SpanFactory() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
// will use Underline span instead of Strikethrough
|
||||
return new UnderlineSpan();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
```
|
99
docs/docs/v3/ext-tables/README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# Tables extension
|
||||
|
||||
<MavenBadge :artifact="'ext-tables'" />
|
||||
|
||||
This extension adds support for GFM tables.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
// create default instance of TablePlugin
|
||||
.usePlugin(TablePlugin.create(context))
|
||||
```
|
||||
|
||||
```java
|
||||
final TableTheme tableTheme = TableTheme.builder()
|
||||
.tableBorderColor(Color.RED)
|
||||
.tableBorderWidth(0)
|
||||
.tableCellPadding(0)
|
||||
.tableHeaderRowBackgroundColor(Color.BLACK)
|
||||
.tableEvenRowBackgroundColor(Color.GREEN)
|
||||
.tableOddRowBackgroundColor(Color.YELLOW)
|
||||
.build();
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(TablePlugin.create(tableTheme))
|
||||
```
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(TablePlugin.create(builder ->
|
||||
builder
|
||||
.tableBorderColor(Color.RED)
|
||||
.tableBorderWidth(0)
|
||||
.tableCellPadding(0)
|
||||
.tableHeaderRowBackgroundColor(Color.BLACK)
|
||||
.tableEvenRowBackgroundColor(Color.GREEN)
|
||||
.tableOddRowBackgroundColor(Color.YELLOW)
|
||||
))
|
||||
```
|
||||
|
||||
Please note, that _by default_ tables have limitations. For example, there is no support
|
||||
for images inside table cells. And table contents won't be copied to clipboard if a TextView
|
||||
has such functionality. Table will always take full width of a TextView in which it is displayed.
|
||||
All columns will always be the of the same width. So, _default_ implementation provides basic
|
||||
functionality which can answer some needs. These all come from the limited nature of the TextView
|
||||
to display such content.
|
||||
|
||||
In order to provide full-fledged experience, tables must be displayed in a special widget.
|
||||
Since version `3.0.0` Markwon provides a special artifact `markwon-recycler` that allows
|
||||
to render markdown in a set of widgets in a RecyclerView. It also gives ability to change
|
||||
display widget form TextView to any other.
|
||||
|
||||
```java
|
||||
final Table table = Table.parse(Markwon, TableBlock);
|
||||
myTableWidget.setTable(table);
|
||||
```
|
||||
|
||||
:::tip
|
||||
To take advantage of this functionality and render tables without limitations (including
|
||||
horizontally scrollable layout when its contents exceed screen width), refer to [recycler-table](/docs/v3/recycler-table/)
|
||||
module documentation that adds support for rendering `TableBlock` markdown node inside Android-native `TableLayout` widget.
|
||||
:::
|
||||
|
||||
## Theme
|
||||
|
||||
### Cell padding
|
||||
|
||||
Padding inside a table cell
|
||||
|
||||
<ThemeProperty name="tableCellPadding" type="@Px int" defaults="0" />
|
||||
|
||||
### Border color
|
||||
|
||||
The color of table borders
|
||||
|
||||
<ThemeProperty name="tableBorderColor" type="@ColorInt int" defaults="(text color) with 75 (0-255) alpha" />
|
||||
|
||||
### Border width
|
||||
|
||||
The width of table borders
|
||||
|
||||
<ThemeProperty name="tableBorderWidth" type="@Px int" defaults="Stroke with of context TextPaint" />
|
||||
|
||||
### Odd row background
|
||||
|
||||
Background of an odd table row
|
||||
|
||||
<ThemeProperty name="tableOddRowBackgroundColor" type="@ColorInt int" defaults="(text color) with 22 (0-255) alpha" />
|
||||
|
||||
### Even row background <Badge text="1.1.1" />
|
||||
|
||||
Background of an even table row
|
||||
|
||||
<ThemeProperty name="tableEventRowBackgroundColor" type="@ColorInt int" defaults="0" />
|
||||
|
||||
### Header row background <Badge text="1.1.1" />
|
||||
|
||||
Background of header table row
|
||||
|
||||
<ThemeProperty name="tableHeaderRowBackgroundColor" type="@ColorInt int" defaults="0" />
|
146
docs/docs/v3/ext-tasklist/README.md
Normal file
@ -0,0 +1,146 @@
|
||||
# Task list extension
|
||||
|
||||
<MavenBadge :artifact="'ext-tasklist'" />
|
||||
|
||||
Adds support for GFM (Github-flavored markdown) task-lists:
|
||||
|
||||
```java
|
||||
Markwon.builder(context)
|
||||
.usePlugin(TaskListPlugin.create(context));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Create a default instance of `TaskListPlugin` with `TaskListDrawable` initialized to use
|
||||
`android.R.attr.textColorLink` as primary color and `android.R.attr.colorBackground` as background
|
||||
```java
|
||||
TaskListPlugin.create(context);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Create an instance of `TaskListPlugin` with exact color values to use:
|
||||
```java
|
||||
// obtain color values
|
||||
final int checkedFillColor = /* */;
|
||||
final int normalOutlineColor = /* */;
|
||||
final int checkMarkColor = /* */;
|
||||
|
||||
TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Specify own drawable for a task list item:
|
||||
|
||||
```java
|
||||
// obtain drawable
|
||||
final Drawable drawable = /* */;
|
||||
|
||||
TaskListPlugin.create(drawable);
|
||||
```
|
||||
|
||||
:::warning
|
||||
Please note that custom drawable for a task list item must correctly handle state
|
||||
in order to display done/not-done:
|
||||
|
||||
```java
|
||||
public class MyTaskListDrawable extends Drawable {
|
||||
|
||||
private boolean isChecked;
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
// draw accordingly to the isChecked value
|
||||
}
|
||||
|
||||
/* implementation omitted */
|
||||
|
||||
@Override
|
||||
protected boolean onStateChange(int[] state) {
|
||||
final boolean isChecked = contains(state, android.R.attr.state_checked);
|
||||
final boolean result = this.isChecked != isChecked;
|
||||
if (result) {
|
||||
this.isChecked = isChecked;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean contains(@Nullable int[] states, int value) {
|
||||
if (states != null) {
|
||||
for (int state : states) {
|
||||
if (state == value) {
|
||||
// NB return here
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## Task list mutation
|
||||
|
||||
It is possible to mutate task list item state (toggle done/not-done). But note
|
||||
that `Markwon` won't handle state change internally by any means and this change
|
||||
is merely a visual one. If you need to persist state of a task list
|
||||
item change you have to implement it yourself. This should get your started:
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
|
||||
// obtain original SpanFactory set by TaskListPlugin
|
||||
final SpanFactory origin = builder.getFactory(TaskListItem.class);
|
||||
if (origin == null) {
|
||||
// or throw, as it's a bit weird state and we expect
|
||||
// this factory to be present
|
||||
return;
|
||||
}
|
||||
|
||||
builder.setFactory(TaskListItem.class, new SpanFactory() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
// it's a bit non-secure behavior and we should validate
|
||||
// the type of returned span first, but for the sake of brevity
|
||||
// we skip this step
|
||||
final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props);
|
||||
|
||||
if (span == null) {
|
||||
// or throw
|
||||
return null;
|
||||
}
|
||||
|
||||
// return an array of spans
|
||||
return new Object[]{
|
||||
span,
|
||||
new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
// toggle VISUAL state
|
||||
span.setDone(!span.isDone());
|
||||
|
||||
// do not forget to invalidate widget
|
||||
widget.invalidate();
|
||||
|
||||
// execute your persistence logic
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
// no-op, so appearance is not changed (otherwise
|
||||
// task list item will look like a link)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
63
docs/docs/v3/html/README.md
Normal file
@ -0,0 +1,63 @@
|
||||
# HTML
|
||||
|
||||
This artifact encapsulates HTML parsing from the core artifact and provides
|
||||
few predefined `TagHandlers`
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.build();
|
||||
```
|
||||
|
||||
As this artifact brings modified [jsoup](https://github.com/jhy/jsoup) library
|
||||
it was moved to a standalone module in order to minimize dependencies and unused code
|
||||
in applications that does not require HTML render capabilities.
|
||||
|
||||
Before <Badge text="2.0.0" /> `Markwon` used android `Html` class for parsing and
|
||||
rendering. Unfortunately, according to markdown specification, markdown can contain
|
||||
HTML in _unpredictable_ way if rendered _outside_ of browser. For example:
|
||||
|
||||
```markdown{4}
|
||||
<i>
|
||||
Hello from italics tag
|
||||
|
||||
</i><b>bold></b>
|
||||
```
|
||||
|
||||
This snippet could be represented as:
|
||||
* HtmlBlock (`<i>\nHello from italics tag`)
|
||||
* HtmlInline (`<i>`)
|
||||
* HtmlInline (`<b>`)
|
||||
* Text (`bold`)
|
||||
* HtmlInline (`</b>`)
|
||||
|
||||
:::tip A bit of background
|
||||
<br>
|
||||
<GithubIssue id="52" displayName="This issue" /> had brought attention to differences between HTML & commonmark implementations. <br><br>
|
||||
:::
|
||||
|
||||
Unfortunately Android `HTML` class cannot parse a _fragment_ of HTML to later
|
||||
be included in a bigger set of content. This is why the decision was made to bring
|
||||
HTML parsing _in-markwon-house_
|
||||
|
||||
## Predefined TagHandlers
|
||||
* `<img>`
|
||||
* `<a>`
|
||||
* `<blockquote>`
|
||||
* `<sub>`
|
||||
* `<sup>`
|
||||
* `<b>, <strong>`
|
||||
* `<s>, <del>`
|
||||
* `<u>, <ins>`
|
||||
* `<ul>, <ol>`
|
||||
* `<i>, <cite>, <em>, <dfn>`
|
||||
* `<h1>, <h2>, <h3>, <h4>, <h5>, <h6>`
|
||||
|
||||
:::tip
|
||||
All predefined tag handlers will use styling spans for native markdown content.
|
||||
So, if your `Markwon` instance was configured to, for example, render Emphasis
|
||||
nodes as a <span style="color: #FF0000">red text</span> then HTML tag handler will
|
||||
use the same span. This includes images, links, UrlResolver, LinkProcessor, etc
|
||||
:::
|
||||
|
||||
To learn more about defining own TagHandlers, please refer to [html-renderer docs](/docs/v3/core/html-renderer.md)
|
15
docs/docs/v3/image/gif.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Image GIF
|
||||
|
||||
<MavenBadge :artifact="'image-gif'" />
|
||||
|
||||
Adds support for GIF images inside markdown.
|
||||
Relies on [android-gif-drawable library](https://github.com/koral--/android-gif-drawable)
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
// it's required to register ImagesPlugin
|
||||
.usePlugin(ImagesPlugin.create(context))
|
||||
// add GIF support for images
|
||||
.usePlugin(GifPlugin.create())
|
||||
.build();
|
||||
```
|
28
docs/docs/v3/image/okhttp.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Image OkHttp
|
||||
|
||||
<MavenBadge :artifact="'image-okhttp'" />
|
||||
|
||||
Uses [okhttp library](https://github.com/square/okhttp) as the network transport fro images. Since <Badge text="3.0.0" />
|
||||
`Markwon` uses a system-native `HttpUrlConnection` and does not rely on any
|
||||
3rd-party tool to download resources from network. It can answer the most common needs,
|
||||
but if you would like to have a custom redirect policy or add an explicit caching
|
||||
of downloaded resources OkHttp might be a better option.
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
// it's required to register ImagesPlugin
|
||||
.usePlugin(ImagesPlugin.create(context))
|
||||
|
||||
// will create default instance of OkHttpClient
|
||||
.usePlugin(OkHttpImagesPlugin.create())
|
||||
|
||||
// or accept a configured client
|
||||
.usePlugin(OkHttpImagesPlugin.create(new OkHttpClient()))
|
||||
.build();
|
||||
```
|
||||
|
||||
## Proguard
|
||||
```proguard
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
```
|
25
docs/docs/v3/image/svg.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Image SVG
|
||||
|
||||
<MavenBadge :artifact="'image-svg'" />
|
||||
|
||||
Adds support for SVG images inside markdown.
|
||||
Relies on [androidsvg library](https://github.com/BigBadaboom/androidsvg)
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
// it's required to register ImagesPlugin
|
||||
.usePlugin(ImagesPlugin.create(context))
|
||||
.usePlugin(SvgPlugin.create(context.getResources()))
|
||||
.build();
|
||||
```
|
||||
|
||||
:::tip
|
||||
`SvgPlugin` requires `Resources` in order to scale SVG media based on display density
|
||||
:::
|
||||
|
||||
## Proguard
|
||||
|
||||
```proguard
|
||||
-keep class com.caverock.androidsvg.** { *; }
|
||||
-dontwarn com.caverock.androidsvg.**
|
||||
```
|
34
docs/docs/v3/install.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
prev: false
|
||||
next: /docs/v3/core/getting-started.md
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||

|
||||

|
||||
|
||||
<ArtifactPicker />
|
||||
|
||||
## Snapshot
|
||||
|
||||
In order to use latest `SNAPSHOT` version add snapshot repository
|
||||
to your root project's `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
// this one 👇
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } // 👈 this one
|
||||
// this one 👆
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip Info
|
||||
All official artifacts share the same version number and all
|
||||
are uploaded to **release** and **snapshot** repositories
|
||||
:::
|
||||
|
12
docs/docs/v3/migration-2-3.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Migration 2.x.x -> 3.x.x
|
||||
|
||||
* strikethrough moved to standalone module
|
||||
* tables moved to standalone module
|
||||
* core functionality of `AsyncDrawableLoader` moved to `core` module
|
||||
* * Handling of GIF and SVG media moved to standalone modules (`gif` and `svg` respectively)
|
||||
* * OkHttpClient to download images moved to standalone module
|
||||
* HTML no longer _implicitly_ added to core functionality, it must be specified __explicitly__ (as an artifact)
|
||||
* removed `markwon-view` module
|
||||
* changed Maven artifacts group to `ru.noties.markwon`
|
||||
* removed `errorDrawable` in AsyncDrawableLoader in favor of a drawable provider
|
||||
* added placeholder for AsyncDrawableProvider
|
92
docs/docs/v3/recycler-table/README.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Recycler Table <Badge text="3.0.0" />
|
||||
|
||||
<MavenBadge :artifact="'recycler-table'" />
|
||||
|
||||
Artifact that provides [MarkwonAdapter.Entry](/docs/v3/recycler/) to render `TableBlock` inside
|
||||
Android-native `TableLayout` widget.
|
||||
|
||||
<img :src="$withBase('/assets/recycler-table-screenshot.png')" alt="screenshot" width="45%">
|
||||
<br>
|
||||
<small><em><sup>*</sup> It's possible to wrap `TableLayout` inside a `HorizontalScrollView` to include all table content</em></small>
|
||||
|
||||
---
|
||||
|
||||
Register instance of `TableEntry` with `MarkwonAdapter` to render TableBlocks:
|
||||
```java
|
||||
final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text)
|
||||
.include(TableBlock.class, TableEntry.create(builder -> builder
|
||||
.tableLayout(R.layout.adapter_table_block, R.id.table_layout)
|
||||
.textLayoutIsRoot(R.layout.view_table_entry_cell)))
|
||||
.build();
|
||||
```
|
||||
|
||||
`TableEntry` requires at least 2 arguments:
|
||||
* `tableLayout` - layout with `TableLayout` inside
|
||||
* `textLayout` - layout with `TextView` inside (represents independent table cell)
|
||||
|
||||
In case when required view is the root of layout specific builder methods can be used:
|
||||
* `tableLayoutIsRoot(int)`
|
||||
* `textLayoutIsRoot(int)`
|
||||
|
||||
If your layouts have different structure (for example wrap a `TableView` inside a `HorizontalScrollView`)
|
||||
then you should use methods that accept ID of required view inside layout:
|
||||
* `tableLayout(int, int)`
|
||||
* `textLayout(int, int)`
|
||||
|
||||
---
|
||||
|
||||
To display `TableBlock` as a `TableLayout` specific `MarkwonPlugin` must be used: `TableEntryPlugin`.
|
||||
|
||||
:::warning
|
||||
Do not use `TablePlugin` if you wish to display markdown tables via `TableEntry`. Use **TableEntryPlugin** instead
|
||||
:::
|
||||
|
||||
`TableEntryPlugin` can reuse existing `TablePlugin` to make appearance of tables the same in both contexts:
|
||||
when rendering _natively_ in a TextView and when rendering in RecyclerView with TableEntry.
|
||||
|
||||
* `TableEntryPlugin.create(Context)` - creates plugin with default `TableTheme`
|
||||
* `TableEntryPlugin.create(TableTheme)` - creates plugin with provided `TableTheme`
|
||||
* `TableEntryPlugin.create(TablePlugin.ThemeConfigure)` - creates plugin with theme configured by `ThemeConfigure`
|
||||
* `TableEntryPlugin.create(TablePlugin)` - creates plugin with `TableTheme` used in provided `TablePlugin`
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(TableEntryPlugin.create(context))
|
||||
// other plugins
|
||||
.build();
|
||||
```
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(TableEntryPlugin.create(builder -> builder
|
||||
.tableBorderWidth(0)
|
||||
.tableHeaderRowBackgroundColor(Color.RED)))
|
||||
// other plugins
|
||||
.build();
|
||||
```
|
||||
|
||||
## Table with scrollable content
|
||||
|
||||
To stretch table columns to fit the width of screen or to make table scrollable when content exceeds screen width
|
||||
this layout can be used:
|
||||
|
||||
```xml
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:scrollbarStyle="outsideInset">
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/table_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="*" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
```
|
153
docs/docs/v3/recycler/README.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Recycler <Badge text="3.0.0" />
|
||||
|
||||
<MavenBadge :artifact="'recycler'" />
|
||||
|
||||
This artifact allows displaying markdown in a set of Android widgets
|
||||
inside a RecyclerView. Can be useful when displaying lengthy markdown
|
||||
content or **displaying certain markdown blocks inside specific widgets**.
|
||||
|
||||
```java
|
||||
// create an adapter that will use a TextView for each block of markdown
|
||||
// `createTextViewIsRoot` accepts a layout in which TextView is the root view
|
||||
final MarkwonAdapter adapter =
|
||||
MarkwonAdapter.createTextViewIsRoot(R.layout.adapter_default_entry);
|
||||
```
|
||||
|
||||
```java
|
||||
// `create` method accepts a layout with TextView and ID of a TextView
|
||||
// which allows wrapping a TextView inside another widget or combine with other widgets
|
||||
final MarkwonAdapter adapter =
|
||||
MarkwonAdapter.create(R.layout.adapter_default_entry, R.id.text_view);
|
||||
|
||||
// initialize RecyclerView (LayoutManager, Decorations, etc)
|
||||
final RecyclerView recyclerView = obtainRecyclerView();
|
||||
|
||||
// set adapter
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
// obtain an instance of Markwon (register all required plugins)
|
||||
final Markwon markwon = obtainMarkwon();
|
||||
|
||||
// set markdown to be displayed
|
||||
adapter.setMarkdown(markwon, "# This is markdown!");
|
||||
|
||||
// NB, adapter does not handle updates on its own, please use
|
||||
// whatever method appropriate for you.
|
||||
adapter.notifyDataSetChanged();
|
||||
```
|
||||
|
||||
Initialized adapter above will use a TextView for each markdown block.
|
||||
In order to tell adapter to render certain blocks differently a `builder` can be used.
|
||||
For example, let's render `FencedCodeBlock` inside a `HorizontalScrollView`:
|
||||
|
||||
```java
|
||||
// we still need to have a _default_ entry
|
||||
final MarkwonAdapter adapter =
|
||||
MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_default_entry)
|
||||
.include(FencedCodeBlock.class, new FencedCodeBlockEntry())
|
||||
.build();
|
||||
```
|
||||
|
||||
where `FencedCodeBlockEntry` is:
|
||||
|
||||
```java
|
||||
public class FencedCodeBlockEntry extends MarkwonAdapter.Entry<FencedCodeBlock, FencedCodeBlockEntry.Holder> {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
|
||||
return new Holder(inflater.inflate(R.layout.adapter_fenced_code_block, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull FencedCodeBlock node) {
|
||||
markwon.setParsedMarkdown(holder.textView, markwon.render(node));
|
||||
}
|
||||
|
||||
public static class Holder extends MarkwonAdapter.Holder {
|
||||
|
||||
final TextView textView;
|
||||
|
||||
public Holder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.textView = requireView(R.id.text_view);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
and its layout (`R.layout.adapter_fenced_code_block`):
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingRight="16dip"
|
||||
android:scrollbarStyle="outsideInset">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#0f000000"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
```
|
||||
|
||||
As we apply styling to `FencedCodeBlock` _manually_, we no longer need
|
||||
`Markwon` to apply styling spans for us, so `Markwon` initialization could be:
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
// your other plugins
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(FencedCodeBlock.class, (visitor, fencedCodeBlock) -> {
|
||||
// we actually won't be applying code spans here, as our custom view will
|
||||
// draw background and apply mono typeface
|
||||
//
|
||||
// NB the `trim` operation on literal (as code will have a new line at the end)
|
||||
final CharSequence code = visitor.configuration()
|
||||
.syntaxHighlight()
|
||||
.highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral().trim());
|
||||
visitor.builder().append(code);
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
Previously we have created a `FencedCodeBlockEntry` but all it does is apply markdown to a TextView.
|
||||
For such a case there is a `SimpleEntry` that could be used instead:
|
||||
|
||||
```java
|
||||
final MarkwonAdapter adapter =
|
||||
MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_default_entry)
|
||||
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text_view))
|
||||
.build();
|
||||
```
|
||||
|
||||
:::tip
|
||||
`SimpleEntry` also takes care of _caching_ parsed markdown. So each node will be
|
||||
parsed only once and each subsequent adapter binding call will reuse previously cached markdown.
|
||||
:::
|
||||
|
||||
:::tip Tables
|
||||
There is a standalone artifact that adds support for displaying markdown tables
|
||||
natively via `TableLayout`. Please refer to its [documentation](/docs/v3/recycler-table/)
|
||||
:::
|
69
docs/docs/v3/syntax-highlight/README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Syntax highlight
|
||||
|
||||
<MavenBadge :artifact="'syntax-highlight'" />
|
||||
|
||||
This is a simple module to add **syntax highlight** functionality to your markdown rendered with `Markwon` library. It is based on [Prism4j](https://github.com/noties/Prism4j) so lead there to understand how to configure `Prism4j` instance.
|
||||
|
||||
<img :src="$withBase('/art/markwon-syntax-default.png')" alt="theme-default" width="80%">
|
||||
|
||||
|
||||
<img :src="$withBase('/art/markwon-syntax-darkula.png')" alt="theme-darkula" width="80%">
|
||||
|
||||
---
|
||||
|
||||
First, we need to obtain an instance of `Prism4jSyntaxHighlight` which implements Markwon's `SyntaxHighlight`:
|
||||
|
||||
```java
|
||||
final SyntaxHighlight highlight =
|
||||
Prism4jSyntaxHighlight.create(Prism4j, Prism4jTheme);
|
||||
```
|
||||
|
||||
we also can obtain an instance of `Prism4jSyntaxHighlight` that has a _fallback_ option (if a language is not defined in `Prism4j` instance, fallback language can be used):
|
||||
|
||||
```java
|
||||
final SyntaxHighlight highlight =
|
||||
Prism4jSyntaxHighlight.create(Prism4j, Prism4jTheme, String);
|
||||
```
|
||||
|
||||
Generally obtaining a `Prism4j` instance is pretty easy:
|
||||
|
||||
```java
|
||||
final Prism4j prism4j = new Prism4j(new GrammarLocatorDef());
|
||||
```
|
||||
|
||||
Where `GrammarLocatorDef` is a generated grammar locator (if you use `prism4j-bundler` annotation processor)
|
||||
|
||||
`Prism4jTheme` is a specific type that is defined in this module (`prism4j` doesn't know anything about rendering). It has 2 implementations:
|
||||
|
||||
* `Prism4jThemeDefault`
|
||||
* `Prism4jThemeDarkula`
|
||||
|
||||
Both of them can be obtained via factory method `create`:
|
||||
|
||||
* `Prism4jThemeDefault.create()`
|
||||
* `Prism4jThemeDarkula.create()`
|
||||
|
||||
But of cause nothing is stopping you from defining your own theme:
|
||||
|
||||
```java
|
||||
public interface Prism4jTheme {
|
||||
|
||||
@ColorInt
|
||||
int background();
|
||||
|
||||
@ColorInt
|
||||
int textColor();
|
||||
|
||||
void apply(
|
||||
@NonNull String language,
|
||||
@NonNull Prism4j.Syntax syntax,
|
||||
@NonNull SpannableStringBuilder builder,
|
||||
int start,
|
||||
int end
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
You can extend `Prism4jThemeBase` which has some helper methods
|
||||
:::
|
34
docs/package-lock.json
generated
@ -2471,6 +2471,24 @@
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
|
||||
},
|
||||
"commonmark": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.28.1.tgz",
|
||||
"integrity": "sha1-Buq41SM4uDn6Gi11rwCF7tGxvq4=",
|
||||
"requires": {
|
||||
"entities": "~ 1.1.1",
|
||||
"mdurl": "~ 1.0.1",
|
||||
"minimist": "~ 1.2.0",
|
||||
"string.prototype.repeat": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
|
||||
@ -5465,9 +5483,9 @@
|
||||
}
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz",
|
||||
"integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz",
|
||||
"integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
@ -5700,11 +5718,6 @@
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.3.tgz",
|
||||
"integrity": "sha512-x/OdaRzLYxAjmB+jIVlXuE3nX7tZTLDQxm58RkgjTLyQ+I290jYQvPS9cJjVN6SM3U6K6CHKYNgUtPNZmLblYQ=="
|
||||
},
|
||||
"markdown-it-task-lists": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
|
||||
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="
|
||||
},
|
||||
"math-expression-evaluator": {
|
||||
"version": "1.2.17",
|
||||
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
|
||||
@ -9060,6 +9073,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"string.prototype.repeat": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz",
|
||||
"integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"scripts": {
|
||||
"docs:build": "vuepress build"
|
||||
"docs:build": "node ./collectArtifacts.js && vuepress build"
|
||||
},
|
||||
"dependencies": {
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"commonmark": "^0.28.1",
|
||||
"vuepress": "^0.14.2"
|
||||
}
|
||||
}
|
||||
|
3
docs/sandbox.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Commonmark Sandbox
|
||||
|
||||
<CommonmarkSandbox />
|
@ -6,10 +6,10 @@ org.gradle.configureondemand=true
|
||||
android.enableBuildCache=true
|
||||
android.buildCacheDir=build/pre-dex-cache
|
||||
|
||||
VERSION_NAME=2.0.1
|
||||
VERSION_NAME=3.0.0
|
||||
|
||||
GROUP=ru.noties
|
||||
POM_DESCRIPTION=Markwon
|
||||
GROUP=ru.noties.markwon
|
||||
POM_DESCRIPTION=Markwon markdown for Android
|
||||
POM_URL=https://github.com/noties/Markwon
|
||||
POM_SCM_URL=https://github.com/noties/Markwon
|
||||
POM_SCM_CONNECTION=scm:git:git://github.com/noties/Markwon.git
|
||||
|
@ -15,16 +15,20 @@ android {
|
||||
|
||||
dependencies {
|
||||
|
||||
api project(':markwon-html-parser-api')
|
||||
|
||||
deps.with {
|
||||
api it['support-annotations']
|
||||
api it['commonmark']
|
||||
}
|
||||
|
||||
deps.test.with {
|
||||
deps['test'].with {
|
||||
|
||||
testImplementation project(':markwon-test-span')
|
||||
|
||||
testImplementation it['junit']
|
||||
testImplementation it['robolectric']
|
||||
testImplementation it['mockito']
|
||||
|
||||
testImplementation it['commons-io']
|
||||
}
|
||||
}
|
||||
|
4
markwon-core/gradle.properties
Normal file
@ -0,0 +1,4 @@
|
||||
POM_NAME=Core
|
||||
POM_ARTIFACT_ID=core
|
||||
POM_DESCRIPTION=Core Markwon artifact that includes basic markdown parsing and rendering
|
||||
POM_PACKAGING=aar
|
@ -0,0 +1,92 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import ru.noties.markwon.core.CorePlugin;
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.priority.Priority;
|
||||
|
||||
/**
|
||||
* Class that extends {@link MarkwonPlugin} with all methods implemented (empty body)
|
||||
* for easier plugin implementation. Only required methods can be overriden
|
||||
*
|
||||
* @see MarkwonPlugin
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Priority priority() {
|
||||
// by default all come after CorePlugin
|
||||
return Priority.after(CorePlugin.class);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String processMarkdown(@NonNull String markdown) {
|
||||
return markdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeRender(@NonNull Node node) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import ru.noties.markwon.spans.LinkSpan;
|
||||
import ru.noties.markwon.core.spans.LinkSpan;
|
||||
|
||||
public class LinkResolverDef implements LinkSpan.Resolver {
|
||||
@Override
|
136
markwon-core/src/main/java/ru/noties/markwon/Markwon.java
Normal file
@ -0,0 +1,136 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import ru.noties.markwon.core.CorePlugin;
|
||||
|
||||
/**
|
||||
* Class to parse and render markdown. Since version 3.0.0 instance specific (previously consisted
|
||||
* of static stateless methods). An instance of builder can be obtained via {@link #builder(Context)}
|
||||
* method.
|
||||
*
|
||||
* @see #create(Context)
|
||||
* @see #builder(Context)
|
||||
* @see Builder
|
||||
*/
|
||||
public abstract class Markwon {
|
||||
|
||||
/**
|
||||
* Factory method to create a <em>minimally</em> functional {@link Markwon} instance. This
|
||||
* instance will have <strong>only</strong> {@link CorePlugin} registered. If you wish
|
||||
* to configure this instance more consider using {@link #builder(Context)} method.
|
||||
*
|
||||
* @return {@link Markwon} instance with only CorePlugin registered
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Markwon create(@NonNull Context context) {
|
||||
return builder(context)
|
||||
.usePlugin(CorePlugin.create())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to obtain an instance of {@link Builder}.
|
||||
*
|
||||
* @see Builder
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull Context context) {
|
||||
return new MarkwonBuilderImpl(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to parse markdown (without rendering)
|
||||
*
|
||||
* @param input markdown input to parse
|
||||
* @return parsed via commonmark-java <code>org.commonmark.node.Node</code>
|
||||
* @see #render(Node)
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Node parse(@NonNull String input);
|
||||
|
||||
/**
|
||||
* Create Spanned markdown from parsed Node (via {@link #parse(String)} call).
|
||||
* <p>
|
||||
* Please note that returned Spanned has few limitations. For example, images, tables
|
||||
* and ordered lists require TextView to be properly displayed. This is why images and tables
|
||||
* most likely won\'t work in this case. Ordered lists might have mis-measurements. Whenever
|
||||
* possible use {@link #setMarkdown(TextView, String)} or {@link #setParsedMarkdown(TextView, Spanned)}
|
||||
* as these methods will additionally call specific {@link MarkwonPlugin} methods to <em>prepare</em>
|
||||
* proper display.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Spanned render(@NonNull Node node);
|
||||
|
||||
/**
|
||||
* This method will {@link #parse(String)} and {@link #render(Node)} supplied markdown. Returned
|
||||
* Spanned has the same limitations as from {@link #render(Node)} method.
|
||||
*
|
||||
* @param input markdown input
|
||||
* @see #parse(String)
|
||||
* @see #render(Node)
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Spanned toMarkdown(@NonNull String input);
|
||||
|
||||
public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown);
|
||||
|
||||
public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown);
|
||||
|
||||
/**
|
||||
* Requests information if certain plugin has been registered. Please note that this
|
||||
* method will check for super classes also, so if supplied with {@code markwon.hasPlugin(MarkwonPlugin.class)}
|
||||
* this method (if has at least one plugin) will return true. If for example a custom
|
||||
* (subclassed) version of a {@link CorePlugin} has been registered and given name
|
||||
* {@code CorePlugin2}, then both {@code markwon.hasPlugin(CorePlugin2.class)} and
|
||||
* {@code markwon.hasPlugin(CorePlugin.class)} will return true.
|
||||
*
|
||||
* @param plugin type to query
|
||||
* @return true if a plugin is used when configuring this {@link Markwon} instance
|
||||
*/
|
||||
public abstract boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> plugin);
|
||||
|
||||
@Nullable
|
||||
public abstract <P extends MarkwonPlugin> P getPlugin(@NonNull Class<P> type);
|
||||
|
||||
/**
|
||||
* Builder for {@link Markwon}.
|
||||
* <p>
|
||||
* Please note that the order in which plugins are supplied is important as this order will be
|
||||
* used through the whole usage of built Markwon instance
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface Builder {
|
||||
|
||||
/**
|
||||
* Specify bufferType when applying text to a TextView {@code textView.setText(CharSequence,BufferType)}.
|
||||
* By default `BufferType.SPANNABLE` is used
|
||||
*
|
||||
* @param bufferType BufferType
|
||||
*/
|
||||
@NonNull
|
||||
Builder bufferType(@NonNull TextView.BufferType bufferType);
|
||||
|
||||
@NonNull
|
||||
Builder usePlugin(@NonNull MarkwonPlugin plugin);
|
||||
|
||||
@NonNull
|
||||
Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins);
|
||||
|
||||
@NonNull
|
||||
Markwon build();
|
||||
}
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.core.CorePlugin;
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.priority.PriorityProcessor;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final List<MarkwonPlugin> plugins = new ArrayList<>(3);
|
||||
|
||||
private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE;
|
||||
|
||||
private PriorityProcessor priorityProcessor;
|
||||
|
||||
MarkwonBuilderImpl(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Markwon.Builder bufferType(@NonNull TextView.BufferType bufferType) {
|
||||
this.bufferType = bufferType;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Markwon.Builder usePlugin(@NonNull MarkwonPlugin plugin) {
|
||||
plugins.add(plugin);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Markwon.Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins) {
|
||||
|
||||
final Iterator<? extends MarkwonPlugin> iterator = plugins.iterator();
|
||||
|
||||
MarkwonPlugin plugin;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
plugin = iterator.next();
|
||||
if (plugin == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.plugins.add(plugin);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
@NonNull
|
||||
public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) {
|
||||
this.priorityProcessor = priorityProcessor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Markwon build() {
|
||||
|
||||
if (plugins.isEmpty()) {
|
||||
throw new IllegalStateException("No plugins were added to this builder. Use #usePlugin " +
|
||||
"method to add them");
|
||||
}
|
||||
|
||||
// this class will sort plugins to match a priority/dependency graph that we have
|
||||
PriorityProcessor priorityProcessor = this.priorityProcessor;
|
||||
if (priorityProcessor == null) {
|
||||
// strictly speaking we do not need updating this field
|
||||
// as we are not building this class to be reused between multiple `build` calls
|
||||
priorityProcessor = this.priorityProcessor = PriorityProcessor.create();
|
||||
}
|
||||
|
||||
// please note that this method must not modify supplied collection
|
||||
// if nothing should be done -> the same collection can be returned
|
||||
final List<MarkwonPlugin> plugins = preparePlugins(priorityProcessor, this.plugins);
|
||||
|
||||
final Parser.Builder parserBuilder = new Parser.Builder();
|
||||
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
|
||||
final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
|
||||
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
|
||||
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
|
||||
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
|
||||
final MarkwonHtmlRenderer.Builder htmlRendererBuilder = MarkwonHtmlRenderer.builder();
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.configureParser(parserBuilder);
|
||||
plugin.configureTheme(themeBuilder);
|
||||
plugin.configureImages(asyncDrawableLoaderBuilder);
|
||||
plugin.configureConfiguration(configurationBuilder);
|
||||
plugin.configureVisitor(visitorBuilder);
|
||||
plugin.configureSpansFactory(spanFactoryBuilder);
|
||||
plugin.configureHtmlRenderer(htmlRendererBuilder);
|
||||
}
|
||||
|
||||
final MarkwonConfiguration configuration = configurationBuilder.build(
|
||||
themeBuilder.build(),
|
||||
asyncDrawableLoaderBuilder.build(),
|
||||
htmlRendererBuilder.build(),
|
||||
spanFactoryBuilder.build());
|
||||
|
||||
final RenderProps renderProps = new RenderPropsImpl();
|
||||
|
||||
return new MarkwonImpl(
|
||||
bufferType,
|
||||
parserBuilder.build(),
|
||||
visitorBuilder.build(configuration, renderProps),
|
||||
Collections.unmodifiableList(plugins)
|
||||
);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
static List<MarkwonPlugin> preparePlugins(
|
||||
@NonNull PriorityProcessor priorityProcessor,
|
||||
@NonNull List<MarkwonPlugin> plugins) {
|
||||
|
||||
// with this method we will ensure that CorePlugin is added IF and ONLY IF
|
||||
// there are plugins that depend on it. If CorePlugin is added, or there are
|
||||
// no plugins that require it, CorePlugin won't be added
|
||||
final List<MarkwonPlugin> out = ensureImplicitCoreIfHasDependents(plugins);
|
||||
|
||||
return priorityProcessor.process(out);
|
||||
}
|
||||
|
||||
// this method will _implicitly_ add CorePlugin if there is at least one plugin
|
||||
// that depends on CorePlugin
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
static List<MarkwonPlugin> ensureImplicitCoreIfHasDependents(@NonNull List<MarkwonPlugin> plugins) {
|
||||
// loop over plugins -> if CorePlugin is found -> break;
|
||||
// iterate over all plugins and check if CorePlugin is requested
|
||||
|
||||
boolean hasCore = false;
|
||||
boolean hasCoreDependents = false;
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
|
||||
// here we do not check for exact match (a user could've subclasses CorePlugin
|
||||
// and supplied it. In this case we DO NOT implicitly add CorePlugin
|
||||
//
|
||||
// if core is present already we do not need to iterate anymore -> as nothing
|
||||
// will be changed (and we actually do not care if there are any dependents of Core
|
||||
// as it's present anyway)
|
||||
if (CorePlugin.class.isAssignableFrom(plugin.getClass())) {
|
||||
hasCore = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// if plugin has CorePlugin in dependencies -> mark for addition
|
||||
if (!hasCoreDependents) {
|
||||
// here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass
|
||||
// or something -> ignore)
|
||||
if (plugin.priority().after().contains(CorePlugin.class)) {
|
||||
hasCoreDependents = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// important thing here is to check if corePlugin is added
|
||||
// add it _only_ if it's not present
|
||||
if (hasCoreDependents && !hasCore) {
|
||||
final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1);
|
||||
// add default instance of CorePlugin
|
||||
out.add(CorePlugin.create());
|
||||
out.addAll(plugins);
|
||||
return out;
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.core.spans.LinkSpan;
|
||||
import ru.noties.markwon.html.MarkwonHtmlParser;
|
||||
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.image.ImageSizeResolver;
|
||||
import ru.noties.markwon.image.ImageSizeResolverDef;
|
||||
import ru.noties.markwon.syntax.SyntaxHighlight;
|
||||
import ru.noties.markwon.syntax.SyntaxHighlightNoOp;
|
||||
import ru.noties.markwon.urlprocessor.UrlProcessor;
|
||||
import ru.noties.markwon.urlprocessor.UrlProcessorNoOp;
|
||||
|
||||
/**
|
||||
* since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration`
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class MarkwonConfiguration {
|
||||
|
||||
@NonNull
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
private final MarkwonTheme theme;
|
||||
private final AsyncDrawableLoader asyncDrawableLoader;
|
||||
private final SyntaxHighlight syntaxHighlight;
|
||||
private final LinkSpan.Resolver linkResolver;
|
||||
private final UrlProcessor urlProcessor;
|
||||
private final ImageSizeResolver imageSizeResolver;
|
||||
private final MarkwonHtmlParser htmlParser;
|
||||
private final MarkwonHtmlRenderer htmlRenderer;
|
||||
|
||||
// @since 3.0.0
|
||||
private final MarkwonSpansFactory spansFactory;
|
||||
|
||||
private MarkwonConfiguration(@NonNull Builder builder) {
|
||||
this.theme = builder.theme;
|
||||
this.asyncDrawableLoader = builder.asyncDrawableLoader;
|
||||
this.syntaxHighlight = builder.syntaxHighlight;
|
||||
this.linkResolver = builder.linkResolver;
|
||||
this.urlProcessor = builder.urlProcessor;
|
||||
this.imageSizeResolver = builder.imageSizeResolver;
|
||||
this.spansFactory = builder.spansFactory;
|
||||
this.htmlParser = builder.htmlParser;
|
||||
this.htmlRenderer = builder.htmlRenderer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonTheme theme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoader asyncDrawableLoader() {
|
||||
return asyncDrawableLoader;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public SyntaxHighlight syntaxHighlight() {
|
||||
return syntaxHighlight;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public LinkSpan.Resolver linkResolver() {
|
||||
return linkResolver;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public UrlProcessor urlProcessor() {
|
||||
return urlProcessor;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ImageSizeResolver imageSizeResolver() {
|
||||
return imageSizeResolver;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonHtmlParser htmlParser() {
|
||||
return htmlParser;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonHtmlRenderer htmlRenderer() {
|
||||
return htmlRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public MarkwonSpansFactory spansFactory() {
|
||||
return spansFactory;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "UnusedReturnValue"})
|
||||
public static class Builder {
|
||||
|
||||
private MarkwonTheme theme;
|
||||
private AsyncDrawableLoader asyncDrawableLoader;
|
||||
private SyntaxHighlight syntaxHighlight;
|
||||
private LinkSpan.Resolver linkResolver;
|
||||
private UrlProcessor urlProcessor;
|
||||
private ImageSizeResolver imageSizeResolver;
|
||||
private MarkwonHtmlParser htmlParser;
|
||||
private MarkwonHtmlRenderer htmlRenderer;
|
||||
private MarkwonSpansFactory spansFactory;
|
||||
|
||||
Builder() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
|
||||
this.syntaxHighlight = syntaxHighlight;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder linkResolver(@NonNull LinkSpan.Resolver linkResolver) {
|
||||
this.linkResolver = linkResolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder urlProcessor(@NonNull UrlProcessor urlProcessor) {
|
||||
this.urlProcessor = urlProcessor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder htmlParser(@NonNull MarkwonHtmlParser htmlParser) {
|
||||
this.htmlParser = htmlParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@NonNull
|
||||
public Builder imageSizeResolver(@NonNull ImageSizeResolver imageSizeResolver) {
|
||||
this.imageSizeResolver = imageSizeResolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonConfiguration build(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull AsyncDrawableLoader asyncDrawableLoader,
|
||||
@NonNull MarkwonHtmlRenderer htmlRenderer,
|
||||
@NonNull MarkwonSpansFactory spansFactory) {
|
||||
|
||||
this.theme = theme;
|
||||
this.asyncDrawableLoader = asyncDrawableLoader;
|
||||
this.htmlRenderer = htmlRenderer;
|
||||
this.spansFactory = spansFactory;
|
||||
|
||||
if (syntaxHighlight == null) {
|
||||
syntaxHighlight = new SyntaxHighlightNoOp();
|
||||
}
|
||||
|
||||
if (linkResolver == null) {
|
||||
linkResolver = new LinkResolverDef();
|
||||
}
|
||||
|
||||
if (urlProcessor == null) {
|
||||
urlProcessor = new UrlProcessorNoOp();
|
||||
}
|
||||
|
||||
if (imageSizeResolver == null) {
|
||||
imageSizeResolver = new ImageSizeResolverDef();
|
||||
}
|
||||
|
||||
if (htmlParser == null) {
|
||||
htmlParser = MarkwonHtmlParser.noOp();
|
||||
}
|
||||
|
||||
return new MarkwonConfiguration(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
110
markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java
Normal file
@ -0,0 +1,110 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class MarkwonImpl extends Markwon {
|
||||
|
||||
private final TextView.BufferType bufferType;
|
||||
private final Parser parser;
|
||||
private final MarkwonVisitor visitor;
|
||||
private final List<MarkwonPlugin> plugins;
|
||||
|
||||
MarkwonImpl(
|
||||
@NonNull TextView.BufferType bufferType,
|
||||
@NonNull Parser parser,
|
||||
@NonNull MarkwonVisitor visitor,
|
||||
@NonNull List<MarkwonPlugin> plugins) {
|
||||
this.bufferType = bufferType;
|
||||
this.parser = parser;
|
||||
this.visitor = visitor;
|
||||
this.plugins = plugins;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Node parse(@NonNull String input) {
|
||||
|
||||
// make sure that all plugins are called `processMarkdown` before parsing
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
input = plugin.processMarkdown(input);
|
||||
}
|
||||
|
||||
return parser.parse(input);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Spanned render(@NonNull Node node) {
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.beforeRender(node);
|
||||
}
|
||||
|
||||
node.accept(visitor);
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.afterRender(node, visitor);
|
||||
}
|
||||
|
||||
final Spanned spanned = visitor.builder().spannableStringBuilder();
|
||||
|
||||
// clear render props and builder after rendering
|
||||
visitor.clear();
|
||||
|
||||
return spanned;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Spanned toMarkdown(@NonNull String input) {
|
||||
return render(parse(input));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkdown(@NonNull TextView textView, @NonNull String markdown) {
|
||||
setParsedMarkdown(textView, toMarkdown(markdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.beforeSetText(textView, markdown);
|
||||
}
|
||||
|
||||
textView.setText(markdown, bufferType);
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.afterSetText(textView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> type) {
|
||||
return getPlugin(type) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <P extends MarkwonPlugin> P getPlugin(@NonNull Class<P> type) {
|
||||
MarkwonPlugin out = null;
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
if (type.isAssignableFrom(plugin.getClass())) {
|
||||
out = plugin;
|
||||
}
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (P) out;
|
||||
}
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public abstract class MarkwonNodeRenderer {
|
||||
|
||||
public interface ViewProvider<N extends Node> {
|
||||
|
||||
/**
|
||||
* Please note that you should not attach created View to specified group. It will be done
|
||||
* automatically.
|
||||
*/
|
||||
@NonNull
|
||||
View provide(
|
||||
@NonNull LayoutInflater inflater,
|
||||
@NonNull ViewGroup group,
|
||||
@NonNull Markwon markwon,
|
||||
@NonNull N n);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull ViewProvider<Node> defaultViewProvider) {
|
||||
return new Builder(defaultViewProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param defaultViewProviderLayoutResId layout resource id to be used in default view provider
|
||||
* @param defaultViewProviderTextViewId id of a TextView in specified layout
|
||||
* @return Builder
|
||||
* @see SimpleTextViewProvider
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder(
|
||||
@LayoutRes int defaultViewProviderLayoutResId,
|
||||
@IdRes int defaultViewProviderTextViewId) {
|
||||
return new Builder(new SimpleTextViewProvider(
|
||||
defaultViewProviderLayoutResId,
|
||||
defaultViewProviderTextViewId));
|
||||
}
|
||||
|
||||
public abstract void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull String markdown);
|
||||
|
||||
public abstract void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull Node root);
|
||||
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final ViewProvider<Node> defaultViewProvider;
|
||||
|
||||
private MarkwonReducer reducer;
|
||||
private Map<Class<? extends Node>, ViewProvider<Node>> viewProviders;
|
||||
private LayoutInflater inflater;
|
||||
|
||||
public Builder(@NonNull ViewProvider<Node> defaultViewProvider) {
|
||||
this.defaultViewProvider = defaultViewProvider;
|
||||
this.viewProviders = new HashMap<>(3);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder reducer(@NonNull MarkwonReducer reducer) {
|
||||
this.reducer = reducer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public <N extends Node> Builder viewProvider(
|
||||
@NonNull Class<N> type,
|
||||
@NonNull ViewProvider<? super N> viewProvider) {
|
||||
//noinspection unchecked
|
||||
viewProviders.put(type, (ViewProvider<Node>) viewProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder inflater(@NonNull LayoutInflater inflater) {
|
||||
this.inflater = inflater;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonNodeRenderer build() {
|
||||
if (reducer == null) {
|
||||
reducer = MarkwonReducer.directChildren();
|
||||
}
|
||||
return new Impl(this);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SimpleTextViewProvider implements ViewProvider<Node> {
|
||||
|
||||
private final int layoutResId;
|
||||
private final int textViewId;
|
||||
|
||||
public SimpleTextViewProvider(@LayoutRes int layoutResId, @IdRes int textViewId) {
|
||||
this.layoutResId = layoutResId;
|
||||
this.textViewId = textViewId;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View provide(
|
||||
@NonNull LayoutInflater inflater,
|
||||
@NonNull ViewGroup group,
|
||||
@NonNull Markwon markwon,
|
||||
@NonNull Node node) {
|
||||
final View view = inflater.inflate(layoutResId, group, false);
|
||||
final TextView textView = view.findViewById(textViewId);
|
||||
markwon.setParsedMarkdown(textView, markwon.render(node));
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
static class Impl extends MarkwonNodeRenderer {
|
||||
|
||||
private final MarkwonReducer reducer;
|
||||
private final Map<Class<? extends Node>, ViewProvider<Node>> viewProviders;
|
||||
private final ViewProvider<Node> defaultViewProvider;
|
||||
|
||||
private LayoutInflater inflater;
|
||||
|
||||
Impl(@NonNull Builder builder) {
|
||||
this.reducer = builder.reducer;
|
||||
this.viewProviders = builder.viewProviders;
|
||||
this.defaultViewProvider = builder.defaultViewProvider;
|
||||
this.inflater = builder.inflater;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull String markdown) {
|
||||
render(group, markwon, markwon.parse(markdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull Node root) {
|
||||
|
||||
final LayoutInflater inflater = ensureLayoutInflater(group.getContext());
|
||||
|
||||
ViewProvider<Node> viewProvider;
|
||||
|
||||
for (Node node : reducer.reduce(root)) {
|
||||
viewProvider = viewProvider(node);
|
||||
group.addView(viewProvider.provide(inflater, group, markwon, node));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private LayoutInflater ensureLayoutInflater(@NonNull Context context) {
|
||||
LayoutInflater inflater = this.inflater;
|
||||
if (inflater == null) {
|
||||
inflater = this.inflater = LayoutInflater.from(context);
|
||||
}
|
||||
return inflater;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ViewProvider<Node> viewProvider(@NonNull Node node) {
|
||||
|
||||
// check for specific node view provider
|
||||
final ViewProvider<Node> provider = viewProviders.get(node.getClass());
|
||||
if (provider != null) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
// if it's not present, then we can return a default one
|
||||
return defaultViewProvider;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
143
markwon-core/src/main/java/ru/noties/markwon/MarkwonPlugin.java
Normal file
@ -0,0 +1,143 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.image.MediaDecoder;
|
||||
import ru.noties.markwon.image.SchemeHandler;
|
||||
import ru.noties.markwon.priority.Priority;
|
||||
|
||||
/**
|
||||
* Class represents a plugin (extension) to Markwon to configure how parsing and rendering
|
||||
* of markdown is carried on.
|
||||
*
|
||||
* @see AbstractMarkwonPlugin
|
||||
* @see ru.noties.markwon.core.CorePlugin
|
||||
* @see ru.noties.markwon.image.ImagesPlugin
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface MarkwonPlugin {
|
||||
|
||||
/**
|
||||
* Method to configure <code>org.commonmark.parser.Parser</code> (for example register custom
|
||||
* extension, etc).
|
||||
*/
|
||||
void configureParser(@NonNull Parser.Builder builder);
|
||||
|
||||
/**
|
||||
* Modify {@link MarkwonTheme} that is used for rendering of markdown.
|
||||
*
|
||||
* @see MarkwonTheme
|
||||
* @see MarkwonTheme.Builder
|
||||
*/
|
||||
void configureTheme(@NonNull MarkwonTheme.Builder builder);
|
||||
|
||||
/**
|
||||
* Configure image loading functionality. For example add new content-types
|
||||
* {@link AsyncDrawableLoader.Builder#addMediaDecoder(String, MediaDecoder)}, a transport
|
||||
* layer (network, file, etc) {@link AsyncDrawableLoader.Builder#addSchemeHandler(String, SchemeHandler)}
|
||||
* or modify existing properties.
|
||||
*
|
||||
* @see AsyncDrawableLoader
|
||||
* @see AsyncDrawableLoader.Builder
|
||||
*/
|
||||
void configureImages(@NonNull AsyncDrawableLoader.Builder builder);
|
||||
|
||||
/**
|
||||
* Configure {@link MarkwonConfiguration}
|
||||
*
|
||||
* @see MarkwonConfiguration
|
||||
* @see MarkwonConfiguration.Builder
|
||||
*/
|
||||
void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder);
|
||||
|
||||
/**
|
||||
* Configure {@link MarkwonVisitor} to accept new node types or override already registered nodes.
|
||||
*
|
||||
* @see MarkwonVisitor
|
||||
* @see MarkwonVisitor.Builder
|
||||
*/
|
||||
void configureVisitor(@NonNull MarkwonVisitor.Builder builder);
|
||||
|
||||
/**
|
||||
* Configure {@link MarkwonSpansFactory} to change what spans are used for certain node types.
|
||||
*
|
||||
* @see MarkwonSpansFactory
|
||||
* @see MarkwonSpansFactory.Builder
|
||||
*/
|
||||
void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder);
|
||||
|
||||
/**
|
||||
* Configure {@link MarkwonHtmlRenderer} to add or remove HTML {@link ru.noties.markwon.html.TagHandler}s
|
||||
*
|
||||
* @see MarkwonHtmlRenderer
|
||||
* @see MarkwonHtmlRenderer.Builder
|
||||
*/
|
||||
void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder);
|
||||
|
||||
@NonNull
|
||||
Priority priority();
|
||||
|
||||
/**
|
||||
* Process input markdown and return new string to be used in parsing stage further.
|
||||
* Can be described as <code>pre-processing</code> of markdown String.
|
||||
*
|
||||
* @param markdown String to process
|
||||
* @return processed markdown String
|
||||
*/
|
||||
@NonNull
|
||||
String processMarkdown(@NonNull String markdown);
|
||||
|
||||
/**
|
||||
* This method will be called <strong>before</strong> rendering will occur thus making possible
|
||||
* to <code>post-process</code> parsed node (make changes for example).
|
||||
*
|
||||
* @param node root parsed org.commonmark.node.Node
|
||||
*/
|
||||
void beforeRender(@NonNull Node node);
|
||||
|
||||
/**
|
||||
* This method will be called <strong>after</strong> rendering (but before applying markdown to a
|
||||
* TextView, if such action will happen). It can be used to clean some
|
||||
* internal state, or trigger certain action. Please note that modifying <code>node</code> won\'t
|
||||
* have any effect as it has been already <i>visited</i> at this stage.
|
||||
*
|
||||
* @param node root parsed org.commonmark.node.Node
|
||||
* @param visitor {@link MarkwonVisitor} instance used to render markdown
|
||||
*/
|
||||
void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor);
|
||||
|
||||
/**
|
||||
* This method will be called <strong>before</strong> calling <code>TextView#setText</code>.
|
||||
* <p>
|
||||
* It can be useful to prepare a TextView for markdown. For example {@link ru.noties.markwon.image.ImagesPlugin}
|
||||
* uses this method to unregister previously registered {@link ru.noties.markwon.image.AsyncDrawableSpan}
|
||||
* (if there are such spans in this TextView at this point). Or {@link ru.noties.markwon.core.CorePlugin}
|
||||
* which measures ordered list numbers
|
||||
*
|
||||
* @param textView TextView to which <code>markdown</code> will be applied
|
||||
* @param markdown Parsed markdown
|
||||
*/
|
||||
void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown);
|
||||
|
||||
/**
|
||||
* This method will be called <strong>after</strong> markdown was applied.
|
||||
* <p>
|
||||
* It can be useful to trigger certain action on spans/textView. For example {@link ru.noties.markwon.image.ImagesPlugin}
|
||||
* uses this method to register {@link ru.noties.markwon.image.AsyncDrawableSpan} and start
|
||||
* asynchronously loading images.
|
||||
* <p>
|
||||
* Unlike {@link #beforeSetText(TextView, Spanned)} this method does not receive parsed markdown
|
||||
* as at this point spans must be queried by calling <code>TextView#getText#getSpans</code>.
|
||||
*
|
||||
* @param textView TextView to which markdown was applied
|
||||
*/
|
||||
void afterSetText(@NonNull TextView textView);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public abstract class MarkwonReducer {
|
||||
|
||||
/**
|
||||
* @return direct children of supplied Node. In the most usual case
|
||||
* will return all BlockNodes of a Document
|
||||
*/
|
||||
@NonNull
|
||||
public static MarkwonReducer directChildren() {
|
||||
return new DirectChildren();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public abstract List<Node> reduce(@NonNull Node node);
|
||||
|
||||
|
||||
static class DirectChildren extends MarkwonReducer {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Node> reduce(@NonNull Node root) {
|
||||
|
||||
final List<Node> list;
|
||||
|
||||
// we will extract all blocks that are direct children of Document
|
||||
Node node = root.getFirstChild();
|
||||
|
||||
// please note, that if there are no children -> we will return a list with
|
||||
// single element (which was supplied)
|
||||
if (node == null) {
|
||||
list = Collections.singletonList(root);
|
||||
} else {
|
||||
|
||||
list = new ArrayList<>();
|
||||
|
||||
Node temp;
|
||||
|
||||
while (node != null) {
|
||||
list.add(node);
|
||||
temp = node.getNext();
|
||||
node.unlink();
|
||||
node = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
/**
|
||||
* Class that controls what spans are used for certain Nodes.
|
||||
*
|
||||
* @see SpanFactory
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface MarkwonSpansFactory {
|
||||
|
||||
/**
|
||||
* Returns registered {@link SpanFactory} or <code>null</code> if a factory for this node type
|
||||
* is not registered. There is {@link #require(Class)} method that will throw an exception
|
||||
* if required {@link SpanFactory} is not registered, thus making return type <code>non-null</code>
|
||||
*
|
||||
* @param node type of the node
|
||||
* @return registered {@link SpanFactory} or null if it\'s not registered
|
||||
* @see #require(Class)
|
||||
*/
|
||||
@Nullable
|
||||
<N extends Node> SpanFactory get(@NonNull Class<N> node);
|
||||
|
||||
@NonNull
|
||||
<N extends Node> SpanFactory require(@NonNull Class<N> node);
|
||||
|
||||
|
||||
interface Builder {
|
||||
|
||||
@NonNull
|
||||
<N extends Node> Builder setFactory(@NonNull Class<N> node, @Nullable SpanFactory factory);
|
||||
|
||||
/**
|
||||
* Can be useful when <em>enhancing</em> an already defined SpanFactory with another one.
|
||||
*/
|
||||
@Nullable
|
||||
<N extends Node> SpanFactory getFactory(@NonNull Class<N> node);
|
||||
|
||||
@NonNull
|
||||
MarkwonSpansFactory build();
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class MarkwonSpansFactoryImpl implements MarkwonSpansFactory {
|
||||
|
||||
private final Map<Class<? extends Node>, SpanFactory> factories;
|
||||
|
||||
MarkwonSpansFactoryImpl(@NonNull Map<Class<? extends Node>, SpanFactory> factories) {
|
||||
this.factories = factories;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <N extends Node> SpanFactory get(@NonNull Class<N> node) {
|
||||
return factories.get(node);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <N extends Node> SpanFactory require(@NonNull Class<N> node) {
|
||||
final SpanFactory f = get(node);
|
||||
if (f == null) {
|
||||
throw new NullPointerException(node.getName());
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
static class BuilderImpl implements Builder {
|
||||
|
||||
private final Map<Class<? extends Node>, SpanFactory> factories =
|
||||
new HashMap<>(3);
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <N extends Node> Builder setFactory(@NonNull Class<N> node, @Nullable SpanFactory factory) {
|
||||
if (factory == null) {
|
||||
factories.remove(node);
|
||||
} else {
|
||||
factories.put(node, factory);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <N extends Node> SpanFactory getFactory(@NonNull Class<N> node) {
|
||||
return factories.get(node);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkwonSpansFactory build() {
|
||||
return new MarkwonSpansFactoryImpl(Collections.unmodifiableMap(factories));
|
||||
}
|
||||
}
|
||||
}
|
136
markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java
Normal file
@ -0,0 +1,136 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.Visitor;
|
||||
|
||||
/**
|
||||
* Configurable visitor of parsed markdown. Allows visiting certain (registered) nodes without
|
||||
* need to create own instance of this class.
|
||||
*
|
||||
* @see Builder#on(Class, NodeVisitor)
|
||||
* @see MarkwonPlugin#configureVisitor(Builder)
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface MarkwonVisitor extends Visitor {
|
||||
|
||||
/**
|
||||
* @see Builder#on(Class, NodeVisitor)
|
||||
*/
|
||||
interface NodeVisitor<N extends Node> {
|
||||
void visit(@NonNull MarkwonVisitor visitor, @NonNull N n);
|
||||
}
|
||||
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* @param node to register
|
||||
* @param nodeVisitor {@link NodeVisitor} to be used or null to ignore previously registered
|
||||
* visitor for this node
|
||||
*/
|
||||
@NonNull
|
||||
<N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<? super N> nodeVisitor);
|
||||
|
||||
@NonNull
|
||||
MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
MarkwonConfiguration configuration();
|
||||
|
||||
@NonNull
|
||||
RenderProps renderProps();
|
||||
|
||||
@NonNull
|
||||
SpannableBuilder builder();
|
||||
|
||||
/**
|
||||
* Visits all children of supplied node.
|
||||
*
|
||||
* @param node to visit
|
||||
*/
|
||||
void visitChildren(@NonNull Node node);
|
||||
|
||||
/**
|
||||
* Executes a check if there is further content available.
|
||||
*
|
||||
* @param node to check
|
||||
* @return boolean indicating if there are more nodes after supplied one
|
||||
*/
|
||||
boolean hasNext(@NonNull Node node);
|
||||
|
||||
/**
|
||||
* This method <strong>ensures</strong> that further content will start at a new line. If current
|
||||
* last character is already a new line, then it won\'t do anything.
|
||||
*/
|
||||
void ensureNewLine();
|
||||
|
||||
/**
|
||||
* This method inserts a new line without any condition checking (unlike {@link #ensureNewLine()}).
|
||||
*/
|
||||
void forceNewLine();
|
||||
|
||||
/**
|
||||
* Helper method to call <code>builder().length()</code>
|
||||
*
|
||||
* @return current length of underlying {@link SpannableBuilder}
|
||||
*/
|
||||
int length();
|
||||
|
||||
/**
|
||||
* Clears state of visitor (both {@link RenderProps} and {@link SpannableBuilder} will be cleared
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* Sets <code>spans</code> to underlying {@link SpannableBuilder} from <em>start</em>
|
||||
* to <em>{@link SpannableBuilder#length()}</em>.
|
||||
*
|
||||
* @param start start position of spans
|
||||
* @param spans to apply
|
||||
*/
|
||||
void setSpans(int start, @Nullable Object spans);
|
||||
|
||||
/**
|
||||
* Helper method to obtain and apply spans for supplied Node. Internally queries {@link SpanFactory}
|
||||
* for the node (via {@link MarkwonSpansFactory#require(Class)} thus throwing an exception
|
||||
* if there is no {@link SpanFactory} registered for the node).
|
||||
*
|
||||
* @param node to retrieve {@link SpanFactory} for
|
||||
* @param start start position for further {@link #setSpans(int, Object)} call
|
||||
* @see #setSpansForNodeOptional(Node, int)
|
||||
*/
|
||||
<N extends Node> void setSpansForNode(@NonNull N node, int start);
|
||||
|
||||
/**
|
||||
* The same as {@link #setSpansForNode(Node, int)} but can be used in situations when there is
|
||||
* no access to a Node instance (for example in HTML rendering which doesn\'t have markdown Nodes).
|
||||
*
|
||||
* @see #setSpansForNode(Node, int)
|
||||
*/
|
||||
<N extends Node> void setSpansForNode(@NonNull Class<N> node, int start);
|
||||
|
||||
// does not throw if there is no SpanFactory registered for this node
|
||||
|
||||
/**
|
||||
* Helper method to apply spans from a {@link SpanFactory} <b>if</b> it\'s registered in
|
||||
* {@link MarkwonSpansFactory} instance. Otherwise ignores this call (no spans will be applied).
|
||||
* If there is a need to ensure that specified <code>node</code> has a {@link SpanFactory} registered,
|
||||
* then {@link #setSpansForNode(Node, int)} can be used. {@link #setSpansForNode(Node, int)} internally
|
||||
* uses {@link MarkwonSpansFactory#require(Class)}. This method uses {@link MarkwonSpansFactory#get(Class)}.
|
||||
*
|
||||
* @see #setSpansForNode(Node, int)
|
||||
*/
|
||||
<N extends Node> void setSpansForNodeOptional(@NonNull N node, int start);
|
||||
|
||||
/**
|
||||
* The same as {@link #setSpansForNodeOptional(Node, int)} but can be used in situations when
|
||||
* there is no access to a Node instance (for example in HTML rendering).
|
||||
*
|
||||
* @see #setSpansForNodeOptional(Node, int)
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
<N extends Node> void setSpansForNodeOptional(@NonNull Class<N> node, int start);
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.BlockQuote;
|
||||
import org.commonmark.node.BulletList;
|
||||
import org.commonmark.node.Code;
|
||||
import org.commonmark.node.CustomBlock;
|
||||
import org.commonmark.node.CustomNode;
|
||||
import org.commonmark.node.Document;
|
||||
import org.commonmark.node.Emphasis;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.node.HardLineBreak;
|
||||
import org.commonmark.node.Heading;
|
||||
import org.commonmark.node.HtmlBlock;
|
||||
import org.commonmark.node.HtmlInline;
|
||||
import org.commonmark.node.Image;
|
||||
import org.commonmark.node.IndentedCodeBlock;
|
||||
import org.commonmark.node.Link;
|
||||
import org.commonmark.node.ListItem;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.OrderedList;
|
||||
import org.commonmark.node.Paragraph;
|
||||
import org.commonmark.node.SoftLineBreak;
|
||||
import org.commonmark.node.StrongEmphasis;
|
||||
import org.commonmark.node.Text;
|
||||
import org.commonmark.node.ThematicBreak;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class MarkwonVisitorImpl implements MarkwonVisitor {
|
||||
|
||||
private final MarkwonConfiguration configuration;
|
||||
|
||||
private final RenderProps renderProps;
|
||||
|
||||
private final SpannableBuilder builder;
|
||||
|
||||
private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes;
|
||||
|
||||
MarkwonVisitorImpl(
|
||||
@NonNull MarkwonConfiguration configuration,
|
||||
@NonNull RenderProps renderProps,
|
||||
@NonNull SpannableBuilder builder,
|
||||
@NonNull Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes) {
|
||||
this.configuration = configuration;
|
||||
this.renderProps = renderProps;
|
||||
this.builder = builder;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BlockQuote blockQuote) {
|
||||
visit((Node) blockQuote);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BulletList bulletList) {
|
||||
visit((Node) bulletList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Code code) {
|
||||
visit((Node) code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Document document) {
|
||||
visit((Node) document);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Emphasis emphasis) {
|
||||
visit((Node) emphasis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(FencedCodeBlock fencedCodeBlock) {
|
||||
visit((Node) fencedCodeBlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HardLineBreak hardLineBreak) {
|
||||
visit((Node) hardLineBreak);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Heading heading) {
|
||||
visit((Node) heading);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ThematicBreak thematicBreak) {
|
||||
visit((Node) thematicBreak);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HtmlInline htmlInline) {
|
||||
visit((Node) htmlInline);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HtmlBlock htmlBlock) {
|
||||
visit((Node) htmlBlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Image image) {
|
||||
visit((Node) image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(IndentedCodeBlock indentedCodeBlock) {
|
||||
visit((Node) indentedCodeBlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Link link) {
|
||||
visit((Node) link);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ListItem listItem) {
|
||||
visit((Node) listItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(OrderedList orderedList) {
|
||||
visit((Node) orderedList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Paragraph paragraph) {
|
||||
visit((Node) paragraph);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(SoftLineBreak softLineBreak) {
|
||||
visit((Node) softLineBreak);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(StrongEmphasis strongEmphasis) {
|
||||
visit((Node) strongEmphasis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Text text) {
|
||||
visit((Node) text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CustomBlock customBlock) {
|
||||
visit((Node) customBlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CustomNode customNode) {
|
||||
visit((Node) customNode);
|
||||
}
|
||||
|
||||
private void visit(@NonNull Node node) {
|
||||
//noinspection unchecked
|
||||
final NodeVisitor<Node> nodeVisitor = (NodeVisitor<Node>) nodes.get(node.getClass());
|
||||
if (nodeVisitor != null) {
|
||||
nodeVisitor.visit(this, node);
|
||||
} else {
|
||||
visitChildren(node);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkwonConfiguration configuration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RenderProps renderProps() {
|
||||
return renderProps;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SpannableBuilder builder() {
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitChildren(@NonNull Node parent) {
|
||||
Node node = parent.getFirstChild();
|
||||
while (node != null) {
|
||||
// A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
|
||||
// node after visiting it. So get the next node before visiting.
|
||||
Node next = node.getNext();
|
||||
node.accept(this);
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext(@NonNull Node node) {
|
||||
return node.getNext() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureNewLine() {
|
||||
if (builder.length() > 0
|
||||
&& '\n' != builder.lastChar()) {
|
||||
builder.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceNewLine() {
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return builder.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSpans(int start, @Nullable Object spans) {
|
||||
SpannableBuilder.setSpans(builder, spans, start, builder.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
renderProps.clearAll();
|
||||
builder.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <N extends Node> void setSpansForNode(@NonNull N node, int start) {
|
||||
setSpansForNode(node.getClass(), start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <N extends Node> void setSpansForNode(@NonNull Class<N> node, int start) {
|
||||
setSpans(start, configuration.spansFactory().require(node).getSpans(configuration, renderProps));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <N extends Node> void setSpansForNodeOptional(@NonNull N node, int start) {
|
||||
setSpansForNodeOptional(node.getClass(), start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <N extends Node> void setSpansForNodeOptional(@NonNull Class<N> node, int start) {
|
||||
final SpanFactory factory = configuration.spansFactory().get(node);
|
||||
if (factory != null) {
|
||||
setSpans(start, factory.getSpans(configuration, renderProps));
|
||||
}
|
||||
}
|
||||
|
||||
static class BuilderImpl implements Builder {
|
||||
|
||||
private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes = new HashMap<>();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<? super N> nodeVisitor) {
|
||||
// we should allow `null` to exclude node from being visited (for example to disable
|
||||
// some functionality)
|
||||
if (nodeVisitor == null) {
|
||||
nodes.remove(node);
|
||||
} else {
|
||||
nodes.put(node, nodeVisitor);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) {
|
||||
return new MarkwonVisitorImpl(
|
||||
configuration,
|
||||
renderProps,
|
||||
new SpannableBuilder(),
|
||||
Collections.unmodifiableMap(nodes));
|
||||
}
|
||||
}
|
||||
}
|