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.
This commit is contained in:
Dimitry 2019-03-18 17:13:44 +03:00 committed by GitHub
commit d7558c8e65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
519 changed files with 16496 additions and 6289 deletions

View File

@ -10,7 +10,7 @@ android:
- tools
- build-tools-28.0.3
- android-27
- android-28
branches:
except:

View File

@ -2,11 +2,6 @@
# Markwon
[![markwon](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22)
[![markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon-image-loader.svg?label=markwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22)
[![markwon-syntax-highlight](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax-highlight.svg?label=markwon-syntax-highlight)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22)
[![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22)
[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](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
![stable](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=stable)
![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/ru.noties.markwon/core.svg?label=snapshot)
```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>`)

View File

@ -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 {

View File

@ -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 {

View File

@ -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" />

View File

@ -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);
}

View File

@ -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();
// }
}

View File

@ -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
);
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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/";

View File

@ -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);
}

View File

@ -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);

View 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);
}
}

View File

@ -1,4 +1,4 @@
package ru.noties.markwon;
package ru.noties.markwon.gif;
import android.graphics.Canvas;
import android.graphics.ColorFilter;

View File

@ -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();
}
}
}

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

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

View File

@ -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" />
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View 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

View 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

View File

@ -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"
]
@ -76,9 +83,6 @@ ext {
'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'
]

View 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 };

View 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>&nbsp;markwon_version =&nbsp;</span>
<span class="token string">'{{latestVersion}}'</span>
</div>
<br>
<div class="selected-artifact-script" v-for="artifact in selectedArtifacts">
<span>implementation&nbsp;</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>

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

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

View File

@ -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>
@ -25,14 +28,16 @@ var map = {
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": {
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/",

View File

@ -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;
}
}
}

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

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

View File

@ -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' }],
['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'));
}
}
}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -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;
}

View File

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

View File

@ -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/>
[![markwon](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20)
[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](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` &amp; `file` schemes
and 2 themes included: Light &amp; 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
View 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
View 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` &amp; `file` schemes
and 2 themes included: Light &amp; Dark. It can be downloaded from [releases](https://github.com/noties/Markwon/releases)
:::

View File

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

View File

@ -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)

View File

@ -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:

View File

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

View File

@ -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.

View File

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

View File

@ -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"
```

View 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: `![img](./art/image.JPG)` 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: `![img](./art/image.JPG)` 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.
:::

View 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.
:::

View 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.

View 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
View 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 ![alt](https://my.image/1.JPG)");
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 ![alt](https://my.image/1.JPG)");
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.
:::

View 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.
:::

View 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));
```

View 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

View 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
View 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 &amp; H2
<ThemeProperty name="headingBreakHeight" type="@Px int" defaults="Stroke width of context TextPaint" />
### Break color
The color of a brake under H1 &amp; 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" />

View 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();
}
}
});
}
```

View 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();
```

View 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();
}
});
}
})
```

View 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" />

View 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();
```

View 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 &amp; 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
View 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();
```

View 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
View 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
View File

@ -0,0 +1,34 @@
---
prev: false
next: /docs/v3/core/getting-started.md
---
# Installation
![stable](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=stable)
![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/ru.noties.markwon/core.svg?label=snapshot)
<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
:::

View 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

View 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>
```

View 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/)
:::

View 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
View File

@ -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",

View File

@ -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
View File

@ -0,0 +1,3 @@
# Commonmark Sandbox
<CommonmarkSandbox />

View File

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

View File

@ -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']
}
}

View 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

View File

@ -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) {
}
}

View File

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

View 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();
}
}

View File

@ -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;
}
}

View File

@ -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` -&gt; `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);
}
}
}

View 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;
}
}

View File

@ -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;
}
}
}

View 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);
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}
}

View 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);
}

View File

@ -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));
}
}
}

Some files were not shown because too many files have changed in this diff Show More