Syntax highlight plugin, image modules (svg, gif), working sample application

This commit is contained in:
Dimitry Ivanov 2018-11-25 15:48:58 +03:00
parent 3526e16565
commit 2efd12f020
65 changed files with 2146 additions and 598 deletions

View File

@ -29,7 +29,8 @@ android {
dependencies { dependencies {
implementation project(':markwon') implementation project(':markwon')
implementation project(':markwon-image-loader') implementation project(':markwon-image-gif')
implementation project(':markwon-image-svg')
implementation project(':markwon-syntax-highlight') implementation project(':markwon-syntax-highlight')
deps.with { deps.with {

View File

@ -14,11 +14,6 @@ import dagger.Module;
import dagger.Provides; import dagger.Provides;
import okhttp3.Cache; import okhttp3.Cache;
import okhttp3.OkHttpClient; 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.Prism4jThemeDarkula;
import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.markwon.syntax.Prism4jThemeDefault;
import ru.noties.prism4j.Prism4j; import ru.noties.prism4j.Prism4j;
@ -72,23 +67,6 @@ class AppModule {
return new UriProcessorImpl(); 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 @Provides
@Singleton @Singleton
Prism4j prism4j() { Prism4j prism4j() {
@ -104,12 +82,12 @@ class AppModule {
@Singleton @Singleton
@Provides @Provides
Prism4jThemeDarkula prism4jThemeDarkula() { Prism4jThemeDarkula prism4jThemeDarkula() {
return Prism4jThemeDarkula.create(); return Prism4jThemeDarkula.create(0x0Fffffff);
}
@Singleton
@Provides
GifProcessor gifProcessor() {
return GifProcessor.create();
} }
//
// @Singleton
// @Provides
// GifProcessor gifProcessor() {
// return GifProcessor.create();
// }
} }

View File

@ -6,9 +6,10 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import pl.droidsonroids.gif.GifDrawable; import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.image.AsyncDrawable;
public class GifAwareAsyncDrawable extends AsyncDrawable { public class GifAwareAsyncDrawable extends AsyncDrawable {
@ -23,7 +24,7 @@ public class GifAwareAsyncDrawable extends AsyncDrawable {
public GifAwareAsyncDrawable( public GifAwareAsyncDrawable(
@NonNull Drawable gifPlaceholder, @NonNull Drawable gifPlaceholder,
@NonNull String destination, @NonNull String destination,
@NonNull Loader loader, @NonNull AsyncDrawableLoader loader,
@Nullable ImageSizeResolver imageSizeResolver, @Nullable ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize) { @Nullable ImageSize imageSize) {
super(destination, loader, imageSizeResolver, imageSize); super(destination, loader, imageSizeResolver, imageSize);

View File

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

View File

@ -3,9 +3,10 @@ package ru.noties.markwon;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver; 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.AsyncDrawableSpan;
import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.MarkwonTheme;
@ -19,7 +20,7 @@ public class GifAwareSpannableFactory extends SpannableFactoryDef {
@Nullable @Nullable
@Override @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( return new AsyncDrawableSpan(
theme, theme,
new GifAwareAsyncDrawable( new GifAwareAsyncDrawable(

View File

@ -29,9 +29,9 @@ public class MainActivity extends Activity {
@Inject @Inject
UriProcessor uriProcessor; UriProcessor uriProcessor;
//
@Inject // @Inject
GifProcessor gifProcessor; // GifProcessor gifProcessor;
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
@ -66,26 +66,14 @@ public class MainActivity extends Activity {
appBarRenderer.render(appBarState()); 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() { markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
@Override @Override
public void apply(final String text) { public void apply(final String text) {
markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() { markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() {
@Override @Override
public void onMarkdownReady(CharSequence markdown) { public void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown) {
Markwon.setText(textView, markdown); markwon2.setParsedMarkdown(textView, markdown);
gifProcessor.process(textView);
Views.setVisible(progress, false); Views.setVisible(progress, false);
} }

View File

@ -13,24 +13,23 @@ import java.util.concurrent.Future;
import javax.inject.Inject; import javax.inject.Inject;
import ru.noties.debug.Debug; import ru.noties.debug.Debug;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.syntax.Prism4jSyntaxHighlight; 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.Prism4jTheme;
import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDarkula;
import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.markwon.syntax.Prism4jThemeDefault;
import ru.noties.markwon.syntax.SyntaxHighlightPlugin;
import ru.noties.prism4j.Prism4j; import ru.noties.prism4j.Prism4j;
@ActivityScope @ActivityScope
public class MarkdownRenderer { public class MarkdownRenderer {
interface MarkdownReadyListener { interface MarkdownReadyListener {
void onMarkdownReady(CharSequence markdown); void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown);
} }
@Inject
AsyncDrawable.Loader loader;
@Inject @Inject
ExecutorService service; ExecutorService service;
@ -78,40 +77,39 @@ public class MarkdownRenderer {
? prism4jThemeDefault ? prism4jThemeDefault
: prism4JThemeDarkula; : prism4JThemeDarkula;
final int background = isLightTheme // final int background = isLightTheme
? prism4jTheme.background() // ? prism4jTheme.background()
: 0x0Fffffff; // : 0x0Fffffff;
final GifPlaceholder gifPlaceholder = new GifPlaceholder( final Markwon2 markwon2 = Markwon2.builder(context)
context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), .use(CorePlugin.create())
0x20000000 .use(ImagesPlugin.createWithAssets(context))
); .use(SvgPlugin.create(context.getResources()))
.use(GifPlugin.create(false))
final MarkwonConfiguration configuration = MarkwonConfiguration.builder(context) .use(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
.asyncDrawableLoader(loader) .use(GifAwarePlugin.create(context))
.urlProcessor(urlProcessor) .use(new AbstractMarkwonPlugin() {
.syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme)) @Override
.theme(MarkwonTheme.builderWithDefaults(context) public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
.codeBackgroundColor(background) builder.urlProcessor(urlProcessor);
.codeTextColor(prism4jTheme.textColor()) }
.build()) })
.factory(new GifAwareSpannableFactory(gifPlaceholder))
.build(); .build();
final long start = SystemClock.uptimeMillis(); final long start = SystemClock.uptimeMillis();
final CharSequence text = Markwon.markdown(configuration, markdown); final CharSequence text = markwon2.toMarkdown(markdown);
final long end = SystemClock.uptimeMillis(); final long end = SystemClock.uptimeMillis();
Debug.i("toMarkdown rendered: %d ms", end - start); Debug.i("markdown rendered: %d ms", end - start);
if (!isCancelled()) { if (!isCancelled()) {
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
if (!isCancelled()) { if (!isCancelled()) {
listener.onMarkdownReady(text); listener.onMarkdownReady(markwon2, text);
task = null; task = null;
} }
} }

View File

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

View File

@ -0,0 +1 @@
<manifest package="ru.noties.markwon.image.gif" />

View File

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

View File

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

View File

@ -20,7 +20,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.image.AsyncDrawable;
public class AsyncDrawableLoader implements AsyncDrawable.Loader { public class AsyncDrawableLoader implements AsyncDrawable.Loader {

View File

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

View File

@ -0,0 +1 @@
<manifest package="ru.noties.markwon.image.svg" />

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package ru.noties.markwon.syntax; package ru.noties.markwon.syntax;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
@ -12,12 +13,23 @@ public class Prism4jThemeDarkula extends Prism4jThemeBase {
@NonNull @NonNull
public static Prism4jThemeDarkula create() { 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 @Override
public int background() { public int background() {
return 0xFF2d2d2d; return background;
} }
@Override @Override

View File

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

View File

@ -5,6 +5,7 @@ import android.widget.TextView;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.MarkwonTheme;
public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { 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 @Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
@ -40,7 +46,7 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
} }
@Override @Override
public void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { public void afterSetText(@NonNull TextView textView) {
} }
} }

View File

@ -14,8 +14,11 @@ import org.commonmark.parser.Parser;
import java.util.Arrays; 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.renderer.SpannableRenderer;
import ru.noties.markwon.spans.OrderedListItemSpan; import ru.noties.markwon.spans.OrderedListItemSpan;
import ru.noties.markwon.table.TableRowSpan;
import ru.noties.markwon.tasklist.TaskListExtension; import ru.noties.markwon.tasklist.TaskListExtension;
@SuppressWarnings({"WeakerAccess", "unused"}) @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 * 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} * 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); * 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 * in order to avoid keeping drawables in memory after they have been removed from layout
* *
* @param view a {@link TextView} * @param view a {@link TextView}
* @see ru.noties.markwon.spans.AsyncDrawable * @see AsyncDrawable
* @see ru.noties.markwon.spans.AsyncDrawableSpan * @see ru.noties.markwon.spans.AsyncDrawableSpan
* @see DrawablesScheduler#schedule(TextView) * @see DrawablesScheduler#schedule(TextView)
* @see DrawablesScheduler#unschedule(TextView) * @see DrawablesScheduler#unschedule(TextView)
* @since 1.0.0 * @since 1.0.0
*/ */
public static void scheduleDrawables(@NonNull TextView view) { 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 * @since 1.0.0
*/ */
public static void unscheduleDrawables(@NonNull TextView view) { 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 * 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 * 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 * 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). * `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 * Precede this call with {@link #unscheduleTableRows(TextView)} in order to
* de-reference previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}&#39;s * de-reference previously scheduled {@link TableRowSpan}&#39;s
* *
* @param view a {@link TextView} * @param view a {@link TextView}
* @see #unscheduleTableRows(TextView) * @see #unscheduleTableRows(TextView)
* @since 1.0.0 * @since 1.0.0
*/ */
public static void scheduleTableRows(@NonNull TextView view) { public static void scheduleTableRows(@NonNull TextView view) {
TableRowsScheduler.schedule(view); // TableRowsScheduler.schedule(view);
} }
/** /**
* De-references previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}&#39;s * De-references previously scheduled {@link TableRowSpan}&#39;s
* *
* @param view a {@link TextView} * @param view a {@link TextView}
* @see #scheduleTableRows(TextView) * @see #scheduleTableRows(TextView)
* @since 1.0.0 * @since 1.0.0
*/ */
public static void unscheduleTableRows(@NonNull TextView view) { public static void unscheduleTableRows(@NonNull TextView view) {
TableRowsScheduler.unschedule(view); // TableRowsScheduler.unschedule(view);
} }
private Markwon() { private Markwon() {

View File

@ -9,6 +9,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.MarkwonTheme;
class MarkwonBuilderImpl implements Markwon2.Builder { class MarkwonBuilderImpl implements Markwon2.Builder {
@ -34,19 +35,25 @@ class MarkwonBuilderImpl implements Markwon2.Builder {
final Parser.Builder parserBuilder = new Parser.Builder(); final Parser.Builder parserBuilder = new Parser.Builder();
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(context); final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(context);
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
for (MarkwonPlugin plugin : plugins) { for (MarkwonPlugin plugin : plugins) {
plugin.configureParser(parserBuilder); plugin.configureParser(parserBuilder);
plugin.configureTheme(themeBuilder); plugin.configureTheme(themeBuilder);
plugin.configureImages(asyncDrawableLoaderBuilder);
plugin.configureConfiguration(configurationBuilder); plugin.configureConfiguration(configurationBuilder);
plugin.configureVisitor(visitorBuilder); plugin.configureVisitor(visitorBuilder);
} }
final MarkwonConfiguration configuration = configurationBuilder.build(
themeBuilder.build(),
asyncDrawableLoaderBuilder.build());
return new MarkwonImpl( return new MarkwonImpl(
parserBuilder.build(), parserBuilder.build(),
visitorBuilder.build(configurationBuilder.build(themeBuilder.build())), visitorBuilder.build(configuration),
Collections.unmodifiableList(plugins) Collections.unmodifiableList(plugins)
); );
} }

View File

@ -4,10 +4,11 @@ import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import ru.noties.markwon.html.api.MarkwonHtmlParser; 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.ImageSizeResolver;
import ru.noties.markwon.renderer.ImageSizeResolverDef; import ru.noties.markwon.renderer.ImageSizeResolverDef;
import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.MarkwonTheme;
@ -20,7 +21,7 @@ public class MarkwonConfiguration {
// creates default configuration // creates default configuration
@NonNull @NonNull
public static MarkwonConfiguration create(@NonNull Context context) { 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 @NonNull
@ -30,7 +31,7 @@ public class MarkwonConfiguration {
private final MarkwonTheme theme; private final MarkwonTheme theme;
private final AsyncDrawable.Loader asyncDrawableLoader; private final AsyncDrawableLoader asyncDrawableLoader;
private final SyntaxHighlight syntaxHighlight; private final SyntaxHighlight syntaxHighlight;
private final LinkSpan.Resolver linkResolver; private final LinkSpan.Resolver linkResolver;
private final UrlProcessor urlProcessor; private final UrlProcessor urlProcessor;
@ -69,7 +70,7 @@ public class MarkwonConfiguration {
} }
@NonNull @NonNull
public AsyncDrawable.Loader asyncDrawableLoader() { public AsyncDrawableLoader asyncDrawableLoader() {
return asyncDrawableLoader; return asyncDrawableLoader;
} }
@ -136,7 +137,7 @@ public class MarkwonConfiguration {
private final Context context; private final Context context;
private MarkwonTheme theme; private MarkwonTheme theme;
private AsyncDrawable.Loader asyncDrawableLoader; private AsyncDrawableLoader asyncDrawableLoader;
private SyntaxHighlight syntaxHighlight; private SyntaxHighlight syntaxHighlight;
private LinkSpan.Resolver linkResolver; private LinkSpan.Resolver linkResolver;
private UrlProcessor urlProcessor; private UrlProcessor urlProcessor;
@ -166,19 +167,6 @@ public class MarkwonConfiguration {
this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; 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 @NonNull
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
this.syntaxHighlight = syntaxHighlight; this.syntaxHighlight = syntaxHighlight;
@ -260,13 +248,10 @@ public class MarkwonConfiguration {
} }
@NonNull @NonNull
public MarkwonConfiguration build(@NonNull MarkwonTheme theme) { public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) {
this.theme = theme; this.theme = theme;
this.asyncDrawableLoader = asyncDrawableLoader;
if (asyncDrawableLoader == null) {
asyncDrawableLoader = new AsyncDrawableLoaderNoOp();
}
if (syntaxHighlight == null) { if (syntaxHighlight == null) {
syntaxHighlight = new SyntaxHighlightNoOp(); syntaxHighlight = new SyntaxHighlightNoOp();

View File

@ -62,7 +62,7 @@ class MarkwonImpl extends Markwon2 {
textView.setText(markdown); textView.setText(markdown);
for (MarkwonPlugin plugin : plugins) { for (MarkwonPlugin plugin : plugins) {
plugin.afterSetText(textView, markdown); plugin.afterSetText(textView);
} }
} }
} }

View File

@ -5,6 +5,7 @@ import android.widget.TextView;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.MarkwonTheme;
public interface MarkwonPlugin { public interface MarkwonPlugin {
@ -13,11 +14,12 @@ public interface MarkwonPlugin {
void configureTheme(@NonNull MarkwonTheme.Builder builder); void configureTheme(@NonNull MarkwonTheme.Builder builder);
void configureImages(@NonNull AsyncDrawableLoader.Builder builder);
void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder); void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder);
void configureVisitor(@NonNull MarkwonVisitor.Builder builder); void configureVisitor(@NonNull MarkwonVisitor.Builder builder);
// images
// html // html
@NonNull @NonNull
@ -25,5 +27,8 @@ public interface MarkwonPlugin {
void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown); 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);
} }

View File

@ -17,7 +17,7 @@ public interface MarkwonVisitor extends Visitor {
interface Builder { interface Builder {
@NonNull @NonNull
<N extends Node> Builder on(@NonNull Class<N> node, @NonNull NodeVisitor<N> nodeVisitor); <N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<N> nodeVisitor);
@NonNull @NonNull
MarkwonVisitor build(@NonNull MarkwonConfiguration configuration); MarkwonVisitor build(@NonNull MarkwonConfiguration configuration);

View File

@ -281,8 +281,14 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
@NonNull @NonNull
@Override @Override
public <N extends Node> Builder on(@NonNull Class<N> node, @NonNull NodeVisitor<N> nodeVisitor) { public <N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<N> nodeVisitor) {
// we should allow `null` to exclude node from being visited (for example to disable
// some functionality)
if (nodeVisitor == null) {
nodes.remove(node);
} else {
nodes.put(node, nodeVisitor); nodes.put(node, nodeVisitor);
}
return this; return this;
} }

View File

@ -3,14 +3,11 @@ package ru.noties.markwon;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; 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.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.MarkwonTheme; 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 * Each method can return null or a Span object or an array of spans
@ -46,16 +43,6 @@ public interface SpannableFactory {
@Nullable @Nullable
Object strikethrough(); Object strikethrough();
@Nullable
Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone);
@Nullable
Object tableRow(
@NonNull MarkwonTheme theme,
@NonNull List<TableRowSpan.Cell> cells,
boolean isHeader,
boolean isOdd);
/** /**
* @since 1.1.1 * @since 1.1.1
*/ */
@ -66,7 +53,7 @@ public interface SpannableFactory {
Object image( Object image(
@NonNull MarkwonTheme theme, @NonNull MarkwonTheme theme,
@NonNull String destination, @NonNull String destination,
@NonNull AsyncDrawable.Loader loader, @NonNull AsyncDrawableLoader loader,
@NonNull ImageSizeResolver imageSizeResolver, @NonNull ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize, @Nullable ImageSize imageSize,
boolean replacementTextIsLink); boolean replacementTextIsLink);

View File

@ -5,11 +5,10 @@ import android.support.annotation.Nullable;
import android.text.style.StrikethroughSpan; import android.text.style.StrikethroughSpan;
import android.text.style.UnderlineSpan; 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.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.AsyncDrawableSpan; import ru.noties.markwon.spans.AsyncDrawableSpan;
import ru.noties.markwon.spans.BlockQuoteSpan; import ru.noties.markwon.spans.BlockQuoteSpan;
import ru.noties.markwon.spans.BulletListItemSpan; 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.EmphasisSpan;
import ru.noties.markwon.spans.HeadingSpan; import ru.noties.markwon.spans.HeadingSpan;
import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.OrderedListItemSpan;
import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.MarkwonTheme;
import ru.noties.markwon.spans.OrderedListItemSpan;
import ru.noties.markwon.spans.StrongEmphasisSpan; import ru.noties.markwon.spans.StrongEmphasisSpan;
import ru.noties.markwon.spans.SubScriptSpan; import ru.noties.markwon.spans.SubScriptSpan;
import ru.noties.markwon.spans.SuperScriptSpan; import ru.noties.markwon.spans.SuperScriptSpan;
import ru.noties.markwon.spans.TableRowSpan;
import ru.noties.markwon.tasklist.TaskListSpan;
import ru.noties.markwon.spans.ThematicBreakSpan; import ru.noties.markwon.spans.ThematicBreakSpan;
/** /**
@ -91,18 +88,6 @@ public class SpannableFactoryDef implements SpannableFactory {
return new StrikethroughSpan(); 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<TableRowSpan.Cell> cells, boolean isHeader, boolean isOdd) {
return new TableRowSpan(theme, cells, isHeader, isOdd);
}
/** /**
* @since 1.1.1 * @since 1.1.1
*/ */
@ -114,7 +99,7 @@ public class SpannableFactoryDef implements SpannableFactory {
@Nullable @Nullable
@Override @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( return new AsyncDrawableSpan(
theme, theme,
new AsyncDrawable( new AsyncDrawable(

View File

@ -1,4 +1,4 @@
package ru.noties.markwon; package ru.noties.markwon.core;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@ -15,10 +15,10 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import ru.noties.markwon.renderer.R; import ru.noties.markwon.renderer.R;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.image.AsyncDrawable;
import ru.noties.markwon.spans.AsyncDrawableSpan; import ru.noties.markwon.spans.AsyncDrawableSpan;
abstract class DrawablesScheduler { abstract class AsyncDrawableScheduler {
static void schedule(@NonNull final TextView textView) { static void schedule(@NonNull final TextView textView) {
@ -104,7 +104,7 @@ abstract class DrawablesScheduler {
return list; return list;
} }
private DrawablesScheduler() { private AsyncDrawableScheduler() {
} }
private static class DrawableCallbackImpl implements Drawable.Callback { private static class DrawableCallbackImpl implements Drawable.Callback {

View File

@ -11,7 +11,6 @@ import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak; import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading; import org.commonmark.node.Heading;
import org.commonmark.node.Image;
import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.IndentedCodeBlock;
import org.commonmark.node.Link; import org.commonmark.node.Link;
import org.commonmark.node.ListBlock; import org.commonmark.node.ListBlock;
@ -73,13 +72,19 @@ public class CorePlugin extends AbstractMarkwonPlugin {
softLineBreak(builder); softLineBreak(builder);
hardLineBreak(builder); hardLineBreak(builder);
paragraph(builder); paragraph(builder);
image(builder); // image(builder);
link(builder); link(builder);
} }
@Override @Override
public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) {
OrderedListItemSpan.measure(textView, 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) { 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<Image>() {
@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) { protected void link(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Link.class, new MarkwonVisitor.NodeVisitor<Link>() { builder.on(Link.class, new MarkwonVisitor.NodeVisitor<Link>() {
@Override @Override

View File

@ -1,4 +1,4 @@
package ru.noties.markwon.spans; package ru.noties.markwon.image;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.ColorFilter; import android.graphics.ColorFilter;
@ -15,15 +15,8 @@ import ru.noties.markwon.renderer.ImageSizeResolver;
public class AsyncDrawable extends Drawable { 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 String destination;
private final Loader loader; private final AsyncDrawableLoader loader;
private final ImageSize imageSize; private final ImageSize imageSize;
private final ImageSizeResolver imageSizeResolver; private final ImageSizeResolver imageSizeResolver;
@ -38,7 +31,7 @@ public class AsyncDrawable extends Drawable {
*/ */
public AsyncDrawable( public AsyncDrawable(
@NonNull String destination, @NonNull String destination,
@NonNull Loader loader, @NonNull AsyncDrawableLoader loader,
@Nullable ImageSizeResolver imageSizeResolver, @Nullable ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize @Nullable ImageSize imageSize
) { ) {

View File

@ -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<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
final Map<String, MediaDecoder> 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<String> 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<String> 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);
}
}
}

View File

@ -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<String, SchemeHandler> schemeHandlers;
private final Map<String, MediaDecoder> mediaDecoders;
private final MediaDecoder defaultMediaDecoder;
private final Drawable errorDrawable;
private final Handler mainThread;
private final Map<String, Future<?>> 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<AsyncDrawable> reference = new WeakReference<AsyncDrawable>(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);
}
}
});
}
}

View File

@ -1,10 +1,8 @@
package ru.noties.markwon; package ru.noties.markwon.image;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import ru.noties.markwon.spans.AsyncDrawable; public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader {
class AsyncDrawableLoaderNoOp implements AsyncDrawable.Loader {
@Override @Override
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,7 @@ import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.spans.MarkwonTheme; 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.TaskListBlock;
import ru.noties.markwon.tasklist.TaskListItem; import ru.noties.markwon.tasklist.TaskListItem;
@ -321,112 +321,92 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(customNode); visitChildren(customNode);
setSpan(length, factory.strikethrough()); setSpan(length, factory.strikethrough());
} else if (customNode instanceof TaskListItem) { } else {
// 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)) {
super.visit(customNode); super.visit(customNode);
} }
} }
private boolean handleTableNodes(CustomNode node) { // private boolean handleTableNodes(CustomNode node) {
//
final boolean handled; // final boolean handled;
//
if (node instanceof TableBody) { // if (node instanceof TableBody) {
//
visitChildren(node); // visitChildren(node);
tableRows = 0; // tableRows = 0;
handled = true; // handled = true;
//
if (hasNext(node)) { // if (hasNext(node)) {
newLine(); // newLine();
builder.append('\n'); // builder.append('\n');
} // }
//
} else if (node instanceof TableRow || node instanceof TableHead) { // } else if (node instanceof TableRow || node instanceof TableHead) {
//
final int length = builder.length(); // final int length = builder.length();
//
visitChildren(node); // visitChildren(node);
//
if (pendingTableRow != null) { // if (pendingTableRow != null) {
//
// @since 2.0.0 // // @since 2.0.0
// we cannot rely on hasNext(TableHead) as it's not reliable // // we cannot rely on hasNext(TableHead) as it's not reliable
// we must apply new line manually and then exclude it from tableRow span // // we must apply new line manually and then exclude it from tableRow span
final boolean addNewLine; // final boolean addNewLine;
{ // {
final int builderLength = builder.length(); // final int builderLength = builder.length();
addNewLine = builderLength > 0 // addNewLine = builderLength > 0
&& '\n' != builder.charAt(builderLength - 1); // && '\n' != builder.charAt(builderLength - 1);
} // }
if (addNewLine) { // if (addNewLine) {
builder.append('\n'); // builder.append('\n');
} // }
//
// @since 1.0.4 Replace table char with non-breakable space // // @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 // // we need this because if table is at the end of the text, then it will be
// trimmed from the final result // // trimmed from the final result
builder.append('\u00a0'); // builder.append('\u00a0');
//
final Object span = factory.tableRow( // final Object span = factory.tableRow(
theme, // theme,
pendingTableRow, // pendingTableRow,
tableRowIsHeader, // tableRowIsHeader,
tableRows % 2 == 1); // tableRows % 2 == 1);
//
tableRows = tableRowIsHeader // tableRows = tableRowIsHeader
? 0 // ? 0
: tableRows + 1; // : tableRows + 1;
//
setSpan(addNewLine ? length + 1 : length, span); // setSpan(addNewLine ? length + 1 : length, span);
//
pendingTableRow = null; // pendingTableRow = null;
} // }
//
handled = true; // handled = true;
//
} else if (node instanceof TableCell) { // } else if (node instanceof TableCell) {
//
final TableCell cell = (TableCell) node; // final TableCell cell = (TableCell) node;
final int length = builder.length(); // final int length = builder.length();
visitChildren(cell); // visitChildren(cell);
if (pendingTableRow == null) { // if (pendingTableRow == null) {
pendingTableRow = new ArrayList<>(2); // pendingTableRow = new ArrayList<>(2);
} // }
//
pendingTableRow.add(new TableRowSpan.Cell( // pendingTableRow.add(new TableRowSpan.Cell(
tableCellAlignment(cell.getAlignment()), // tableCellAlignment(cell.getAlignment()),
builder.removeFromEnd(length) // builder.removeFromEnd(length)
)); // ));
//
tableRowIsHeader = cell.isHeader(); // tableRowIsHeader = cell.isHeader();
//
handled = true; // handled = true;
} else { // } else {
handled = false; // handled = false;
} // }
//
return handled; // return handled;
} // }
@Override @Override
public void visit(Paragraph paragraph) { public void visit(Paragraph paragraph) {
@ -530,26 +510,26 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
return false; return false;
} }
@TableRowSpan.Alignment // @TableRowSpan.Alignment
private static int tableCellAlignment(TableCell.Alignment alignment) { // private static int tableCellAlignment(TableCell.Alignment alignment) {
final int out; // final int out;
if (alignment != null) { // if (alignment != null) {
switch (alignment) { // switch (alignment) {
case CENTER: // case CENTER:
out = TableRowSpan.ALIGN_CENTER; // out = TableRowSpan.ALIGN_CENTER;
break; // break;
case RIGHT: // case RIGHT:
out = TableRowSpan.ALIGN_RIGHT; // out = TableRowSpan.ALIGN_RIGHT;
break; // break;
default: // default:
out = TableRowSpan.ALIGN_LEFT; // out = TableRowSpan.ALIGN_LEFT;
break; // break;
} // }
} else { // } else {
out = TableRowSpan.ALIGN_LEFT; // out = TableRowSpan.ALIGN_LEFT;
} // }
return out; // return out;
} // }
/** /**
* @since 2.0.0 * @since 2.0.0

View File

@ -12,6 +12,8 @@ import android.text.style.ReplacementSpan;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import ru.noties.markwon.image.AsyncDrawable;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class AsyncDrawableSpan extends ReplacementSpan { public class AsyncDrawableSpan extends ReplacementSpan {
@ -29,16 +31,16 @@ public class AsyncDrawableSpan extends ReplacementSpan {
private final int alignment; private final int alignment;
private final boolean replacementTextIsLink; private final boolean replacementTextIsLink;
public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) { // public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) {
this(theme, drawable, ALIGN_BOTTOM); // this(theme, drawable, ALIGN_BOTTOM);
} // }
public AsyncDrawableSpan( // public AsyncDrawableSpan(
@NonNull MarkwonTheme theme, // @NonNull MarkwonTheme theme,
@NonNull AsyncDrawable drawable, // @NonNull AsyncDrawable drawable,
@Alignment int alignment) { // @Alignment int alignment) {
this(theme, drawable, alignment, false); // this(theme, drawable, alignment, false);
} // }
public AsyncDrawableSpan( public AsyncDrawableSpan(
@NonNull MarkwonTheme theme, @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? // will it make sense to have additional background/borders for an image replacement?
// let's focus on main functionality and then think of it // 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) { if (replacementTextIsLink) {
theme.applyLinkStyle(paint); theme.applyLinkStyle(paint);
} }
@ -150,4 +152,9 @@ public class AsyncDrawableSpan extends ReplacementSpan {
public AsyncDrawable getDrawable() { public AsyncDrawable getDrawable() {
return drawable; 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));
}
} }

View File

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

View File

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

View File

@ -1,25 +1,22 @@
package ru.noties.markwon.spans; package ru.noties.markwon.spans;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.AttrRes;
import android.support.annotation.ColorInt; import android.support.annotation.ColorInt;
import android.support.annotation.FloatRange; import android.support.annotation.FloatRange;
import android.support.annotation.IntRange; import android.support.annotation.IntRange;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Px; import android.support.annotation.Px;
import android.support.annotation.Size; import android.support.annotation.Size;
import android.text.TextPaint; import android.text.TextPaint;
import android.util.TypedValue;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import ru.noties.markwon.tasklist.TaskListDrawable; import ru.noties.markwon.utils.ColorUtils;
import ru.noties.markwon.utils.Dip;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class MarkwonTheme { public class MarkwonTheme {
@ -77,36 +74,14 @@ public class MarkwonTheme {
@NonNull @NonNull
public static Builder builderWithDefaults(@NonNull Context context) { public static Builder builderWithDefaults(@NonNull Context context) {
// by default we will be using link color for the checkbox color final Dip dip = Dip.create(context);
// & 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);
return new Builder() return new Builder()
.codeMultilineMargin(dip.toPx(8)) .codeMultilineMargin(dip.toPx(8))
.blockMargin(dip.toPx(24)) .blockMargin(dip.toPx(24))
.blockQuoteWidth(dip.toPx(4)) .blockQuoteWidth(dip.toPx(4))
.bulletListItemStrokeWidth(dip.toPx(1)) .bulletListItemStrokeWidth(dip.toPx(1))
.headingBreakHeight(dip.toPx(1)) .headingBreakHeight(dip.toPx(1))
.thematicBreakHeight(dip.toPx(4)) .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();
}
} }
protected static final int BLOCK_QUOTE_DEF_COLOR_ALPHA = 25; 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 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; protected final int linkColor;
// used in quote, lists // used in quote, lists
@ -197,30 +168,6 @@ public class MarkwonTheme {
// by default paint.strokeWidth // by default paint.strokeWidth
protected final int thematicBreakHeight; 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) { protected MarkwonTheme(@NonNull Builder builder) {
this.linkColor = builder.linkColor; this.linkColor = builder.linkColor;
this.blockMargin = builder.blockMargin; this.blockMargin = builder.blockMargin;
@ -243,13 +190,6 @@ public class MarkwonTheme {
this.scriptTextSizeRatio = builder.scriptTextSizeRatio; this.scriptTextSizeRatio = builder.scriptTextSizeRatio;
this.thematicBreakColor = builder.thematicBreakColor; this.thematicBreakColor = builder.thematicBreakColor;
this.thematicBreakHeight = builder.thematicBreakHeight; 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") @SuppressWarnings("unused")
public static class Builder { public static class Builder {
@ -557,13 +432,6 @@ public class MarkwonTheme {
private float scriptTextSizeRatio; private float scriptTextSizeRatio;
private int thematicBreakColor; private int thematicBreakColor;
private int thematicBreakHeight = -1; 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() { Builder() {
} }
@ -590,11 +458,6 @@ public class MarkwonTheme {
this.scriptTextSizeRatio = theme.scriptTextSizeRatio; this.scriptTextSizeRatio = theme.scriptTextSizeRatio;
this.thematicBreakColor = theme.thematicBreakColor; this.thematicBreakColor = theme.thematicBreakColor;
this.thematicBreakHeight = theme.thematicBreakHeight; 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 @NonNull
@ -742,81 +605,10 @@ public class MarkwonTheme {
return this; 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 @NonNull
public MarkwonTheme build() { public MarkwonTheme build() {
return new MarkwonTheme(this); 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);
}
}
} }

View File

@ -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<TableRowSpan.Cell> 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<TableBody>() {
@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<TableRow>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableRow tableRow) {
visitRow(visitor, tableRow);
}
})
.on(TableHead.class, new MarkwonVisitor.NodeVisitor<TableHead>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableHead tableHead) {
visitRow(visitor, tableHead);
}
})
.on(TableCell.class, new MarkwonVisitor.NodeVisitor<TableCell>() {
@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;
}
}
}

View File

@ -1,4 +1,4 @@
package ru.noties.markwon.spans; package ru.noties.markwon.table;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.graphics.Canvas; import android.graphics.Canvas;
@ -61,22 +61,22 @@ public class TableRowSpan extends ReplacementSpan {
} }
} }
private final MarkwonTheme theme; private final TableTheme theme;
private final List<Cell> cells; private final List<Cell> cells;
private final List<StaticLayout> layouts; private final List<StaticLayout> layouts;
private final TextPaint textPaint; private final TextPaint textPaint;
private final boolean header; private final boolean header;
private final boolean odd; private final boolean odd;
private final Rect rect = ObjectsPool.rect(); private final Rect rect = new Rect();
private final Paint paint = ObjectsPool.paint(); private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int width; private int width;
private int height; private int height;
private Invalidator invalidator; private Invalidator invalidator;
public TableRowSpan( public TableRowSpan(
@NonNull MarkwonTheme theme, @NonNull TableTheme theme,
@NonNull List<Cell> cells, @NonNull List<Cell> cells,
boolean header, boolean header,
boolean odd) { boolean odd) {
@ -272,8 +272,7 @@ public class TableRowSpan extends ReplacementSpan {
return out; return out;
} }
public TableRowSpan invalidator(Invalidator invalidator) { public void invalidator(@Nullable Invalidator invalidator) {
this.invalidator = invalidator; this.invalidator = invalidator;
return this;
} }
} }

View File

@ -1,13 +1,13 @@
package ru.noties.markwon; package ru.noties.markwon.table;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import ru.noties.markwon.renderer.R; import ru.noties.markwon.renderer.R;
import ru.noties.markwon.spans.TableRowSpan;
abstract class TableRowsScheduler { abstract class TableRowsScheduler {
@ -57,6 +57,7 @@ abstract class TableRowsScheduler {
} }
} }
@Nullable
private static Object[] extract(@NonNull TextView view) { private static Object[] extract(@NonNull TextView view) {
final Object[] out; final Object[] out;
final CharSequence text = view.getText(); final CharSequence text = view.getText();

View File

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

View File

@ -1,9 +1,12 @@
package ru.noties.markwon.tasklist; package ru.noties.markwon.tasklist;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.AttrRes;
import android.support.annotation.ColorInt; import android.support.annotation.ColorInt;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.TypedValue;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
@ -13,6 +16,11 @@ import ru.noties.markwon.MarkwonVisitor;
public class TaskListPlugin extends AbstractMarkwonPlugin { 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 * @see TaskListDrawable
*/ */
@NonNull @NonNull
@ -22,8 +30,13 @@ public class TaskListPlugin extends AbstractMarkwonPlugin {
@NonNull @NonNull
public static TaskListPlugin create(@NonNull Context context) { 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 @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();
}
}
} }

View File

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

View File

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

View File

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

View File

@ -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<String, DataUri> data = new LinkedHashMap<String, DataUri>() {{
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<String, DataUri> 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)
);
}
}

View File

@ -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<String, Item> expected = new HashMap<String, Item>() {{
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<String, Item> 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<String, Item> expected = new HashMap<String, Item>() {{
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<String, Item> 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);
}
}
}

View File

@ -8,7 +8,7 @@ import ru.noties.markwon.SyntaxHighlight;
import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.UrlProcessor;
import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; 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.LinkSpan;
import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.MarkwonTheme;

View File

@ -30,7 +30,7 @@ import java.util.Set;
import ix.Ix; import ix.Ix;
import ix.IxFunction; import ix.IxFunction;
import ix.IxPredicate; 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.BLOCK_QUOTE;
import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST;

View File

@ -11,10 +11,10 @@ import java.util.Map;
import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver; 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.LinkSpan;
import ru.noties.markwon.spans.MarkwonTheme; 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.BLOCK_QUOTE;
import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST;

View File

@ -18,6 +18,6 @@ android {
dependencies { dependencies {
implementation project(':markwon') implementation project(':markwon')
implementation project(':markwon-image-loader') // implementation project(':markwon-image-loader')
implementation 'ru.noties:jlatexmath-android:0.1.0' implementation 'ru.noties:jlatexmath-android:0.1.0'
} }

View File

@ -1,3 +1,3 @@
rootProject.name = 'MarkwonProject' 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' ':markwon-syntax-highlight', ':markwon-html-parser-api', ':markwon-html-parser-impl'