diff --git a/app/build.gradle b/app/build.gradle index 0869e5e5..9512e8d2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,8 @@ android { dependencies { implementation project(':markwon') - implementation project(':markwon-image-loader') + implementation project(':markwon-image-gif') + implementation project(':markwon-image-svg') implementation project(':markwon-syntax-highlight') deps.with { diff --git a/app/src/main/java/ru/noties/markwon/AppModule.java b/app/src/main/java/ru/noties/markwon/AppModule.java index 3e2f3967..32d3e931 100644 --- a/app/src/main/java/ru/noties/markwon/AppModule.java +++ b/app/src/main/java/ru/noties/markwon/AppModule.java @@ -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(); +// } } diff --git a/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java b/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java index 78d286af..ed16554f 100644 --- a/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java +++ b/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java @@ -6,9 +6,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import pl.droidsonroids.gif.GifDrawable; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; +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); diff --git a/app/src/main/java/ru/noties/markwon/GifAwarePlugin.java b/app/src/main/java/ru/noties/markwon/GifAwarePlugin.java new file mode 100644 index 00000000..bb2fafd4 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifAwarePlugin.java @@ -0,0 +1,35 @@ +package ru.noties.markwon; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.widget.TextView; + +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; + + public GifAwarePlugin(@NonNull Context context) { + this.context = context; + this.processor = GifProcessor.create(); + } + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + final GifPlaceholder gifPlaceholder = new GifPlaceholder( + context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), + 0x20000000 + ); + builder.factory(new GifAwareSpannableFactory(gifPlaceholder)); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + processor.process(textView); + } +} diff --git a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java index 7c387a1c..432a379c 100644 --- a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java +++ b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java @@ -3,9 +3,10 @@ package ru.noties.markwon; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawableSpan; import ru.noties.markwon.spans.MarkwonTheme; @@ -19,7 +20,7 @@ public class GifAwareSpannableFactory extends SpannableFactoryDef { @Nullable @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { return new AsyncDrawableSpan( theme, new GifAwareAsyncDrawable( diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index bd477bcd..84904934 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -29,9 +29,9 @@ public class MainActivity extends Activity { @Inject UriProcessor uriProcessor; - - @Inject - GifProcessor gifProcessor; +// +// @Inject +// GifProcessor gifProcessor; @Override protected void onCreate(final Bundle savedInstanceState) { @@ -66,26 +66,14 @@ public class MainActivity extends Activity { appBarRenderer.render(appBarState()); - if (true) { - final Markwon2 markwon2 = Markwon2.builder(this) - .use(new CorePlugin()) - .use(TaskListPlugin.create(new TaskListDrawable(0xffff0000, 0xffff0000, -1))) - .build(); - final CharSequence markdown = markwon2.toMarkdown("**hello _dear_** `code`\n\n- [ ] first\n- [x] second"); - textView.setText(markdown); - return; - } - markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { @Override 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 Markwon2 markwon2, CharSequence markdown) { - Markwon.setText(textView, markdown); - - gifProcessor.process(textView); + markwon2.setParsedMarkdown(textView, markdown); Views.setVisible(progress, false); } diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index f0eb38e5..053fde93 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -13,24 +13,23 @@ 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.MarkwonTheme; -import ru.noties.markwon.syntax.Prism4jSyntaxHighlight; +import ru.noties.markwon.core.CorePlugin; +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.prism4j.Prism4j; @ActivityScope public class MarkdownRenderer { interface MarkdownReadyListener { - void onMarkdownReady(CharSequence markdown); + void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown); } - @Inject - AsyncDrawable.Loader loader; - @Inject ExecutorService service; @@ -78,40 +77,39 @@ public class MarkdownRenderer { ? prism4jThemeDefault : prism4JThemeDarkula; - final int background = isLightTheme - ? prism4jTheme.background() - : 0x0Fffffff; +// 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 MarkwonConfiguration configuration = MarkwonConfiguration.builder(context) - .asyncDrawableLoader(loader) - .urlProcessor(urlProcessor) - .syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme)) - .theme(MarkwonTheme.builderWithDefaults(context) - .codeBackgroundColor(background) - .codeTextColor(prism4jTheme.textColor()) - .build()) - .factory(new GifAwareSpannableFactory(gifPlaceholder)) + final Markwon2 markwon2 = Markwon2.builder(context) + .use(CorePlugin.create()) + .use(ImagesPlugin.createWithAssets(context)) + .use(SvgPlugin.create(context.getResources())) + .use(GifPlugin.create(false)) + .use(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) + .use(GifAwarePlugin.create(context)) + .use(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 CharSequence text = markwon2.toMarkdown(markdown); final long end = SystemClock.uptimeMillis(); - Debug.i("toMarkdown rendered: %d ms", end - start); + Debug.i("markdown rendered: %d ms", end - start); if (!isCancelled()) { handler.post(new Runnable() { @Override public void run() { if (!isCancelled()) { - listener.onMarkdownReady(text); + listener.onMarkdownReady(markwon2, text); task = null; } } diff --git a/markwon-image-gif/build.gradle b/markwon-image-gif/build.gradle new file mode 100644 index 00000000..2eeb680e --- /dev/null +++ b/markwon-image-gif/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + + deps.with { + api it['android-gif'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-image-gif/src/main/AndroidManifest.xml b/markwon-image-gif/src/main/AndroidManifest.xml new file mode 100644 index 00000000..649a9a70 --- /dev/null +++ b/markwon-image-gif/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java new file mode 100644 index 00000000..7a6600b8 --- /dev/null +++ b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java @@ -0,0 +1,82 @@ +package ru.noties.markwon.image.gif; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import pl.droidsonroids.gif.GifDrawable; +import ru.noties.markwon.image.MediaDecoder; +import ru.noties.markwon.utils.DrawableUtils; + +/** + * @since 1.1.0 + */ +@SuppressWarnings("WeakerAccess") +public class GifMediaDecoder extends MediaDecoder { + + public static final String CONTENT_TYPE = "image/gif"; + + @NonNull + public static GifMediaDecoder create(boolean autoPlayGif) { + return new GifMediaDecoder(autoPlayGif); + } + + private final boolean autoPlayGif; + + protected GifMediaDecoder(boolean autoPlayGif) { + this.autoPlayGif = autoPlayGif; + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + Drawable out = null; + + final byte[] bytes = readBytes(inputStream); + if (bytes != null) { + try { + out = newGifDrawable(bytes); + DrawableUtils.intrinsicBounds(out); + + if (!autoPlayGif) { + ((GifDrawable) out).pause(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + return out; + } + + @NonNull + protected Drawable newGifDrawable(@NonNull byte[] bytes) throws IOException { + return new GifDrawable(bytes); + } + + @Nullable + protected static byte[] readBytes(@NonNull InputStream stream) { + + byte[] out = null; + + try { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final int length = 1024 * 8; + final byte[] buffer = new byte[length]; + int read; + while ((read = stream.read(buffer, 0, length)) != -1) { + outputStream.write(buffer, 0, read); + } + out = outputStream.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + + return out; + } +} diff --git a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java new file mode 100644 index 00000000..3ee0c7aa --- /dev/null +++ b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java @@ -0,0 +1,30 @@ +package ru.noties.markwon.image.gif; + +import android.support.annotation.NonNull; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.image.AsyncDrawableLoader; + +public class GifPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static GifPlugin create() { + return create(true); + } + + @NonNull + public static GifPlugin create(boolean autoPlay) { + return new GifPlugin(autoPlay); + } + + private final boolean autoPlay; + + public GifPlugin(boolean autoPlay) { + this.autoPlay = autoPlay; + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder.addMediaDecoder(GifMediaDecoder.CONTENT_TYPE, GifMediaDecoder.create(autoPlay)); + } +} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java index 432e8797..5f7a5f01 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java +++ b/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java @@ -20,7 +20,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import okhttp3.OkHttpClient; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; public class AsyncDrawableLoader implements AsyncDrawable.Loader { diff --git a/markwon-image-svg/build.gradle b/markwon-image-svg/build.gradle new file mode 100644 index 00000000..06243a28 --- /dev/null +++ b/markwon-image-svg/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + + deps.with { + api it['android-svg'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-image-svg/src/main/AndroidManifest.xml b/markwon-image-svg/src/main/AndroidManifest.xml new file mode 100644 index 00000000..10432a1d --- /dev/null +++ b/markwon-image-svg/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java new file mode 100644 index 00000000..5c8661d8 --- /dev/null +++ b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java @@ -0,0 +1,73 @@ +package ru.noties.markwon.image.svg; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.caverock.androidsvg.SVG; +import com.caverock.androidsvg.SVGParseException; + +import java.io.InputStream; + +import ru.noties.markwon.image.MediaDecoder; +import ru.noties.markwon.utils.DrawableUtils; + +/** + * @since 1.1.0 + */ +public class SvgMediaDecoder extends MediaDecoder { + + public static final String CONTENT_TYPE = "image/svg+xml"; + + @NonNull + public static SvgMediaDecoder create(@NonNull Resources resources) { + return new SvgMediaDecoder(resources); + } + + private final Resources resources; + + @SuppressWarnings("WeakerAccess") + SvgMediaDecoder(Resources resources) { + this.resources = resources; + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + final Drawable out; + + SVG svg = null; + try { + svg = SVG.getFromInputStream(inputStream); + } catch (SVGParseException e) { + e.printStackTrace(); + } + + if (svg == null) { + out = null; + } else { + + final float w = svg.getDocumentWidth(); + final float h = svg.getDocumentHeight(); + final float density = resources.getDisplayMetrics().density; + + final int width = (int) (w * density + .5F); + final int height = (int) (h * density + .5F); + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); + final Canvas canvas = new Canvas(bitmap); + canvas.scale(density, density); + svg.renderToCanvas(canvas); + + out = new BitmapDrawable(resources, bitmap); + DrawableUtils.intrinsicBounds(out); + } + + return out; + } +} diff --git a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java new file mode 100644 index 00000000..d2396741 --- /dev/null +++ b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java @@ -0,0 +1,26 @@ +package ru.noties.markwon.image.svg; + +import android.content.res.Resources; +import android.support.annotation.NonNull; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.image.AsyncDrawableLoader; + +public class SvgPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static SvgPlugin create(@NonNull Resources resources) { + return new SvgPlugin(resources); + } + + private final Resources resources; + + public SvgPlugin(@NonNull Resources resources) { + this.resources = resources; + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder.addMediaDecoder(SvgMediaDecoder.CONTENT_TYPE, SvgMediaDecoder.create(resources)); + } +} diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java index 7d5c90b4..9a941951 100644 --- a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java @@ -1,5 +1,6 @@ package ru.noties.markwon.syntax; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; @@ -12,12 +13,23 @@ public class Prism4jThemeDarkula extends Prism4jThemeBase { @NonNull public static Prism4jThemeDarkula create() { - return new Prism4jThemeDarkula(); + return new Prism4jThemeDarkula(0xFF2d2d2d); + } + + @NonNull + public static Prism4jThemeDarkula create(@ColorInt int background) { + return new Prism4jThemeDarkula(background); + } + + private final int background; + + public Prism4jThemeDarkula(@ColorInt int background) { + this.background = background; } @Override public int background() { - return 0xFF2d2d2d; + return background; } @Override diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java new file mode 100644 index 00000000..08bf2a73 --- /dev/null +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java @@ -0,0 +1,52 @@ +package ru.noties.markwon.syntax; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.prism4j.Prism4j; + +public class SyntaxHighlightPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static SyntaxHighlightPlugin create( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme) { + return create(prism4j, theme, null); + } + + @NonNull + public static SyntaxHighlightPlugin create( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme, + @Nullable String fallbackLanguage) { + return new SyntaxHighlightPlugin(prism4j, theme, fallbackLanguage); + } + + private final Prism4j prism4j; + private final Prism4jTheme theme; + private final String fallbackLanguage; + + public SyntaxHighlightPlugin( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme, + @Nullable String fallbackLanguage) { + this.prism4j = prism4j; + this.theme = theme; + this.fallbackLanguage = fallbackLanguage; + } + + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder + .codeTextColor(theme.textColor()) + .codeBackgroundColor(theme.background()); + } + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, theme, fallbackLanguage)); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index d0187fac..b1671899 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -5,6 +5,7 @@ import android.widget.TextView; import org.commonmark.parser.Parser; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.spans.MarkwonTheme; public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { @@ -18,6 +19,11 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { } + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + + } + @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { @@ -40,7 +46,7 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { } @Override - public void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + public void afterSetText(@NonNull TextView textView) { } } diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index 5728f21a..022c3ca8 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -14,8 +14,11 @@ import org.commonmark.parser.Parser; import java.util.Arrays; +import ru.noties.markwon.image.AsyncDrawable; +//import ru.noties.markwon.image.DrawablesScheduler; import ru.noties.markwon.renderer.SpannableRenderer; import ru.noties.markwon.spans.OrderedListItemSpan; +import ru.noties.markwon.table.TableRowSpan; import ru.noties.markwon.tasklist.TaskListExtension; @SuppressWarnings({"WeakerAccess", "unused"}) @@ -148,7 +151,7 @@ public abstract class Markwon { } /** - * This method adds support for {@link ru.noties.markwon.spans.AsyncDrawable} to be used. As + * This method adds support for {@link AsyncDrawable} to be used. As * textView seems not to support drawables that change bounds (and gives no means * to update the layout), we create own {@link android.graphics.drawable.Drawable.Callback} * and apply it. So, textView can display drawables, that are: async (loading from disk, network); @@ -157,14 +160,14 @@ public abstract class Markwon { * in order to avoid keeping drawables in memory after they have been removed from layout * * @param view a {@link TextView} - * @see ru.noties.markwon.spans.AsyncDrawable + * @see AsyncDrawable * @see ru.noties.markwon.spans.AsyncDrawableSpan * @see DrawablesScheduler#schedule(TextView) * @see DrawablesScheduler#unschedule(TextView) * @since 1.0.0 */ public static void scheduleDrawables(@NonNull TextView view) { - DrawablesScheduler.schedule(view); +// DrawablesScheduler.schedule(view); } /** @@ -175,7 +178,7 @@ public abstract class Markwon { * @since 1.0.0 */ public static void unscheduleDrawables(@NonNull TextView view) { - DrawablesScheduler.unschedule(view); +// DrawablesScheduler.unschedule(view); } /** @@ -185,28 +188,28 @@ public abstract class Markwon { * to return `size` (width) of our replacement, but we are not provided * with the total one (canvas width). In order to correctly calculate height of our * table cell text, we must have available width first. This method gives - * ability for {@link ru.noties.markwon.spans.TableRowSpan} to invalidate + * ability for {@link TableRowSpan} to invalidate * `view` when it encounters such a situation (when available width is not known or have changed). * Precede this call with {@link #unscheduleTableRows(TextView)} in order to - * de-reference previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s + * de-reference previously scheduled {@link TableRowSpan}'s * * @param view a {@link TextView} * @see #unscheduleTableRows(TextView) * @since 1.0.0 */ public static void scheduleTableRows(@NonNull TextView view) { - TableRowsScheduler.schedule(view); +// TableRowsScheduler.schedule(view); } /** - * De-references previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s + * De-references previously scheduled {@link TableRowSpan}'s * * @param view a {@link TextView} * @see #scheduleTableRows(TextView) * @since 1.0.0 */ public static void unscheduleTableRows(@NonNull TextView view) { - TableRowsScheduler.unschedule(view); +// TableRowsScheduler.unschedule(view); } private Markwon() { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 841c70d4..5bbe9fa5 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.spans.MarkwonTheme; class MarkwonBuilderImpl implements Markwon2.Builder { @@ -34,19 +35,25 @@ class MarkwonBuilderImpl implements Markwon2.Builder { 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(context); final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); for (MarkwonPlugin plugin : plugins) { plugin.configureParser(parserBuilder); plugin.configureTheme(themeBuilder); + plugin.configureImages(asyncDrawableLoaderBuilder); plugin.configureConfiguration(configurationBuilder); plugin.configureVisitor(visitorBuilder); } + final MarkwonConfiguration configuration = configurationBuilder.build( + themeBuilder.build(), + asyncDrawableLoaderBuilder.build()); + return new MarkwonImpl( parserBuilder.build(), - visitorBuilder.build(configurationBuilder.build(themeBuilder.build())), + visitorBuilder.build(configuration), Collections.unmodifiableList(plugins) ); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 82d48f4f..1f6e2216 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -4,10 +4,11 @@ import android.content.Context; import android.support.annotation.NonNull; import ru.noties.markwon.html.api.MarkwonHtmlParser; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.AsyncDrawableLoaderNoOp; import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolverDef; import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; -import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; @@ -20,7 +21,7 @@ public class MarkwonConfiguration { // creates default configuration @NonNull public static MarkwonConfiguration create(@NonNull Context context) { - return new Builder(context).build(MarkwonTheme.create(context)); + return new Builder(context).build(MarkwonTheme.create(context), new AsyncDrawableLoaderNoOp()); } @NonNull @@ -30,7 +31,7 @@ public class MarkwonConfiguration { private final MarkwonTheme theme; - private final AsyncDrawable.Loader asyncDrawableLoader; + private final AsyncDrawableLoader asyncDrawableLoader; private final SyntaxHighlight syntaxHighlight; private final LinkSpan.Resolver linkResolver; private final UrlProcessor urlProcessor; @@ -69,7 +70,7 @@ public class MarkwonConfiguration { } @NonNull - public AsyncDrawable.Loader asyncDrawableLoader() { + public AsyncDrawableLoader asyncDrawableLoader() { return asyncDrawableLoader; } @@ -136,7 +137,7 @@ public class MarkwonConfiguration { private final Context context; private MarkwonTheme theme; - private AsyncDrawable.Loader asyncDrawableLoader; + private AsyncDrawableLoader asyncDrawableLoader; private SyntaxHighlight syntaxHighlight; private LinkSpan.Resolver linkResolver; private UrlProcessor urlProcessor; @@ -166,19 +167,6 @@ public class MarkwonConfiguration { this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; } -// @NonNull -// @Deprecated -// public Builder theme(@NonNull MarkwonTheme theme) { -// this.theme = theme; -// return this; -// } - - @NonNull - public Builder asyncDrawableLoader(@NonNull AsyncDrawable.Loader asyncDrawableLoader) { - this.asyncDrawableLoader = asyncDrawableLoader; - return this; - } - @NonNull public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { this.syntaxHighlight = syntaxHighlight; @@ -260,13 +248,10 @@ public class MarkwonConfiguration { } @NonNull - public MarkwonConfiguration build(@NonNull MarkwonTheme theme) { + public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) { this.theme = theme; - - if (asyncDrawableLoader == null) { - asyncDrawableLoader = new AsyncDrawableLoaderNoOp(); - } + this.asyncDrawableLoader = asyncDrawableLoader; if (syntaxHighlight == null) { syntaxHighlight = new SyntaxHighlightNoOp(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index db38e183..f92881e2 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -62,7 +62,7 @@ class MarkwonImpl extends Markwon2 { textView.setText(markdown); for (MarkwonPlugin plugin : plugins) { - plugin.afterSetText(textView, markdown); + plugin.afterSetText(textView); } } } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index bd54d1d0..690c8f97 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -5,6 +5,7 @@ import android.widget.TextView; import org.commonmark.parser.Parser; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.spans.MarkwonTheme; public interface MarkwonPlugin { @@ -13,11 +14,12 @@ public interface MarkwonPlugin { void configureTheme(@NonNull MarkwonTheme.Builder builder); + void configureImages(@NonNull AsyncDrawableLoader.Builder builder); + void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder); void configureVisitor(@NonNull MarkwonVisitor.Builder builder); - // images // html @NonNull @@ -25,5 +27,8 @@ public interface MarkwonPlugin { void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown); - void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown); + // this method do not receive markdown like `beforeSetText` does because at this + // point TextView already has markdown set and to manipulate spans one must + // request them from TextView (getText()) + void afterSetText(@NonNull TextView textView); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 12a912fc..5bb22998 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -17,7 +17,7 @@ public interface MarkwonVisitor extends Visitor { interface Builder { @NonNull - Builder on(@NonNull Class node, @NonNull NodeVisitor nodeVisitor); + Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor); @NonNull MarkwonVisitor build(@NonNull MarkwonConfiguration configuration); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index c4f98d04..07684737 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -281,8 +281,14 @@ class MarkwonVisitorImpl implements MarkwonVisitor { @NonNull @Override - public Builder on(@NonNull Class node, @NonNull NodeVisitor nodeVisitor) { - nodes.put(node, nodeVisitor); + public Builder on(@NonNull Class node, @Nullable NodeVisitor 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; } diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java index 6297b777..13a64cf4 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java @@ -3,14 +3,11 @@ package ru.noties.markwon; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import java.util.List; - +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.spans.TableRowSpan; /** * Each method can return null or a Span object or an array of spans @@ -46,16 +43,6 @@ public interface SpannableFactory { @Nullable Object strikethrough(); - @Nullable - Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone); - - @Nullable - Object tableRow( - @NonNull MarkwonTheme theme, - @NonNull List cells, - boolean isHeader, - boolean isOdd); - /** * @since 1.1.1 */ @@ -66,7 +53,7 @@ public interface SpannableFactory { Object image( @NonNull MarkwonTheme theme, @NonNull String destination, - @NonNull AsyncDrawable.Loader loader, + @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink); diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java index 6fc8d48f..21074983 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java @@ -5,11 +5,10 @@ import android.support.annotation.Nullable; import android.text.style.StrikethroughSpan; import android.text.style.UnderlineSpan; -import java.util.List; - +import ru.noties.markwon.image.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawableLoader; 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.BlockQuoteSpan; import ru.noties.markwon.spans.BulletListItemSpan; @@ -17,13 +16,11 @@ import ru.noties.markwon.spans.CodeSpan; import ru.noties.markwon.spans.EmphasisSpan; import ru.noties.markwon.spans.HeadingSpan; import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.OrderedListItemSpan; import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.spans.OrderedListItemSpan; import ru.noties.markwon.spans.StrongEmphasisSpan; import ru.noties.markwon.spans.SubScriptSpan; import ru.noties.markwon.spans.SuperScriptSpan; -import ru.noties.markwon.spans.TableRowSpan; -import ru.noties.markwon.tasklist.TaskListSpan; import ru.noties.markwon.spans.ThematicBreakSpan; /** @@ -91,18 +88,6 @@ public class SpannableFactoryDef implements SpannableFactory { return new StrikethroughSpan(); } - @Nullable - @Override - public Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) { - return new TaskListSpan(theme, blockIndent, isDone); - } - - @Nullable - @Override - public Object tableRow(@NonNull MarkwonTheme theme, @NonNull List cells, boolean isHeader, boolean isOdd) { - return new TableRowSpan(theme, cells, isHeader, isOdd); - } - /** * @since 1.1.1 */ @@ -114,7 +99,7 @@ public class SpannableFactoryDef implements SpannableFactory { @Nullable @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { return new AsyncDrawableSpan( theme, new AsyncDrawable( diff --git a/markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java b/markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java similarity index 97% rename from markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java rename to markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java index fd64d9d5..3d7fae23 100644 --- a/markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java +++ b/markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.core; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -15,10 +15,10 @@ import java.util.Collections; import java.util.List; import ru.noties.markwon.renderer.R; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawableSpan; -abstract class DrawablesScheduler { +abstract class AsyncDrawableScheduler { static void schedule(@NonNull final TextView textView) { @@ -104,7 +104,7 @@ abstract class DrawablesScheduler { return list; } - private DrawablesScheduler() { + private AsyncDrawableScheduler() { } private static class DrawableCallbackImpl implements Drawable.Callback { diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 3d4ef0c4..e2893c53 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -11,7 +11,6 @@ import org.commonmark.node.Emphasis; import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.HardLineBreak; import org.commonmark.node.Heading; -import org.commonmark.node.Image; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; import org.commonmark.node.ListBlock; @@ -73,13 +72,19 @@ public class CorePlugin extends AbstractMarkwonPlugin { softLineBreak(builder); hardLineBreak(builder); paragraph(builder); - image(builder); +// image(builder); link(builder); } @Override public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { OrderedListItemSpan.measure(textView, markdown); + AsyncDrawableScheduler.unschedule(textView); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + AsyncDrawableScheduler.schedule(textView); } protected void text(@NonNull MarkwonVisitor.Builder builder) { @@ -366,41 +371,6 @@ public class CorePlugin extends AbstractMarkwonPlugin { }); } - protected void image(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Image.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) { - - final int length = visitor.length(); - - visitor.visitChildren(image); - - // we must check if anything _was_ added, as we need at least one char to render - if (length == visitor.length()) { - visitor.builder().append('\uFFFC'); - } - - final MarkwonConfiguration configuration = visitor.configuration(); - - final Node parent = image.getParent(); - final boolean link = parent instanceof Link; - final String destination = configuration - .urlProcessor() - .process(image.getDestination()); - - final Object spans = visitor.factory().image( - visitor.theme(), - destination, - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - null, - link); - - visitor.setSpans(length, spans); - } - }); - } - protected void link(@NonNull MarkwonVisitor.Builder builder) { builder.on(Link.class, new MarkwonVisitor.NodeVisitor() { @Override diff --git a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java similarity index 93% rename from markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java rename to markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java index 588b8a04..3184f599 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.image; import android.graphics.Canvas; import android.graphics.ColorFilter; @@ -15,15 +15,8 @@ import ru.noties.markwon.renderer.ImageSizeResolver; public class AsyncDrawable extends Drawable { - public interface Loader { - - void load(@NonNull String destination, @NonNull AsyncDrawable drawable); - - void cancel(@NonNull String destination); - } - private final String destination; - private final Loader loader; + private final AsyncDrawableLoader loader; private final ImageSize imageSize; private final ImageSizeResolver imageSizeResolver; @@ -38,7 +31,7 @@ public class AsyncDrawable extends Drawable { */ public AsyncDrawable( @NonNull String destination, - @NonNull Loader loader, + @NonNull AsyncDrawableLoader loader, @Nullable ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize ) { diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java new file mode 100644 index 00000000..1e841f49 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java @@ -0,0 +1,104 @@ +package ru.noties.markwon.image; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public abstract class AsyncDrawableLoader { + + + public abstract void load(@NonNull String destination, @NonNull AsyncDrawable drawable); + + public abstract void cancel(@NonNull String destination); + + + public static class Builder { + + ExecutorService executorService; + final Map schemeHandlers = new HashMap<>(3); + final Map mediaDecoders = new HashMap<>(3); + MediaDecoder defaultMediaDecoder; + Drawable errorDrawable; + + @NonNull + public Builder executorService(@NonNull ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + @NonNull + public Builder addSchemeHandler(@NonNull String scheme, @NonNull SchemeHandler schemeHandler) { + schemeHandlers.put(scheme, schemeHandler); + return this; + } + + @NonNull + public Builder addSchemeHandler(@NonNull Collection schemes, @NonNull SchemeHandler schemeHandler) { + for (String scheme : schemes) { + schemeHandlers.put(scheme, schemeHandler); + } + return this; + } + + @NonNull + public Builder addMediaDecoder(@NonNull String contentType, @NonNull MediaDecoder mediaDecoder) { + mediaDecoders.put(contentType, mediaDecoder); + return this; + } + + @NonNull + public Builder addMediaDecoder(@NonNull Collection contentTypes, @NonNull MediaDecoder mediaDecoder) { + for (String contentType : contentTypes) { + mediaDecoders.put(contentType, mediaDecoder); + } + return this; + } + + @NonNull + public Builder removeSchemeHandler(@NonNull String scheme) { + schemeHandlers.remove(scheme); + return this; + } + + @NonNull + public Builder removeMediaDecoder(@NonNull String contentType) { + mediaDecoders.remove(contentType); + return this; + } + + @NonNull + public Builder defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { + this.defaultMediaDecoder = mediaDecoder; + return this; + } + + @NonNull + public Builder errorDrawable(Drawable errorDrawable) { + this.errorDrawable = errorDrawable; + return this; + } + + @NonNull + public AsyncDrawableLoader build() { + + // if we have no schemeHandlers -> we cannot show anything + // OR if we have no media decoders + if (schemeHandlers.size() == 0 + || (mediaDecoders.size() == 0 && defaultMediaDecoder == null)) { + return new AsyncDrawableLoaderNoOp(); + } + + if (executorService == null) { + executorService = Executors.newCachedThreadPool(); + } + + return new AsyncDrawableLoaderImpl(this); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java new file mode 100644 index 00000000..281d61fc --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java @@ -0,0 +1,135 @@ +package ru.noties.markwon.image; + +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { + + private final ExecutorService executorService; + private final Map schemeHandlers; + private final Map mediaDecoders; + private final MediaDecoder defaultMediaDecoder; + private final Drawable errorDrawable; + + private final Handler mainThread; + + private final Map> requests = new HashMap<>(2); + + AsyncDrawableLoaderImpl(@NonNull Builder builder) { + this.executorService = builder.executorService; + this.schemeHandlers = builder.schemeHandlers; + this.mediaDecoders = builder.mediaDecoders; + this.defaultMediaDecoder = builder.defaultMediaDecoder; + this.errorDrawable = builder.errorDrawable; + this.mainThread = new Handler(Looper.getMainLooper()); + } + + @Override + public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { + // if drawable is not a link -> show loading placeholder... + requests.put(destination, execute(destination, drawable)); + } + + @Override + public void cancel(@NonNull String destination) { + final Future request = requests.remove(destination); + if (request != null) { + request.cancel(true); + } + } + + private Future execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) { + + final WeakReference reference = new WeakReference(drawable); + + // todo: should we cancel pending request for the same destination? + // we _could_ but there is possibility that one resource is request in multiple places + + // todo: error handing (simply applying errorDrawable is not a good solution + // as reason for an error is unclear (no scheme handler, no input data, error decoding, etc) + + // todo: more efficient ImageMediaDecoder... BitmapFactory.decodeStream is a bit not optimal + // for big images for sure. We _could_ introduce internal Drawable that will check for + // image bounds (but we will need to cache inputStream in order to inspect and optimize + // input image...) + + return executorService.submit(new Runnable() { + @Override + public void run() { + + final ImageItem item; + + final Uri uri = Uri.parse(destination); + + final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); + + if (schemeHandler != null) { + item = schemeHandler.handle(destination, uri); + } else { + item = null; + } + + final InputStream inputStream = item != null + ? item.inputStream() + : null; + + Drawable result = null; + + if (inputStream != null) { + try { + + MediaDecoder mediaDecoder = mediaDecoders.get(item.contentType()); + if (mediaDecoder == null) { + mediaDecoder = defaultMediaDecoder; + } + + if (mediaDecoder != null) { + result = mediaDecoder.decode(inputStream); + } + + } finally { + try { + inputStream.close(); + } catch (IOException e) { + // ignored + } + } + } + + // if result is null, we assume it's an error + if (result == null) { + result = errorDrawable; + } + + if (result != null) { + final Drawable out = result; + mainThread.post(new Runnable() { + @Override + public void run() { + final boolean canDeliver = requests.remove(destination) != null; + if (canDeliver) { + final AsyncDrawable asyncDrawable = reference.get(); + if (asyncDrawable != null && asyncDrawable.isAttached()) { + asyncDrawable.setResult(out); + } + } + } + }); + } else { + requests.remove(destination); + } + } + }); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java similarity index 62% rename from markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java rename to markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java index 6a568e3a..08e1283f 100644 --- a/markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java @@ -1,10 +1,8 @@ -package ru.noties.markwon; +package ru.noties.markwon.image; import android.support.annotation.NonNull; -import ru.noties.markwon.spans.AsyncDrawable; - -class AsyncDrawableLoaderNoOp implements AsyncDrawable.Loader { +public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { @Override public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImageItem.java b/markwon/src/main/java/ru/noties/markwon/image/ImageItem.java new file mode 100644 index 00000000..2dc4b729 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageItem.java @@ -0,0 +1,31 @@ +package ru.noties.markwon.image; + +import android.support.annotation.Nullable; + +import java.io.InputStream; + +/** + * @since 2.0.0 + */ +public class ImageItem { + + private final String contentType; + private final InputStream inputStream; + + public ImageItem( + @Nullable String contentType, + @Nullable InputStream inputStream) { + this.contentType = contentType; + this.inputStream = inputStream; + } + + @Nullable + public String contentType() { + return contentType; + } + + @Nullable + public InputStream inputStream() { + return inputStream; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java b/markwon/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java new file mode 100644 index 00000000..796d016e --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java @@ -0,0 +1,52 @@ +package ru.noties.markwon.image; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.InputStream; + +import ru.noties.markwon.utils.DrawableUtils; + +/** + * This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases. + * Here we just assume that supplied InputStream is of image type and try to decode it. + * + * @since 1.1.0 + */ +public class ImageMediaDecoder extends MediaDecoder { + + @NonNull + public static ImageMediaDecoder create(@NonNull Resources resources) { + return new ImageMediaDecoder(resources); + } + + private final Resources resources; + + @SuppressWarnings("WeakerAccess") + ImageMediaDecoder(Resources resources) { + this.resources = resources; + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + final Drawable out; + + // absolutely not optimal... thing + final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + if (bitmap != null) { + out = new BitmapDrawable(resources, bitmap); + DrawableUtils.intrinsicBounds(out); + } else { + out = null; + } + + return out; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java new file mode 100644 index 00000000..0154eb8f --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -0,0 +1,92 @@ +package ru.noties.markwon.image; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.commonmark.node.Image; +import org.commonmark.node.Link; +import org.commonmark.node.Node; + +import java.util.Arrays; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.image.data.DataUriSchemeHandler; +import ru.noties.markwon.image.file.FileSchemeHandler; +import ru.noties.markwon.image.network.NetworkSchemeHandler; + +public class ImagesPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static ImagesPlugin create(@NonNull Context context) { + return new ImagesPlugin(context, false); + } + + @NonNull + public static ImagesPlugin createWithAssets(@NonNull Context context) { + return new ImagesPlugin(context, true); + } + + private final Context context; + private final boolean useAssets; + + private ImagesPlugin(Context context, boolean useAssets) { + this.context = context; + this.useAssets = useAssets; + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + + final FileSchemeHandler fileSchemeHandler = useAssets + ? FileSchemeHandler.createWithAssets(context.getAssets()) + : FileSchemeHandler.create(); + + builder + .addSchemeHandler(DataUriSchemeHandler.SCHEME, DataUriSchemeHandler.create()) + .addSchemeHandler(FileSchemeHandler.SCHEME, fileSchemeHandler) + .addSchemeHandler( + Arrays.asList( + NetworkSchemeHandler.SCHEME_HTTP, + NetworkSchemeHandler.SCHEME_HTTPS), + NetworkSchemeHandler.create()) + .defaultMediaDecoder(ImageMediaDecoder.create(context.getResources())); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Image.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) { + + final int length = visitor.length(); + + visitor.visitChildren(image); + + // we must check if anything _was_ added, as we need at least one char to render + if (length == visitor.length()) { + visitor.builder().append('\uFFFC'); + } + + final MarkwonConfiguration configuration = visitor.configuration(); + + final Node parent = image.getParent(); + final boolean link = parent instanceof Link; + final String destination = configuration + .urlProcessor() + .process(image.getDestination()); + + final Object spans = visitor.factory().image( + visitor.theme(), + destination, + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + null, + link); + + visitor.setSpans(length, spans); + } + }); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/MediaDecoder.java b/markwon/src/main/java/ru/noties/markwon/image/MediaDecoder.java new file mode 100644 index 00000000..68d0ff33 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/MediaDecoder.java @@ -0,0 +1,16 @@ +package ru.noties.markwon.image; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.InputStream; + +/** + * @since 3.0.0 + */ +public abstract class MediaDecoder { + + @Nullable + public abstract Drawable decode(@NonNull InputStream inputStream); +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/SchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/SchemeHandler.java new file mode 100644 index 00000000..cac1c801 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/SchemeHandler.java @@ -0,0 +1,14 @@ +package ru.noties.markwon.image; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * @since 3.0.0 + */ +public abstract class SchemeHandler { + + @Nullable + public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri); +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/data/DataUri.java b/markwon/src/main/java/ru/noties/markwon/image/data/DataUri.java new file mode 100644 index 00000000..6e812c92 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/data/DataUri.java @@ -0,0 +1,60 @@ +package ru.noties.markwon.image.data; + +import android.support.annotation.Nullable; + +public class DataUri { + + private final String contentType; + private final boolean base64; + private final String data; + + public DataUri(@Nullable String contentType, boolean base64, @Nullable String data) { + this.contentType = contentType; + this.base64 = base64; + this.data = data; + } + + @Nullable + public String contentType() { + return contentType; + } + + public boolean base64() { + return base64; + } + + @Nullable + public String data() { + return data; + } + + @Override + public String toString() { + return "DataUri{" + + "contentType='" + contentType + '\'' + + ", base64=" + base64 + + ", data='" + data + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DataUri dataUri = (DataUri) o; + + if (base64 != dataUri.base64) return false; + if (contentType != null ? !contentType.equals(dataUri.contentType) : dataUri.contentType != null) + return false; + return data != null ? data.equals(dataUri.data) : dataUri.data == null; + } + + @Override + public int hashCode() { + int result = contentType != null ? contentType.hashCode() : 0; + result = 31 * result + (base64 ? 1 : 0); + result = 31 * result + (data != null ? data.hashCode() : 0); + return result; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java new file mode 100644 index 00000000..7e3d4f73 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java @@ -0,0 +1,41 @@ +package ru.noties.markwon.image.data; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Base64; + +public abstract class DataUriDecoder { + + @Nullable + public abstract byte[] decode(@NonNull DataUri dataUri); + + @NonNull + public static DataUriDecoder create() { + return new Impl(); + } + + static class Impl extends DataUriDecoder { + + @Nullable + @Override + public byte[] decode(@NonNull DataUri dataUri) { + + final String data = dataUri.data(); + + if (!TextUtils.isEmpty(data)) { + try { + if (dataUri.base64()) { + return Base64.decode(data.getBytes("UTF-8"), Base64.DEFAULT); + } else { + return data.getBytes("UTF-8"); + } + } catch (Throwable t) { + return null; + } + } else { + return null; + } + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/data/DataUriParser.java b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriParser.java new file mode 100644 index 00000000..0768ee4a --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriParser.java @@ -0,0 +1,79 @@ +package ru.noties.markwon.image.data; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public abstract class DataUriParser { + + @Nullable + public abstract DataUri parse(@NonNull String input); + + + @NonNull + public static DataUriParser create() { + return new Impl(); + } + + static class Impl extends DataUriParser { + + @Nullable + @Override + public DataUri parse(@NonNull String input) { + + final int index = input.indexOf(','); + // we expect exactly one comma + if (index < 0) { + return null; + } + + final String contentType; + final boolean base64; + + if (index > 0) { + final String part = input.substring(0, index); + final String[] parts = part.split(";"); + final int length = parts.length; + if (length > 0) { + // if one: either content-type or base64 + if (length == 1) { + final String value = parts[0]; + if ("base64".equals(value)) { + contentType = null; + base64 = true; + } else { + contentType = value.indexOf('/') > -1 + ? value + : null; + base64 = false; + } + } else { + contentType = parts[0].indexOf('/') > -1 + ? parts[0] + : null; + base64 = "base64".equals(parts[length - 1]); + } + } else { + contentType = null; + base64 = false; + } + } else { + contentType = null; + base64 = false; + } + + final String data; + if (index < input.length()) { + final String value = input.substring(index + 1, input.length()).replaceAll("\n", ""); + if (value.length() == 0) { + data = null; + } else { + data = value; + } + } else { + data = null; + } + + return new DataUri(contentType, base64, data); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java new file mode 100644 index 00000000..8f44bb02 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java @@ -0,0 +1,65 @@ +package ru.noties.markwon.image.data; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.ByteArrayInputStream; + +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; + +/** + * @since 3.0.0 + */ +public class DataUriSchemeHandler extends SchemeHandler { + + public static final String SCHEME = "data"; + + @NonNull + public static DataUriSchemeHandler create() { + return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create()); + } + + private static final String START = "data:"; + + private final DataUriParser uriParser; + private final DataUriDecoder uriDecoder; + + @SuppressWarnings("WeakerAccess") + DataUriSchemeHandler(@NonNull DataUriParser uriParser, @NonNull DataUriDecoder uriDecoder) { + this.uriParser = uriParser; + this.uriDecoder = uriDecoder; + } + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + + if (!raw.startsWith(START)) { + return null; + } + + String part = raw.substring(START.length()); + + // this part is added to support `data://` with which this functionality was released + if (part.startsWith("//")) { + part = part.substring(2); + } + + final DataUri dataUri = uriParser.parse(part); + if (dataUri == null) { + return null; + } + + final byte[] bytes = uriDecoder.decode(dataUri); + if (bytes == null) { + return null; + } + + return new ImageItem( + dataUri.contentType(), + new ByteArrayInputStream(bytes) + ); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java new file mode 100644 index 00000000..712899aa --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java @@ -0,0 +1,105 @@ +package ru.noties.markwon.image.file; + +import android.content.res.AssetManager; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.webkit.MimeTypeMap; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; + +/** + * @since 3.0.0 + */ +public class FileSchemeHandler extends SchemeHandler { + + public static final String SCHEME = "file"; + + @NonNull + public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) { + return new FileSchemeHandler(assetManager); + } + + @NonNull + public static FileSchemeHandler create() { + return new FileSchemeHandler(null); + } + + private static final String FILE_ANDROID_ASSETS = "android_asset"; + + @Nullable + private final AssetManager assetManager; + + @SuppressWarnings("WeakerAccess") + FileSchemeHandler(@Nullable AssetManager assetManager) { + this.assetManager = assetManager; + } + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + + final List segments = uri.getPathSegments(); + if (segments == null + || segments.size() == 0) { + // pointing to file & having no path segments is no use + return null; + } + + final ImageItem out; + + InputStream inputStream = null; + + final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0)); + final String fileName = uri.getLastPathSegment(); + + if (assets) { + + // no handling of assets here if we have no assetsManager + if (assetManager != null) { + + final StringBuilder path = new StringBuilder(); + for (int i = 1, size = segments.size(); i < size; i++) { + if (i != 1) { + path.append('/'); + } + path.append(segments.get(i)); + } + // load assets + + try { + inputStream = assetManager.open(path.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } else { + try { + inputStream = new BufferedInputStream(new FileInputStream(new File(uri.getPath()))); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + if (inputStream != null) { + final String contentType = MimeTypeMap + .getSingleton() + .getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(fileName)); + out = new ImageItem(contentType, inputStream); + } else { + out = null; + } + + return out; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java new file mode 100644 index 00000000..c5352d4b --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java @@ -0,0 +1,64 @@ +package ru.noties.markwon.image.network; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; + +public class NetworkSchemeHandler extends SchemeHandler { + + public static final String SCHEME_HTTP = "http"; + public static final String SCHEME_HTTPS = "https"; + + @NonNull + public static NetworkSchemeHandler create() { + return new NetworkSchemeHandler(); + } + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + + try { + + final URL url = new URL(raw); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.connect(); + + final int responseCode = connection.getResponseCode(); + if (responseCode >= 200 && responseCode < 300) { + final String contentType = contentType(connection.getHeaderField("Content-Type")); + final InputStream inputStream = new BufferedInputStream(connection.getInputStream()); + return new ImageItem(contentType, inputStream); + } + + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + @Nullable + static String contentType(@Nullable String contentType) { + + if (contentType == null) { + return null; + } + + final int index = contentType.indexOf(';'); + if (index > -1) { + return contentType.substring(0, index); + } + + return contentType; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index 3ea3cc83..6a3a94e3 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -42,7 +42,7 @@ import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.spans.TableRowSpan; +import ru.noties.markwon.table.TableRowSpan; import ru.noties.markwon.tasklist.TaskListBlock; import ru.noties.markwon.tasklist.TaskListItem; @@ -321,112 +321,92 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(customNode); setSpan(length, factory.strikethrough()); - } else if (customNode instanceof TaskListItem) { - - // new in 1.0.1 - - final TaskListItem listItem = (TaskListItem) customNode; - - final int length = builder.length(); - - blockQuoteIndent += listItem.indent(); - - visitChildren(customNode); - - setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done())); - - if (hasNext(customNode)) { - newLine(); - } - - blockQuoteIndent -= listItem.indent(); - - } else if (!handleTableNodes(customNode)) { + } else { super.visit(customNode); } } - private boolean handleTableNodes(CustomNode node) { - - final boolean handled; - - if (node instanceof TableBody) { - - visitChildren(node); - tableRows = 0; - handled = true; - - if (hasNext(node)) { - newLine(); - builder.append('\n'); - } - - } else if (node instanceof TableRow || node instanceof TableHead) { - - final int length = builder.length(); - - visitChildren(node); - - if (pendingTableRow != null) { - - // @since 2.0.0 - // we cannot rely on hasNext(TableHead) as it's not reliable - // we must apply new line manually and then exclude it from tableRow span - final boolean addNewLine; - { - final int builderLength = builder.length(); - addNewLine = builderLength > 0 - && '\n' != builder.charAt(builderLength - 1); - } - if (addNewLine) { - builder.append('\n'); - } - - // @since 1.0.4 Replace table char with non-breakable space - // we need this because if table is at the end of the text, then it will be - // trimmed from the final result - builder.append('\u00a0'); - - final Object span = factory.tableRow( - theme, - pendingTableRow, - tableRowIsHeader, - tableRows % 2 == 1); - - tableRows = tableRowIsHeader - ? 0 - : tableRows + 1; - - setSpan(addNewLine ? length + 1 : length, span); - - pendingTableRow = null; - } - - handled = true; - - } else if (node instanceof TableCell) { - - final TableCell cell = (TableCell) node; - final int length = builder.length(); - visitChildren(cell); - if (pendingTableRow == null) { - pendingTableRow = new ArrayList<>(2); - } - - pendingTableRow.add(new TableRowSpan.Cell( - tableCellAlignment(cell.getAlignment()), - builder.removeFromEnd(length) - )); - - tableRowIsHeader = cell.isHeader(); - - handled = true; - } else { - handled = false; - } - - return handled; - } +// private boolean handleTableNodes(CustomNode node) { +// +// final boolean handled; +// +// if (node instanceof TableBody) { +// +// visitChildren(node); +// tableRows = 0; +// handled = true; +// +// if (hasNext(node)) { +// newLine(); +// builder.append('\n'); +// } +// +// } else if (node instanceof TableRow || node instanceof TableHead) { +// +// final int length = builder.length(); +// +// visitChildren(node); +// +// if (pendingTableRow != null) { +// +// // @since 2.0.0 +// // we cannot rely on hasNext(TableHead) as it's not reliable +// // we must apply new line manually and then exclude it from tableRow span +// final boolean addNewLine; +// { +// final int builderLength = builder.length(); +// addNewLine = builderLength > 0 +// && '\n' != builder.charAt(builderLength - 1); +// } +// if (addNewLine) { +// builder.append('\n'); +// } +// +// // @since 1.0.4 Replace table char with non-breakable space +// // we need this because if table is at the end of the text, then it will be +// // trimmed from the final result +// builder.append('\u00a0'); +// +// final Object span = factory.tableRow( +// theme, +// pendingTableRow, +// tableRowIsHeader, +// tableRows % 2 == 1); +// +// tableRows = tableRowIsHeader +// ? 0 +// : tableRows + 1; +// +// setSpan(addNewLine ? length + 1 : length, span); +// +// pendingTableRow = null; +// } +// +// handled = true; +// +// } else if (node instanceof TableCell) { +// +// final TableCell cell = (TableCell) node; +// final int length = builder.length(); +// visitChildren(cell); +// if (pendingTableRow == null) { +// pendingTableRow = new ArrayList<>(2); +// } +// +// pendingTableRow.add(new TableRowSpan.Cell( +// tableCellAlignment(cell.getAlignment()), +// builder.removeFromEnd(length) +// )); +// +// tableRowIsHeader = cell.isHeader(); +// +// handled = true; +// } else { +// handled = false; +// } +// +// return handled; +// } @Override public void visit(Paragraph paragraph) { @@ -530,26 +510,26 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { return false; } - @TableRowSpan.Alignment - private static int tableCellAlignment(TableCell.Alignment alignment) { - final int out; - if (alignment != null) { - switch (alignment) { - case CENTER: - out = TableRowSpan.ALIGN_CENTER; - break; - case RIGHT: - out = TableRowSpan.ALIGN_RIGHT; - break; - default: - out = TableRowSpan.ALIGN_LEFT; - break; - } - } else { - out = TableRowSpan.ALIGN_LEFT; - } - return out; - } +// @TableRowSpan.Alignment +// private static int tableCellAlignment(TableCell.Alignment alignment) { +// final int out; +// if (alignment != null) { +// switch (alignment) { +// case CENTER: +// out = TableRowSpan.ALIGN_CENTER; +// break; +// case RIGHT: +// out = TableRowSpan.ALIGN_RIGHT; +// break; +// default: +// out = TableRowSpan.ALIGN_LEFT; +// break; +// } +// } else { +// out = TableRowSpan.ALIGN_LEFT; +// } +// return out; +// } /** * @since 2.0.0 diff --git a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java index b1ac85d4..20b5fe9d 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java @@ -12,6 +12,8 @@ import android.text.style.ReplacementSpan; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import ru.noties.markwon.image.AsyncDrawable; + @SuppressWarnings("WeakerAccess") public class AsyncDrawableSpan extends ReplacementSpan { @@ -29,16 +31,16 @@ public class AsyncDrawableSpan extends ReplacementSpan { private final int alignment; private final boolean replacementTextIsLink; - public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) { - this(theme, drawable, ALIGN_BOTTOM); - } +// public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) { +// this(theme, drawable, ALIGN_BOTTOM); +// } - public AsyncDrawableSpan( - @NonNull MarkwonTheme theme, - @NonNull AsyncDrawable drawable, - @Alignment int alignment) { - this(theme, drawable, alignment, false); - } +// public AsyncDrawableSpan( +// @NonNull MarkwonTheme theme, +// @NonNull AsyncDrawable drawable, +// @Alignment int alignment) { +// this(theme, drawable, alignment, false); +// } public AsyncDrawableSpan( @NonNull MarkwonTheme theme, @@ -137,7 +139,7 @@ public class AsyncDrawableSpan extends ReplacementSpan { // will it make sense to have additional background/borders for an image replacement? // let's focus on main functionality and then think of it - final float textY = CanvasUtils.textCenterY(top, bottom, paint); + final float textY = textCenterY(top, bottom, paint); if (replacementTextIsLink) { theme.applyLinkStyle(paint); } @@ -150,4 +152,9 @@ public class AsyncDrawableSpan extends ReplacementSpan { public AsyncDrawable getDrawable() { return drawable; } + + private static float textCenterY(int top, int bottom, @NonNull Paint paint) { + // @since 1.1.1 it's `top +` and not `bottom -` + return (int) (top + ((bottom - top) / 2) - ((paint.descent() + paint.ascent()) / 2.F + .5F)); + } } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/CanvasUtils.java b/markwon/src/main/java/ru/noties/markwon/spans/CanvasUtils.java deleted file mode 100644 index 0851e855..00000000 --- a/markwon/src/main/java/ru/noties/markwon/spans/CanvasUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.noties.markwon.spans; - -import android.graphics.Paint; -import android.support.annotation.NonNull; - -abstract class CanvasUtils { - - static float textCenterY(int top, int bottom, @NonNull Paint paint) { - // @since 1.1.1 it's `top +` and not `bottom -` - return (int) (top + ((bottom - top) / 2) - ((paint.descent() + paint.ascent()) / 2.F + .5F)); - } - - private CanvasUtils() { - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/ColorUtils.java b/markwon/src/main/java/ru/noties/markwon/spans/ColorUtils.java deleted file mode 100644 index 4739d531..00000000 --- a/markwon/src/main/java/ru/noties/markwon/spans/ColorUtils.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.noties.markwon.spans; - -abstract class ColorUtils { - - static int applyAlpha(int color, int alpha) { - return (color & 0x00FFFFFF) | (alpha << 24); - } - - private ColorUtils() { - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java b/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java index b5c9f34a..8f803841 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java @@ -1,25 +1,22 @@ package ru.noties.markwon.spans; import android.content.Context; -import android.content.res.TypedArray; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.annotation.Px; import android.support.annotation.Size; import android.text.TextPaint; -import android.util.TypedValue; import java.util.Arrays; import java.util.Locale; -import ru.noties.markwon.tasklist.TaskListDrawable; +import ru.noties.markwon.utils.ColorUtils; +import ru.noties.markwon.utils.Dip; @SuppressWarnings("WeakerAccess") public class MarkwonTheme { @@ -77,36 +74,14 @@ public class MarkwonTheme { @NonNull public static Builder builderWithDefaults(@NonNull Context context) { - // by default we will be using link color for the checkbox color - // & window background as a checkMark color - final int linkColor = resolve(context, android.R.attr.textColorLink); - final int backgroundColor = resolve(context, android.R.attr.colorBackground); - - // before 1.0.5 build had `linkColor` set, but in order for spans to use default link color - // set directly in widget (or any caller), we should not pass it here - - final Dip dip = new Dip(context); + final Dip dip = Dip.create(context); return new Builder() .codeMultilineMargin(dip.toPx(8)) .blockMargin(dip.toPx(24)) .blockQuoteWidth(dip.toPx(4)) .bulletListItemStrokeWidth(dip.toPx(1)) .headingBreakHeight(dip.toPx(1)) - .thematicBreakHeight(dip.toPx(4)) - .tableCellPadding(dip.toPx(4)) - .tableBorderWidth(dip.toPx(1)) - .taskListDrawable(new TaskListDrawable(linkColor, linkColor, backgroundColor)); - } - - private static int resolve(Context context, @AttrRes int attr) { - final TypedValue typedValue = new TypedValue(); - final int attrs[] = new int[]{attr}; - final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs); - try { - return typedArray.getColor(0, 0); - } finally { - typedArray.recycle(); - } + .thematicBreakHeight(dip.toPx(4)); } protected static final int BLOCK_QUOTE_DEF_COLOR_ALPHA = 25; @@ -126,10 +101,6 @@ public class MarkwonTheme { protected static final int THEMATIC_BREAK_DEF_ALPHA = 25; - protected static final int TABLE_BORDER_DEF_ALPHA = 75; - - protected static final int TABLE_ODD_ROW_DEF_ALPHA = 22; - protected final int linkColor; // used in quote, lists @@ -197,30 +168,6 @@ public class MarkwonTheme { // by default paint.strokeWidth protected final int thematicBreakHeight; - // by default 0 - protected final int tableCellPadding; - - // by default paint.color * TABLE_BORDER_DEF_ALPHA - protected final int tableBorderColor; - - protected final int tableBorderWidth; - - // by default paint.color * TABLE_ODD_ROW_DEF_ALPHA - protected final int tableOddRowBackgroundColor; - - // @since 1.1.1 - // by default no background - protected final int tableEventRowBackgroundColor; - - // @since 1.1.1 - // by default no background - protected final int tableHeaderRowBackgroundColor; - - // drawable that will be used to render checkbox (should be stateful) - // TaskListDrawable can be used - @Deprecated - protected final Drawable taskListDrawable; - protected MarkwonTheme(@NonNull Builder builder) { this.linkColor = builder.linkColor; this.blockMargin = builder.blockMargin; @@ -243,13 +190,6 @@ public class MarkwonTheme { this.scriptTextSizeRatio = builder.scriptTextSizeRatio; this.thematicBreakColor = builder.thematicBreakColor; this.thematicBreakHeight = builder.thematicBreakHeight; - this.tableCellPadding = builder.tableCellPadding; - this.tableBorderColor = builder.tableBorderColor; - this.tableBorderWidth = builder.tableBorderWidth; - this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor; - this.tableEventRowBackgroundColor = builder.tableEvenRowBackgroundColor; - this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor; - this.taskListDrawable = builder.taskListDrawable; } /** @@ -468,71 +408,6 @@ public class MarkwonTheme { } } - public int tableCellPadding() { - return tableCellPadding; - } - - public int tableBorderWidth(@NonNull Paint paint) { - final int out; - if (tableBorderWidth == -1) { - out = (int) (paint.getStrokeWidth() + .5F); - } else { - out = tableBorderWidth; - } - return out; - } - - public void applyTableBorderStyle(@NonNull Paint paint) { - - final int color; - if (tableBorderColor == 0) { - color = ColorUtils.applyAlpha(paint.getColor(), TABLE_BORDER_DEF_ALPHA); - } else { - color = tableBorderColor; - } - - paint.setColor(color); - paint.setStyle(Paint.Style.STROKE); - } - - public void applyTableOddRowStyle(@NonNull Paint paint) { - final int color; - if (tableOddRowBackgroundColor == 0) { - color = ColorUtils.applyAlpha(paint.getColor(), TABLE_ODD_ROW_DEF_ALPHA); - } else { - color = tableOddRowBackgroundColor; - } - paint.setColor(color); - paint.setStyle(Paint.Style.FILL); - } - - /** - * @since 1.1.1 - */ - public void applyTableEvenRowStyle(@NonNull Paint paint) { - // by default to background to even row - paint.setColor(tableEventRowBackgroundColor); - paint.setStyle(Paint.Style.FILL); - } - - /** - * @since 1.1.1 - */ - public void applyTableHeaderRowStyle(@NonNull Paint paint) { - paint.setColor(tableHeaderRowBackgroundColor); - paint.setStyle(Paint.Style.FILL); - } - - /** - * @return a Drawable to be used as a checkbox indication in task lists - * @since 1.0.1 - */ - @Nullable - @Deprecated - public Drawable getTaskListDrawable() { - return taskListDrawable; - } - @SuppressWarnings("unused") public static class Builder { @@ -557,13 +432,6 @@ public class MarkwonTheme { private float scriptTextSizeRatio; private int thematicBreakColor; private int thematicBreakHeight = -1; - private int tableCellPadding; - private int tableBorderColor; - private int tableBorderWidth = -1; - private int tableOddRowBackgroundColor; - private int tableEvenRowBackgroundColor; // @since 1.1.1 - private int tableHeaderRowBackgroundColor; // @since 1.1.1 - private Drawable taskListDrawable; Builder() { } @@ -590,11 +458,6 @@ public class MarkwonTheme { this.scriptTextSizeRatio = theme.scriptTextSizeRatio; this.thematicBreakColor = theme.thematicBreakColor; this.thematicBreakHeight = theme.thematicBreakHeight; - this.tableCellPadding = theme.tableCellPadding; - this.tableBorderColor = theme.tableBorderColor; - this.tableBorderWidth = theme.tableBorderWidth; - this.tableOddRowBackgroundColor = theme.tableOddRowBackgroundColor; - this.taskListDrawable = theme.taskListDrawable; } @NonNull @@ -742,81 +605,10 @@ public class MarkwonTheme { return this; } - @NonNull - public Builder tableCellPadding(@Px int tableCellPadding) { - this.tableCellPadding = tableCellPadding; - return this; - } - - @NonNull - public Builder tableBorderColor(@ColorInt int tableBorderColor) { - this.tableBorderColor = tableBorderColor; - return this; - } - - @NonNull - public Builder tableBorderWidth(@Px int tableBorderWidth) { - this.tableBorderWidth = tableBorderWidth; - return this; - } - - @NonNull - public Builder tableOddRowBackgroundColor(@ColorInt int tableOddRowBackgroundColor) { - this.tableOddRowBackgroundColor = tableOddRowBackgroundColor; - return this; - } - - /** - * @since 1.1.1 - */ - @NonNull - public Builder tableEvenRowBackgroundColor(@ColorInt int tableEvenRowBackgroundColor) { - this.tableEvenRowBackgroundColor = tableEvenRowBackgroundColor; - return this; - } - - /** - * @since 1.1.1 - */ - @NonNull - public Builder tableHeaderRowBackgroundColor(@ColorInt int tableHeaderRowBackgroundColor) { - this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor; - return this; - } - - /** - * Supplied Drawable must be stateful ({@link Drawable#isStateful()} returns true). If a task - * is marked as done, then this drawable will be updated with an {@code int[] { android.R.attr.state_checked }} - * as the state, otherwise an empty array will be used. This library provides a ready to be - * used Drawable: {@link TaskListDrawable} - * - * @param taskListDrawable Drawable to be used as the task list indication (checkbox) - * @see TaskListDrawable - * @since 1.0.1 - */ - @NonNull - @Deprecated - public Builder taskListDrawable(@NonNull Drawable taskListDrawable) { - this.taskListDrawable = taskListDrawable; - return this; - } - @NonNull public MarkwonTheme build() { return new MarkwonTheme(this); } } - private static class Dip { - - private final float density; - - Dip(@NonNull Context context) { - this.density = context.getResources().getDisplayMetrics().density; - } - - int toPx(int dp) { - return (int) (dp * density + .5F); - } - } } diff --git a/markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java b/markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java new file mode 100644 index 00000000..506b8ff3 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java @@ -0,0 +1,189 @@ +package ru.noties.markwon.table; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.widget.TextView; + +import org.commonmark.ext.gfm.tables.TableBody; +import org.commonmark.ext.gfm.tables.TableCell; +import org.commonmark.ext.gfm.tables.TableHead; +import org.commonmark.ext.gfm.tables.TableRow; +import org.commonmark.ext.gfm.tables.TablesExtension; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.SpannableBuilder; + +public class TablePlugin extends AbstractMarkwonPlugin { + + @NonNull + public static TablePlugin create(@NonNull Context context) { + return new TablePlugin(TableTheme.create(context)); + } + + @NonNull + public static TablePlugin create(@NonNull TableTheme tableTheme) { + return new TablePlugin(tableTheme); + } + + private final TableTheme tableTheme; + + TablePlugin(@NonNull TableTheme tableTheme) { + this.tableTheme = tableTheme; + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.extensions(Collections.singleton(TablesExtension.create())); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + TableVisitor.configure(tableTheme, builder); + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + TableRowsScheduler.unschedule(textView); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + TableRowsScheduler.schedule(textView); + } + + private static class TableVisitor { + + static void configure(@NonNull TableTheme tableTheme, @NonNull MarkwonVisitor.Builder builder) { + new TableVisitor(tableTheme, builder); + } + + private final TableTheme tableTheme; + + private List pendingTableRow; + private boolean tableRowIsHeader; + private int tableRows; + + private TableVisitor(@NonNull TableTheme tableTheme, @NonNull MarkwonVisitor.Builder builder) { + this.tableTheme = tableTheme; + builder + .on(TableBody.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableBody tableBody) { + + visitor.visitChildren(tableBody); + tableRows = 0; + + if (visitor.hasNext(tableBody)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }) + .on(TableRow.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableRow tableRow) { + visitRow(visitor, tableRow); + } + }) + .on(TableHead.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableHead tableHead) { + visitRow(visitor, tableHead); + } + }) + .on(TableCell.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableCell tableCell) { + + final int length = visitor.length(); + + visitor.visitChildren(tableCell); + + if (pendingTableRow == null) { + pendingTableRow = new ArrayList<>(2); + } + + pendingTableRow.add(new TableRowSpan.Cell( + tableCellAlignment(tableCell.getAlignment()), + visitor.builder().removeFromEnd(length) + )); + + tableRowIsHeader = tableCell.isHeader(); + } + }); + } + + private void visitRow(@NonNull MarkwonVisitor visitor, @NonNull Node node) { + + final int length = visitor.length(); + + visitor.visitChildren(node); + + if (pendingTableRow != null) { + + final SpannableBuilder builder = visitor.builder(); + + // @since 2.0.0 + // we cannot rely on hasNext(TableHead) as it's not reliable + // we must apply new line manually and then exclude it from tableRow span + final boolean addNewLine; + { + final int builderLength = builder.length(); + addNewLine = builderLength > 0 + && '\n' != builder.charAt(builderLength - 1); + } + + if (addNewLine) { + visitor.forceNewLine(); + } + + // @since 1.0.4 Replace table char with non-breakable space + // we need this because if table is at the end of the text, then it will be + // trimmed from the final result + builder.append('\u00a0'); + + final Object span = new TableRowSpan( + tableTheme, + pendingTableRow, + tableRowIsHeader, + tableRows % 2 == 1); + + tableRows = tableRowIsHeader + ? 0 + : tableRows + 1; + + visitor.setSpans(addNewLine ? length + 1 : length, span); + + pendingTableRow = null; + } + } + + @TableRowSpan.Alignment + private static int tableCellAlignment(TableCell.Alignment alignment) { + final int out; + if (alignment != null) { + switch (alignment) { + case CENTER: + out = TableRowSpan.ALIGN_CENTER; + break; + case RIGHT: + out = TableRowSpan.ALIGN_RIGHT; + break; + default: + out = TableRowSpan.ALIGN_LEFT; + break; + } + } else { + out = TableRowSpan.ALIGN_LEFT; + } + return out; + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java b/markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java rename to markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java index a54b3206..903cf5f3 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.table; import android.annotation.SuppressLint; import android.graphics.Canvas; @@ -61,22 +61,22 @@ public class TableRowSpan extends ReplacementSpan { } } - private final MarkwonTheme theme; + private final TableTheme theme; private final List cells; private final List layouts; private final TextPaint textPaint; private final boolean header; private final boolean odd; - private final Rect rect = ObjectsPool.rect(); - private final Paint paint = ObjectsPool.paint(); + private final Rect rect = new Rect(); + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private int width; private int height; private Invalidator invalidator; public TableRowSpan( - @NonNull MarkwonTheme theme, + @NonNull TableTheme theme, @NonNull List cells, boolean header, boolean odd) { @@ -272,8 +272,7 @@ public class TableRowSpan extends ReplacementSpan { return out; } - public TableRowSpan invalidator(Invalidator invalidator) { + public void invalidator(@Nullable Invalidator invalidator) { this.invalidator = invalidator; - return this; } } diff --git a/markwon/src/main/java/ru/noties/markwon/TableRowsScheduler.java b/markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/TableRowsScheduler.java rename to markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java index 6fc9a584..1d1246b0 100644 --- a/markwon/src/main/java/ru/noties/markwon/TableRowsScheduler.java +++ b/markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java @@ -1,13 +1,13 @@ -package ru.noties.markwon; +package ru.noties.markwon.table; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.Spanned; import android.text.TextUtils; import android.view.View; import android.widget.TextView; import ru.noties.markwon.renderer.R; -import ru.noties.markwon.spans.TableRowSpan; abstract class TableRowsScheduler { @@ -57,6 +57,7 @@ abstract class TableRowsScheduler { } } + @Nullable private static Object[] extract(@NonNull TextView view) { final Object[] out; final CharSequence text = view.getText(); diff --git a/markwon/src/main/java/ru/noties/markwon/table/TableTheme.java b/markwon/src/main/java/ru/noties/markwon/table/TableTheme.java new file mode 100644 index 00000000..56a72321 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/table/TableTheme.java @@ -0,0 +1,164 @@ +package ru.noties.markwon.table; + +import android.content.Context; +import android.graphics.Paint; +import android.support.annotation.NonNull; + +import ru.noties.markwon.utils.ColorUtils; +import ru.noties.markwon.utils.Dip; + +public class TableTheme { + + @NonNull + public static TableTheme create(@NonNull Context context) { + final Dip dip = Dip.create(context); + return builder() + .tableCellPadding(dip.toPx(4)) + .tableBorderWidth(dip.toPx(1)) + .build(); + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + + protected static final int TABLE_BORDER_DEF_ALPHA = 75; + + protected static final int TABLE_ODD_ROW_DEF_ALPHA = 22; + + // by default 0 + protected final int tableCellPadding; + + // by default paint.color * TABLE_BORDER_DEF_ALPHA + protected final int tableBorderColor; + + protected final int tableBorderWidth; + + // by default paint.color * TABLE_ODD_ROW_DEF_ALPHA + protected final int tableOddRowBackgroundColor; + + // @since 1.1.1 + // by default no background + protected final int tableEvenRowBackgroundColor; + + // @since 1.1.1 + // by default no background + protected final int tableHeaderRowBackgroundColor; + + protected TableTheme(@NonNull Builder builder) { + this.tableCellPadding = builder.tableCellPadding; + this.tableBorderColor = builder.tableBorderColor; + this.tableBorderWidth = builder.tableBorderWidth; + this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor; + this.tableEvenRowBackgroundColor = builder.tableEvenRowBackgroundColor; + this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor; + } + + public int tableCellPadding() { + return tableCellPadding; + } + + public int tableBorderWidth(@NonNull Paint paint) { + final int out; + if (tableBorderWidth == -1) { + out = (int) (paint.getStrokeWidth() + .5F); + } else { + out = tableBorderWidth; + } + return out; + } + + public void applyTableBorderStyle(@NonNull Paint paint) { + + final int color; + if (tableBorderColor == 0) { + color = ColorUtils.applyAlpha(paint.getColor(), TABLE_BORDER_DEF_ALPHA); + } else { + color = tableBorderColor; + } + + paint.setColor(color); + paint.setStyle(Paint.Style.STROKE); + } + + public void applyTableOddRowStyle(@NonNull Paint paint) { + final int color; + if (tableOddRowBackgroundColor == 0) { + color = ColorUtils.applyAlpha(paint.getColor(), TABLE_ODD_ROW_DEF_ALPHA); + } else { + color = tableOddRowBackgroundColor; + } + paint.setColor(color); + paint.setStyle(Paint.Style.FILL); + } + + /** + * @since 1.1.1 + */ + public void applyTableEvenRowStyle(@NonNull Paint paint) { + // by default to background to even row + paint.setColor(tableEvenRowBackgroundColor); + paint.setStyle(Paint.Style.FILL); + } + + /** + * @since 1.1.1 + */ + public void applyTableHeaderRowStyle(@NonNull Paint paint) { + paint.setColor(tableHeaderRowBackgroundColor); + paint.setStyle(Paint.Style.FILL); + } + + public static class Builder { + + private int tableCellPadding; + private int tableBorderColor; + private int tableBorderWidth = -1; + private int tableOddRowBackgroundColor; + private int tableEvenRowBackgroundColor; // @since 1.1.1 + private int tableHeaderRowBackgroundColor; // @since 1.1.1 + + @NonNull + public Builder tableCellPadding(int tableCellPadding) { + this.tableCellPadding = tableCellPadding; + return this; + } + + @NonNull + public Builder tableBorderColor(int tableBorderColor) { + this.tableBorderColor = tableBorderColor; + return this; + } + + @NonNull + public Builder tableBorderWidth(int tableBorderWidth) { + this.tableBorderWidth = tableBorderWidth; + return this; + } + + @NonNull + public Builder tableOddRowBackgroundColor(int tableOddRowBackgroundColor) { + this.tableOddRowBackgroundColor = tableOddRowBackgroundColor; + return this; + } + + @NonNull + public Builder tableEvenRowBackgroundColor(int tableEvenRowBackgroundColor) { + this.tableEvenRowBackgroundColor = tableEvenRowBackgroundColor; + return this; + } + + @NonNull + public Builder tableHeaderRowBackgroundColor(int tableHeaderRowBackgroundColor) { + this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor; + return this; + } + + @NonNull + public TableTheme build() { + return new TableTheme(this); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java index bf75d5c7..77c865c4 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java @@ -1,9 +1,12 @@ package ru.noties.markwon.tasklist; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; +import android.util.TypedValue; import org.commonmark.parser.Parser; @@ -13,6 +16,11 @@ import ru.noties.markwon.MarkwonVisitor; public class TaskListPlugin extends AbstractMarkwonPlugin { /** + * Supplied Drawable must be stateful ({@link Drawable#isStateful()} returns true). If a task + * is marked as done, then this drawable will be updated with an {@code int[] { android.R.attr.state_checked }} + * as the state, otherwise an empty array will be used. This library provides a ready to be + * used Drawable: {@link TaskListDrawable} + * * @see TaskListDrawable */ @NonNull @@ -22,8 +30,13 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { @NonNull public static TaskListPlugin create(@NonNull Context context) { - // resolve link color and background color - return null; + + // by default we will be using link color for the checkbox color + // & window background as a checkMark color + final int linkColor = resolve(context, android.R.attr.textColorLink); + final int backgroundColor = resolve(context, android.R.attr.colorBackground); + + return new TaskListPlugin(new TaskListDrawable(linkColor, linkColor, backgroundColor)); } @NonNull @@ -90,4 +103,15 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { } }); } + + private static int resolve(Context context, @AttrRes int attr) { + final TypedValue typedValue = new TypedValue(); + final int attrs[] = new int[]{attr}; + final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs); + try { + return typedArray.getColor(0, 0); + } finally { + typedArray.recycle(); + } + } } diff --git a/markwon/src/main/java/ru/noties/markwon/utils/ColorUtils.java b/markwon/src/main/java/ru/noties/markwon/utils/ColorUtils.java new file mode 100644 index 00000000..d6305132 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/utils/ColorUtils.java @@ -0,0 +1,11 @@ +package ru.noties.markwon.utils; + +public abstract class ColorUtils { + + public static int applyAlpha(int color, int alpha) { + return (color & 0x00FFFFFF) | (alpha << 24); + } + + private ColorUtils() { + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/utils/Dip.java b/markwon/src/main/java/ru/noties/markwon/utils/Dip.java new file mode 100644 index 00000000..6899df8f --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/utils/Dip.java @@ -0,0 +1,29 @@ +package ru.noties.markwon.utils; + +import android.content.Context; +import android.support.annotation.NonNull; + +import ru.noties.markwon.spans.MarkwonTheme; + +public class Dip { + + @NonNull + public static Dip create(@NonNull Context context) { + return new Dip(context.getResources().getDisplayMetrics().density); + } + + @NonNull + public static Dip create(float density) { + return new Dip(density); + } + + private final float density; + + public Dip(float density) { + this.density = density; + } + + public int toPx(int dp) { + return (int) (dp * density + .5F); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/utils/DrawableUtils.java b/markwon/src/main/java/ru/noties/markwon/utils/DrawableUtils.java new file mode 100644 index 00000000..34342093 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/utils/DrawableUtils.java @@ -0,0 +1,13 @@ +package ru.noties.markwon.utils; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; + +public abstract class DrawableUtils { + + public static void intrinsicBounds(@NonNull Drawable drawable) { + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + } + + private DrawableUtils() {} +} diff --git a/markwon/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java b/markwon/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java new file mode 100644 index 00000000..b361a0d3 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java @@ -0,0 +1,119 @@ +package ru.noties.markwon.image.data; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class DataUriParserTest { + + private DataUriParser.Impl impl; + + @Before + public void before() { + impl = new DataUriParser.Impl(); + } + + @Test + public void test() { + + final Map data = new LinkedHashMap() {{ + put(",", new DataUri(null, false, null)); + put("image/svg+xml;base64,!@#$%^&*(", new DataUri("image/svg+xml", true, "!@#$%^&*(")); + put("text/vnd-example+xyz;foo=bar;base64,R0lGODdh", new DataUri("text/vnd-example+xyz", true, "R0lGODdh")); + put("text/plain;charset=UTF-8;page=21,the%20data:1234,5678", new DataUri("text/plain", false, "the%20data:1234,5678")); + }}; + + for (Map.Entry entry : data.entrySet()) { + assertEquals(entry.getKey(), entry.getValue(), impl.parse(entry.getKey())); + } + } + + @Test + public void data_new_lines_are_ignored() { + + final String input = "image/png;base64,iVBORw0KGgoAAA\n" + + "ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4\n" + + "//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU\n" + + "5ErkJggg=="; + + assertEquals( + new DataUri("image/png", true, "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="), + impl.parse(input) + ); + } + + @Test + public void no_comma_returns_null() { + + final String[] inputs = { + "", + "what-ever", + ";;;;;;;", + "some crazy data" + }; + + for (String input : inputs) { + assertNull(input, impl.parse(input)); + } + } + + @Test + public void two_commas() { + final String input = ",,"; // <- second one would be considered data... + assertEquals( + input, + new DataUri(null, false, ","), + impl.parse(input) + ); + } + + @Test + public void more_commas() { + final String input = "first,second,third"; // <- first is just a value (will be ignored) + assertEquals( + input, + new DataUri(null, false, "second,third"), + impl.parse(input) + ); + } + + @Test + public void base64_no_content_type() { + final String input = ";base64,12345"; + assertEquals( + input, + new DataUri(null, true, "12345"), + impl.parse(input) + ); + } + + @Test + public void not_base64_no_content_type() { + final String input = ",qweRTY"; + assertEquals( + input, + new DataUri(null, false, "qweRTY"), + impl.parse(input) + ); + } + + @Test + public void content_type_data_no_base64() { + final String input = "image/png,aSdFg"; + assertEquals( + input, + new DataUri("image/png", false, "aSdFg"), + impl.parse(input) + ); + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java b/markwon/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java new file mode 100644 index 00000000..16dc73b5 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java @@ -0,0 +1,114 @@ +package ru.noties.markwon.image.data; + +import android.net.Uri; +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +import ru.noties.markwon.image.ImageItem; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class DataUriSchemeHandlerTest { + + private DataUriSchemeHandler handler; + + @Before + public void before() { + handler = DataUriSchemeHandler.create(); + } + + @Test + public void scheme_specific_part_is_empty() { + assertNull(handler.handle("data:", Uri.parse("data:"))); + } + + @Test + public void data_uri_is_empty() { + assertNull(handler.handle("data://whatever", Uri.parse("data://whatever"))); + } + + @Test + public void no_data() { + assertNull(handler.handle("data://,", Uri.parse("data://,"))); + } + + @Test + public void correct() { + + final class Item { + + final String contentType; + final String data; + + Item(String contentType, String data) { + this.contentType = contentType; + this.data = data; + } + } + + final Map expected = new HashMap() {{ + put("data://text/plain;,123", new Item("text/plain", "123")); + put("data://image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123")); + }}; + + for (Map.Entry entry : expected.entrySet()) { + final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey())); + assertNotNull(entry.getKey(), item); + assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType()); + assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream())); + } + } + + @Test + public void correct_real() { + + final class Item { + + final String contentType; + final String data; + + Item(String contentType, String data) { + this.contentType = contentType; + this.data = data; + } + } + + final Map expected = new HashMap() {{ + put("data:text/plain;,123", new Item("text/plain", "123")); + put("data:image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123")); + }}; + + for (Map.Entry entry : expected.entrySet()) { + final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey())); + assertNotNull(entry.getKey(), item); + assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType()); + assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream())); + } + } + + @NonNull + private static String readStream(@NonNull InputStream stream) { + try { + final Scanner scanner = new Scanner(stream, "UTF-8").useDelimiter("\\A"); + return scanner.hasNext() + ? scanner.next() + : ""; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java index e094164b..108df62d 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java @@ -8,7 +8,7 @@ import ru.noties.markwon.SyntaxHighlight; import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java index bfba93b1..7f04566b 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java @@ -30,7 +30,7 @@ import java.util.Set; import ix.Ix; import ix.IxFunction; import ix.IxPredicate; -import ru.noties.markwon.spans.TableRowSpan; +import ru.noties.markwon.table.TableRowSpan; import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java index 42d46f99..6e6feb30 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java @@ -11,10 +11,10 @@ import java.util.Map; import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.spans.TableRowSpan; +import ru.noties.markwon.table.TableRowSpan; import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; diff --git a/sample-latex-math/build.gradle b/sample-latex-math/build.gradle index 81732d95..f9f7c1eb 100644 --- a/sample-latex-math/build.gradle +++ b/sample-latex-math/build.gradle @@ -18,6 +18,6 @@ android { dependencies { implementation project(':markwon') - implementation project(':markwon-image-loader') +// implementation project(':markwon-image-loader') implementation 'ru.noties:jlatexmath-android:0.1.0' } diff --git a/settings.gradle b/settings.gradle index 11192dbe..3a1678e4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ rootProject.name = 'MarkwonProject' -include ':app', ':markwon', ':markwon-image-loader', ':markwon-view', ':sample-custom-extension', ':sample-latex-math', +include ':app', ':markwon', ':markwon-view', ':sample-custom-extension', ':sample-latex-math', ':markwon-image-svg', ':markwon-image-gif', ':markwon-syntax-highlight', ':markwon-html-parser-api', ':markwon-html-parser-impl'