Syntax highlight plugin, image modules (svg, gif), working sample application
This commit is contained in:
parent
3526e16565
commit
2efd12f020
@ -29,7 +29,8 @@ android {
|
||||
dependencies {
|
||||
|
||||
implementation project(':markwon')
|
||||
implementation project(':markwon-image-loader')
|
||||
implementation project(':markwon-image-gif')
|
||||
implementation project(':markwon-image-svg')
|
||||
implementation project(':markwon-syntax-highlight')
|
||||
|
||||
deps.with {
|
||||
|
@ -14,11 +14,6 @@ import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.OkHttpClient;
|
||||
import ru.noties.markwon.il.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.il.GifMediaDecoder;
|
||||
import ru.noties.markwon.il.ImageMediaDecoder;
|
||||
import ru.noties.markwon.il.SvgMediaDecoder;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.syntax.Prism4jThemeDarkula;
|
||||
import ru.noties.markwon.syntax.Prism4jThemeDefault;
|
||||
import ru.noties.prism4j.Prism4j;
|
||||
@ -72,23 +67,6 @@ class AppModule {
|
||||
return new UriProcessorImpl();
|
||||
}
|
||||
|
||||
@Provides
|
||||
AsyncDrawable.Loader asyncDrawableLoader(
|
||||
OkHttpClient client,
|
||||
ExecutorService executorService,
|
||||
Resources resources) {
|
||||
return AsyncDrawableLoader.builder()
|
||||
.client(client)
|
||||
.executorService(executorService)
|
||||
.resources(resources)
|
||||
.mediaDecoders(
|
||||
SvgMediaDecoder.create(resources),
|
||||
GifMediaDecoder.create(false),
|
||||
ImageMediaDecoder.create(resources)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Prism4j prism4j() {
|
||||
@ -104,12 +82,12 @@ class AppModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
Prism4jThemeDarkula prism4jThemeDarkula() {
|
||||
return Prism4jThemeDarkula.create();
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
GifProcessor gifProcessor() {
|
||||
return GifProcessor.create();
|
||||
return Prism4jThemeDarkula.create(0x0Fffffff);
|
||||
}
|
||||
//
|
||||
// @Singleton
|
||||
// @Provides
|
||||
// GifProcessor gifProcessor() {
|
||||
// return GifProcessor.create();
|
||||
// }
|
||||
}
|
||||
|
@ -6,9 +6,10 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import pl.droidsonroids.gif.GifDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.renderer.ImageSize;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
|
||||
public class GifAwareAsyncDrawable extends AsyncDrawable {
|
||||
|
||||
@ -23,7 +24,7 @@ public class GifAwareAsyncDrawable extends AsyncDrawable {
|
||||
public GifAwareAsyncDrawable(
|
||||
@NonNull Drawable gifPlaceholder,
|
||||
@NonNull String destination,
|
||||
@NonNull Loader loader,
|
||||
@NonNull AsyncDrawableLoader loader,
|
||||
@Nullable ImageSizeResolver imageSizeResolver,
|
||||
@Nullable ImageSize imageSize) {
|
||||
super(destination, loader, imageSizeResolver, imageSize);
|
||||
|
35
app/src/main/java/ru/noties/markwon/GifAwarePlugin.java
Normal file
35
app/src/main/java/ru/noties/markwon/GifAwarePlugin.java
Normal 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);
|
||||
}
|
||||
}
|
@ -3,9 +3,10 @@ package ru.noties.markwon;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.renderer.ImageSize;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.AsyncDrawableSpan;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
|
||||
@ -19,7 +20,7 @@ public class GifAwareSpannableFactory extends SpannableFactoryDef {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
|
||||
public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
|
||||
return new AsyncDrawableSpan(
|
||||
theme,
|
||||
new GifAwareAsyncDrawable(
|
||||
|
@ -29,9 +29,9 @@ public class MainActivity extends Activity {
|
||||
|
||||
@Inject
|
||||
UriProcessor uriProcessor;
|
||||
|
||||
@Inject
|
||||
GifProcessor gifProcessor;
|
||||
//
|
||||
// @Inject
|
||||
// GifProcessor gifProcessor;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
@ -66,26 +66,14 @@ public class MainActivity extends Activity {
|
||||
|
||||
appBarRenderer.render(appBarState());
|
||||
|
||||
if (true) {
|
||||
final Markwon2 markwon2 = Markwon2.builder(this)
|
||||
.use(new CorePlugin())
|
||||
.use(TaskListPlugin.create(new TaskListDrawable(0xffff0000, 0xffff0000, -1)))
|
||||
.build();
|
||||
final CharSequence markdown = markwon2.toMarkdown("**hello _dear_** `code`\n\n- [ ] first\n- [x] second");
|
||||
textView.setText(markdown);
|
||||
return;
|
||||
}
|
||||
|
||||
markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
|
||||
@Override
|
||||
public void apply(final String text) {
|
||||
markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() {
|
||||
@Override
|
||||
public void onMarkdownReady(CharSequence markdown) {
|
||||
public void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown) {
|
||||
|
||||
Markwon.setText(textView, markdown);
|
||||
|
||||
gifProcessor.process(textView);
|
||||
markwon2.setParsedMarkdown(textView, markdown);
|
||||
|
||||
Views.setVisible(progress, false);
|
||||
}
|
||||
|
@ -13,24 +13,23 @@ import java.util.concurrent.Future;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import ru.noties.debug.Debug;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
import ru.noties.markwon.syntax.Prism4jSyntaxHighlight;
|
||||
import ru.noties.markwon.core.CorePlugin;
|
||||
import ru.noties.markwon.image.ImagesPlugin;
|
||||
import ru.noties.markwon.image.gif.GifPlugin;
|
||||
import ru.noties.markwon.image.svg.SvgPlugin;
|
||||
import ru.noties.markwon.syntax.Prism4jTheme;
|
||||
import ru.noties.markwon.syntax.Prism4jThemeDarkula;
|
||||
import ru.noties.markwon.syntax.Prism4jThemeDefault;
|
||||
import ru.noties.markwon.syntax.SyntaxHighlightPlugin;
|
||||
import ru.noties.prism4j.Prism4j;
|
||||
|
||||
@ActivityScope
|
||||
public class MarkdownRenderer {
|
||||
|
||||
interface MarkdownReadyListener {
|
||||
void onMarkdownReady(CharSequence markdown);
|
||||
void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown);
|
||||
}
|
||||
|
||||
@Inject
|
||||
AsyncDrawable.Loader loader;
|
||||
|
||||
@Inject
|
||||
ExecutorService service;
|
||||
|
||||
@ -78,40 +77,39 @@ public class MarkdownRenderer {
|
||||
? prism4jThemeDefault
|
||||
: prism4JThemeDarkula;
|
||||
|
||||
final int background = isLightTheme
|
||||
? prism4jTheme.background()
|
||||
: 0x0Fffffff;
|
||||
// final int background = isLightTheme
|
||||
// ? prism4jTheme.background()
|
||||
// : 0x0Fffffff;
|
||||
|
||||
final GifPlaceholder gifPlaceholder = new GifPlaceholder(
|
||||
context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white),
|
||||
0x20000000
|
||||
);
|
||||
|
||||
final MarkwonConfiguration configuration = MarkwonConfiguration.builder(context)
|
||||
.asyncDrawableLoader(loader)
|
||||
.urlProcessor(urlProcessor)
|
||||
.syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme))
|
||||
.theme(MarkwonTheme.builderWithDefaults(context)
|
||||
.codeBackgroundColor(background)
|
||||
.codeTextColor(prism4jTheme.textColor())
|
||||
.build())
|
||||
.factory(new GifAwareSpannableFactory(gifPlaceholder))
|
||||
final Markwon2 markwon2 = Markwon2.builder(context)
|
||||
.use(CorePlugin.create())
|
||||
.use(ImagesPlugin.createWithAssets(context))
|
||||
.use(SvgPlugin.create(context.getResources()))
|
||||
.use(GifPlugin.create(false))
|
||||
.use(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
|
||||
.use(GifAwarePlugin.create(context))
|
||||
.use(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.urlProcessor(urlProcessor);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
final long start = SystemClock.uptimeMillis();
|
||||
|
||||
final CharSequence text = Markwon.markdown(configuration, markdown);
|
||||
final CharSequence text = markwon2.toMarkdown(markdown);
|
||||
|
||||
final long end = SystemClock.uptimeMillis();
|
||||
|
||||
Debug.i("toMarkdown rendered: %d ms", end - start);
|
||||
Debug.i("markdown rendered: %d ms", end - start);
|
||||
|
||||
if (!isCancelled()) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isCancelled()) {
|
||||
listener.onMarkdownReady(text);
|
||||
listener.onMarkdownReady(markwon2, text);
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
|
25
markwon-image-gif/build.gradle
Normal file
25
markwon-image-gif/build.gradle
Normal 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)
|
1
markwon-image-gif/src/main/AndroidManifest.xml
Normal file
1
markwon-image-gif/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="ru.noties.markwon.image.gif" />
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
|
||||
public class AsyncDrawableLoader implements AsyncDrawable.Loader {
|
||||
|
||||
|
25
markwon-image-svg/build.gradle
Normal file
25
markwon-image-svg/build.gradle
Normal 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)
|
1
markwon-image-svg/src/main/AndroidManifest.xml
Normal file
1
markwon-image-svg/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="ru.noties.markwon.image.svg" />
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package ru.noties.markwon.syntax;
|
||||
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@ -12,12 +13,23 @@ public class Prism4jThemeDarkula extends Prism4jThemeBase {
|
||||
|
||||
@NonNull
|
||||
public static Prism4jThemeDarkula create() {
|
||||
return new Prism4jThemeDarkula();
|
||||
return new Prism4jThemeDarkula(0xFF2d2d2d);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Prism4jThemeDarkula create(@ColorInt int background) {
|
||||
return new Prism4jThemeDarkula(background);
|
||||
}
|
||||
|
||||
private final int background;
|
||||
|
||||
public Prism4jThemeDarkula(@ColorInt int background) {
|
||||
this.background = background;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int background() {
|
||||
return 0xFF2d2d2d;
|
||||
return background;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
|
||||
public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||
@ -18,6 +19,11 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
|
||||
@ -40,7 +46,7 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown) {
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,11 @@ import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
//import ru.noties.markwon.image.DrawablesScheduler;
|
||||
import ru.noties.markwon.renderer.SpannableRenderer;
|
||||
import ru.noties.markwon.spans.OrderedListItemSpan;
|
||||
import ru.noties.markwon.table.TableRowSpan;
|
||||
import ru.noties.markwon.tasklist.TaskListExtension;
|
||||
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
@ -148,7 +151,7 @@ public abstract class Markwon {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adds support for {@link ru.noties.markwon.spans.AsyncDrawable} to be used. As
|
||||
* This method adds support for {@link AsyncDrawable} to be used. As
|
||||
* textView seems not to support drawables that change bounds (and gives no means
|
||||
* to update the layout), we create own {@link android.graphics.drawable.Drawable.Callback}
|
||||
* and apply it. So, textView can display drawables, that are: async (loading from disk, network);
|
||||
@ -157,14 +160,14 @@ public abstract class Markwon {
|
||||
* in order to avoid keeping drawables in memory after they have been removed from layout
|
||||
*
|
||||
* @param view a {@link TextView}
|
||||
* @see ru.noties.markwon.spans.AsyncDrawable
|
||||
* @see AsyncDrawable
|
||||
* @see ru.noties.markwon.spans.AsyncDrawableSpan
|
||||
* @see DrawablesScheduler#schedule(TextView)
|
||||
* @see DrawablesScheduler#unschedule(TextView)
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static void scheduleDrawables(@NonNull TextView view) {
|
||||
DrawablesScheduler.schedule(view);
|
||||
// DrawablesScheduler.schedule(view);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,7 +178,7 @@ public abstract class Markwon {
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static void unscheduleDrawables(@NonNull TextView view) {
|
||||
DrawablesScheduler.unschedule(view);
|
||||
// DrawablesScheduler.unschedule(view);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,28 +188,28 @@ public abstract class Markwon {
|
||||
* to return `size` (width) of our replacement, but we are not provided
|
||||
* with the total one (canvas width). In order to correctly calculate height of our
|
||||
* table cell text, we must have available width first. This method gives
|
||||
* ability for {@link ru.noties.markwon.spans.TableRowSpan} to invalidate
|
||||
* ability for {@link TableRowSpan} to invalidate
|
||||
* `view` when it encounters such a situation (when available width is not known or have changed).
|
||||
* Precede this call with {@link #unscheduleTableRows(TextView)} in order to
|
||||
* de-reference previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s
|
||||
* de-reference previously scheduled {@link TableRowSpan}'s
|
||||
*
|
||||
* @param view a {@link TextView}
|
||||
* @see #unscheduleTableRows(TextView)
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static void scheduleTableRows(@NonNull TextView view) {
|
||||
TableRowsScheduler.schedule(view);
|
||||
// TableRowsScheduler.schedule(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* De-references previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s
|
||||
* De-references previously scheduled {@link TableRowSpan}'s
|
||||
*
|
||||
* @param view a {@link TextView}
|
||||
* @see #scheduleTableRows(TextView)
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static void unscheduleTableRows(@NonNull TextView view) {
|
||||
TableRowsScheduler.unschedule(view);
|
||||
// TableRowsScheduler.unschedule(view);
|
||||
}
|
||||
|
||||
private Markwon() {
|
||||
|
@ -9,6 +9,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
|
||||
class MarkwonBuilderImpl implements Markwon2.Builder {
|
||||
@ -34,19 +35,25 @@ class MarkwonBuilderImpl implements Markwon2.Builder {
|
||||
|
||||
final Parser.Builder parserBuilder = new Parser.Builder();
|
||||
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
|
||||
final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
|
||||
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(context);
|
||||
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.configureParser(parserBuilder);
|
||||
plugin.configureTheme(themeBuilder);
|
||||
plugin.configureImages(asyncDrawableLoaderBuilder);
|
||||
plugin.configureConfiguration(configurationBuilder);
|
||||
plugin.configureVisitor(visitorBuilder);
|
||||
}
|
||||
|
||||
final MarkwonConfiguration configuration = configurationBuilder.build(
|
||||
themeBuilder.build(),
|
||||
asyncDrawableLoaderBuilder.build());
|
||||
|
||||
return new MarkwonImpl(
|
||||
parserBuilder.build(),
|
||||
visitorBuilder.build(configurationBuilder.build(themeBuilder.build())),
|
||||
visitorBuilder.build(configuration),
|
||||
Collections.unmodifiableList(plugins)
|
||||
);
|
||||
}
|
||||
|
@ -4,10 +4,11 @@ import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import ru.noties.markwon.html.api.MarkwonHtmlParser;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoaderNoOp;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolverDef;
|
||||
import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.LinkSpan;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
|
||||
@ -20,7 +21,7 @@ public class MarkwonConfiguration {
|
||||
// creates default configuration
|
||||
@NonNull
|
||||
public static MarkwonConfiguration create(@NonNull Context context) {
|
||||
return new Builder(context).build(MarkwonTheme.create(context));
|
||||
return new Builder(context).build(MarkwonTheme.create(context), new AsyncDrawableLoaderNoOp());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -30,7 +31,7 @@ public class MarkwonConfiguration {
|
||||
|
||||
|
||||
private final MarkwonTheme theme;
|
||||
private final AsyncDrawable.Loader asyncDrawableLoader;
|
||||
private final AsyncDrawableLoader asyncDrawableLoader;
|
||||
private final SyntaxHighlight syntaxHighlight;
|
||||
private final LinkSpan.Resolver linkResolver;
|
||||
private final UrlProcessor urlProcessor;
|
||||
@ -69,7 +70,7 @@ public class MarkwonConfiguration {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawable.Loader asyncDrawableLoader() {
|
||||
public AsyncDrawableLoader asyncDrawableLoader() {
|
||||
return asyncDrawableLoader;
|
||||
}
|
||||
|
||||
@ -136,7 +137,7 @@ public class MarkwonConfiguration {
|
||||
private final Context context;
|
||||
|
||||
private MarkwonTheme theme;
|
||||
private AsyncDrawable.Loader asyncDrawableLoader;
|
||||
private AsyncDrawableLoader asyncDrawableLoader;
|
||||
private SyntaxHighlight syntaxHighlight;
|
||||
private LinkSpan.Resolver linkResolver;
|
||||
private UrlProcessor urlProcessor;
|
||||
@ -166,19 +167,6 @@ public class MarkwonConfiguration {
|
||||
this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags;
|
||||
}
|
||||
|
||||
// @NonNull
|
||||
// @Deprecated
|
||||
// public Builder theme(@NonNull MarkwonTheme theme) {
|
||||
// this.theme = theme;
|
||||
// return this;
|
||||
// }
|
||||
|
||||
@NonNull
|
||||
public Builder asyncDrawableLoader(@NonNull AsyncDrawable.Loader asyncDrawableLoader) {
|
||||
this.asyncDrawableLoader = asyncDrawableLoader;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
|
||||
this.syntaxHighlight = syntaxHighlight;
|
||||
@ -260,13 +248,10 @@ public class MarkwonConfiguration {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonConfiguration build(@NonNull MarkwonTheme theme) {
|
||||
public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) {
|
||||
|
||||
this.theme = theme;
|
||||
|
||||
if (asyncDrawableLoader == null) {
|
||||
asyncDrawableLoader = new AsyncDrawableLoaderNoOp();
|
||||
}
|
||||
this.asyncDrawableLoader = asyncDrawableLoader;
|
||||
|
||||
if (syntaxHighlight == null) {
|
||||
syntaxHighlight = new SyntaxHighlightNoOp();
|
||||
|
@ -62,7 +62,7 @@ class MarkwonImpl extends Markwon2 {
|
||||
textView.setText(markdown);
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.afterSetText(textView, markdown);
|
||||
plugin.afterSetText(textView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
|
||||
public interface MarkwonPlugin {
|
||||
@ -13,11 +14,12 @@ public interface MarkwonPlugin {
|
||||
|
||||
void configureTheme(@NonNull MarkwonTheme.Builder builder);
|
||||
|
||||
void configureImages(@NonNull AsyncDrawableLoader.Builder builder);
|
||||
|
||||
void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder);
|
||||
|
||||
void configureVisitor(@NonNull MarkwonVisitor.Builder builder);
|
||||
|
||||
// images
|
||||
// html
|
||||
|
||||
@NonNull
|
||||
@ -25,5 +27,8 @@ public interface MarkwonPlugin {
|
||||
|
||||
void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown);
|
||||
|
||||
void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown);
|
||||
// this method do not receive markdown like `beforeSetText` does because at this
|
||||
// point TextView already has markdown set and to manipulate spans one must
|
||||
// request them from TextView (getText())
|
||||
void afterSetText(@NonNull TextView textView);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ public interface MarkwonVisitor extends Visitor {
|
||||
interface Builder {
|
||||
|
||||
@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
|
||||
MarkwonVisitor build(@NonNull MarkwonConfiguration configuration);
|
||||
|
@ -281,8 +281,14 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <N extends Node> Builder on(@NonNull Class<N> node, @NonNull NodeVisitor<N> nodeVisitor) {
|
||||
nodes.put(node, 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);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -3,14 +3,11 @@ package ru.noties.markwon;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.renderer.ImageSize;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.LinkSpan;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
import ru.noties.markwon.spans.TableRowSpan;
|
||||
|
||||
/**
|
||||
* Each method can return null or a Span object or an array of spans
|
||||
@ -46,16 +43,6 @@ public interface SpannableFactory {
|
||||
@Nullable
|
||||
Object strikethrough();
|
||||
|
||||
@Nullable
|
||||
Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone);
|
||||
|
||||
@Nullable
|
||||
Object tableRow(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull List<TableRowSpan.Cell> cells,
|
||||
boolean isHeader,
|
||||
boolean isOdd);
|
||||
|
||||
/**
|
||||
* @since 1.1.1
|
||||
*/
|
||||
@ -66,7 +53,7 @@ public interface SpannableFactory {
|
||||
Object image(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull String destination,
|
||||
@NonNull AsyncDrawable.Loader loader,
|
||||
@NonNull AsyncDrawableLoader loader,
|
||||
@NonNull ImageSizeResolver imageSizeResolver,
|
||||
@Nullable ImageSize imageSize,
|
||||
boolean replacementTextIsLink);
|
||||
|
@ -5,11 +5,10 @@ import android.support.annotation.Nullable;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.renderer.ImageSize;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.AsyncDrawableSpan;
|
||||
import ru.noties.markwon.spans.BlockQuoteSpan;
|
||||
import ru.noties.markwon.spans.BulletListItemSpan;
|
||||
@ -17,13 +16,11 @@ import ru.noties.markwon.spans.CodeSpan;
|
||||
import ru.noties.markwon.spans.EmphasisSpan;
|
||||
import ru.noties.markwon.spans.HeadingSpan;
|
||||
import ru.noties.markwon.spans.LinkSpan;
|
||||
import ru.noties.markwon.spans.OrderedListItemSpan;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
import ru.noties.markwon.spans.OrderedListItemSpan;
|
||||
import ru.noties.markwon.spans.StrongEmphasisSpan;
|
||||
import ru.noties.markwon.spans.SubScriptSpan;
|
||||
import ru.noties.markwon.spans.SuperScriptSpan;
|
||||
import ru.noties.markwon.spans.TableRowSpan;
|
||||
import ru.noties.markwon.tasklist.TaskListSpan;
|
||||
import ru.noties.markwon.spans.ThematicBreakSpan;
|
||||
|
||||
/**
|
||||
@ -91,18 +88,6 @@ public class SpannableFactoryDef implements SpannableFactory {
|
||||
return new StrikethroughSpan();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) {
|
||||
return new TaskListSpan(theme, blockIndent, isDone);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object tableRow(@NonNull MarkwonTheme theme, @NonNull List<TableRowSpan.Cell> cells, boolean isHeader, boolean isOdd) {
|
||||
return new TableRowSpan(theme, cells, isHeader, isOdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.1.1
|
||||
*/
|
||||
@ -114,7 +99,7 @@ public class SpannableFactoryDef implements SpannableFactory {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
|
||||
public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
|
||||
return new AsyncDrawableSpan(
|
||||
theme,
|
||||
new AsyncDrawable(
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon;
|
||||
package ru.noties.markwon.core;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@ -15,10 +15,10 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.renderer.R;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.AsyncDrawableSpan;
|
||||
|
||||
abstract class DrawablesScheduler {
|
||||
abstract class AsyncDrawableScheduler {
|
||||
|
||||
static void schedule(@NonNull final TextView textView) {
|
||||
|
||||
@ -104,7 +104,7 @@ abstract class DrawablesScheduler {
|
||||
return list;
|
||||
}
|
||||
|
||||
private DrawablesScheduler() {
|
||||
private AsyncDrawableScheduler() {
|
||||
}
|
||||
|
||||
private static class DrawableCallbackImpl implements Drawable.Callback {
|
@ -11,7 +11,6 @@ import org.commonmark.node.Emphasis;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.node.HardLineBreak;
|
||||
import org.commonmark.node.Heading;
|
||||
import org.commonmark.node.Image;
|
||||
import org.commonmark.node.IndentedCodeBlock;
|
||||
import org.commonmark.node.Link;
|
||||
import org.commonmark.node.ListBlock;
|
||||
@ -73,13 +72,19 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
||||
softLineBreak(builder);
|
||||
hardLineBreak(builder);
|
||||
paragraph(builder);
|
||||
image(builder);
|
||||
// image(builder);
|
||||
link(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) {
|
||||
OrderedListItemSpan.measure(textView, markdown);
|
||||
AsyncDrawableScheduler.unschedule(textView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
AsyncDrawableScheduler.schedule(textView);
|
||||
}
|
||||
|
||||
protected void text(@NonNull MarkwonVisitor.Builder builder) {
|
||||
@ -366,41 +371,6 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
||||
});
|
||||
}
|
||||
|
||||
protected void image(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Image.class, new MarkwonVisitor.NodeVisitor<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) {
|
||||
builder.on(Link.class, new MarkwonVisitor.NodeVisitor<Link>() {
|
||||
@Override
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon.spans;
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
@ -15,15 +15,8 @@ import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
|
||||
public class AsyncDrawable extends Drawable {
|
||||
|
||||
public interface Loader {
|
||||
|
||||
void load(@NonNull String destination, @NonNull AsyncDrawable drawable);
|
||||
|
||||
void cancel(@NonNull String destination);
|
||||
}
|
||||
|
||||
private final String destination;
|
||||
private final Loader loader;
|
||||
private final AsyncDrawableLoader loader;
|
||||
private final ImageSize imageSize;
|
||||
private final ImageSizeResolver imageSizeResolver;
|
||||
|
||||
@ -38,7 +31,7 @@ public class AsyncDrawable extends Drawable {
|
||||
*/
|
||||
public AsyncDrawable(
|
||||
@NonNull String destination,
|
||||
@NonNull Loader loader,
|
||||
@NonNull AsyncDrawableLoader loader,
|
||||
@Nullable ImageSizeResolver imageSizeResolver,
|
||||
@Nullable ImageSize imageSize
|
||||
) {
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
package ru.noties.markwon;
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
|
||||
class AsyncDrawableLoaderNoOp implements AsyncDrawable.Loader {
|
||||
public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader {
|
||||
@Override
|
||||
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
|
||||
|
31
markwon/src/main/java/ru/noties/markwon/image/ImageItem.java
Normal file
31
markwon/src/main/java/ru/noties/markwon/image/ImageItem.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.SpannableFactory;
|
||||
import ru.noties.markwon.html.api.MarkwonHtmlParser;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
import ru.noties.markwon.spans.TableRowSpan;
|
||||
import ru.noties.markwon.table.TableRowSpan;
|
||||
import ru.noties.markwon.tasklist.TaskListBlock;
|
||||
import ru.noties.markwon.tasklist.TaskListItem;
|
||||
|
||||
@ -321,112 +321,92 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
visitChildren(customNode);
|
||||
setSpan(length, factory.strikethrough());
|
||||
|
||||
} else if (customNode instanceof TaskListItem) {
|
||||
|
||||
// new in 1.0.1
|
||||
|
||||
final TaskListItem listItem = (TaskListItem) customNode;
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
blockQuoteIndent += listItem.indent();
|
||||
|
||||
visitChildren(customNode);
|
||||
|
||||
setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done()));
|
||||
|
||||
if (hasNext(customNode)) {
|
||||
newLine();
|
||||
}
|
||||
|
||||
blockQuoteIndent -= listItem.indent();
|
||||
|
||||
} else if (!handleTableNodes(customNode)) {
|
||||
} else {
|
||||
super.visit(customNode);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleTableNodes(CustomNode node) {
|
||||
|
||||
final boolean handled;
|
||||
|
||||
if (node instanceof TableBody) {
|
||||
|
||||
visitChildren(node);
|
||||
tableRows = 0;
|
||||
handled = true;
|
||||
|
||||
if (hasNext(node)) {
|
||||
newLine();
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
} else if (node instanceof TableRow || node instanceof TableHead) {
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
visitChildren(node);
|
||||
|
||||
if (pendingTableRow != null) {
|
||||
|
||||
// @since 2.0.0
|
||||
// we cannot rely on hasNext(TableHead) as it's not reliable
|
||||
// we must apply new line manually and then exclude it from tableRow span
|
||||
final boolean addNewLine;
|
||||
{
|
||||
final int builderLength = builder.length();
|
||||
addNewLine = builderLength > 0
|
||||
&& '\n' != builder.charAt(builderLength - 1);
|
||||
}
|
||||
if (addNewLine) {
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
// @since 1.0.4 Replace table char with non-breakable space
|
||||
// we need this because if table is at the end of the text, then it will be
|
||||
// trimmed from the final result
|
||||
builder.append('\u00a0');
|
||||
|
||||
final Object span = factory.tableRow(
|
||||
theme,
|
||||
pendingTableRow,
|
||||
tableRowIsHeader,
|
||||
tableRows % 2 == 1);
|
||||
|
||||
tableRows = tableRowIsHeader
|
||||
? 0
|
||||
: tableRows + 1;
|
||||
|
||||
setSpan(addNewLine ? length + 1 : length, span);
|
||||
|
||||
pendingTableRow = null;
|
||||
}
|
||||
|
||||
handled = true;
|
||||
|
||||
} else if (node instanceof TableCell) {
|
||||
|
||||
final TableCell cell = (TableCell) node;
|
||||
final int length = builder.length();
|
||||
visitChildren(cell);
|
||||
if (pendingTableRow == null) {
|
||||
pendingTableRow = new ArrayList<>(2);
|
||||
}
|
||||
|
||||
pendingTableRow.add(new TableRowSpan.Cell(
|
||||
tableCellAlignment(cell.getAlignment()),
|
||||
builder.removeFromEnd(length)
|
||||
));
|
||||
|
||||
tableRowIsHeader = cell.isHeader();
|
||||
|
||||
handled = true;
|
||||
} else {
|
||||
handled = false;
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
// private boolean handleTableNodes(CustomNode node) {
|
||||
//
|
||||
// final boolean handled;
|
||||
//
|
||||
// if (node instanceof TableBody) {
|
||||
//
|
||||
// visitChildren(node);
|
||||
// tableRows = 0;
|
||||
// handled = true;
|
||||
//
|
||||
// if (hasNext(node)) {
|
||||
// newLine();
|
||||
// builder.append('\n');
|
||||
// }
|
||||
//
|
||||
// } else if (node instanceof TableRow || node instanceof TableHead) {
|
||||
//
|
||||
// final int length = builder.length();
|
||||
//
|
||||
// visitChildren(node);
|
||||
//
|
||||
// if (pendingTableRow != null) {
|
||||
//
|
||||
// // @since 2.0.0
|
||||
// // we cannot rely on hasNext(TableHead) as it's not reliable
|
||||
// // we must apply new line manually and then exclude it from tableRow span
|
||||
// final boolean addNewLine;
|
||||
// {
|
||||
// final int builderLength = builder.length();
|
||||
// addNewLine = builderLength > 0
|
||||
// && '\n' != builder.charAt(builderLength - 1);
|
||||
// }
|
||||
// if (addNewLine) {
|
||||
// builder.append('\n');
|
||||
// }
|
||||
//
|
||||
// // @since 1.0.4 Replace table char with non-breakable space
|
||||
// // we need this because if table is at the end of the text, then it will be
|
||||
// // trimmed from the final result
|
||||
// builder.append('\u00a0');
|
||||
//
|
||||
// final Object span = factory.tableRow(
|
||||
// theme,
|
||||
// pendingTableRow,
|
||||
// tableRowIsHeader,
|
||||
// tableRows % 2 == 1);
|
||||
//
|
||||
// tableRows = tableRowIsHeader
|
||||
// ? 0
|
||||
// : tableRows + 1;
|
||||
//
|
||||
// setSpan(addNewLine ? length + 1 : length, span);
|
||||
//
|
||||
// pendingTableRow = null;
|
||||
// }
|
||||
//
|
||||
// handled = true;
|
||||
//
|
||||
// } else if (node instanceof TableCell) {
|
||||
//
|
||||
// final TableCell cell = (TableCell) node;
|
||||
// final int length = builder.length();
|
||||
// visitChildren(cell);
|
||||
// if (pendingTableRow == null) {
|
||||
// pendingTableRow = new ArrayList<>(2);
|
||||
// }
|
||||
//
|
||||
// pendingTableRow.add(new TableRowSpan.Cell(
|
||||
// tableCellAlignment(cell.getAlignment()),
|
||||
// builder.removeFromEnd(length)
|
||||
// ));
|
||||
//
|
||||
// tableRowIsHeader = cell.isHeader();
|
||||
//
|
||||
// handled = true;
|
||||
// } else {
|
||||
// handled = false;
|
||||
// }
|
||||
//
|
||||
// return handled;
|
||||
// }
|
||||
|
||||
@Override
|
||||
public void visit(Paragraph paragraph) {
|
||||
@ -530,26 +510,26 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
return false;
|
||||
}
|
||||
|
||||
@TableRowSpan.Alignment
|
||||
private static int tableCellAlignment(TableCell.Alignment alignment) {
|
||||
final int out;
|
||||
if (alignment != null) {
|
||||
switch (alignment) {
|
||||
case CENTER:
|
||||
out = TableRowSpan.ALIGN_CENTER;
|
||||
break;
|
||||
case RIGHT:
|
||||
out = TableRowSpan.ALIGN_RIGHT;
|
||||
break;
|
||||
default:
|
||||
out = TableRowSpan.ALIGN_LEFT;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
out = TableRowSpan.ALIGN_LEFT;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// @TableRowSpan.Alignment
|
||||
// private static int tableCellAlignment(TableCell.Alignment alignment) {
|
||||
// final int out;
|
||||
// if (alignment != null) {
|
||||
// switch (alignment) {
|
||||
// case CENTER:
|
||||
// out = TableRowSpan.ALIGN_CENTER;
|
||||
// break;
|
||||
// case RIGHT:
|
||||
// out = TableRowSpan.ALIGN_RIGHT;
|
||||
// break;
|
||||
// default:
|
||||
// out = TableRowSpan.ALIGN_LEFT;
|
||||
// break;
|
||||
// }
|
||||
// } else {
|
||||
// out = TableRowSpan.ALIGN_LEFT;
|
||||
// }
|
||||
// return out;
|
||||
// }
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
|
@ -12,6 +12,8 @@ import android.text.style.ReplacementSpan;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class AsyncDrawableSpan extends ReplacementSpan {
|
||||
|
||||
@ -29,16 +31,16 @@ public class AsyncDrawableSpan extends ReplacementSpan {
|
||||
private final int alignment;
|
||||
private final boolean replacementTextIsLink;
|
||||
|
||||
public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) {
|
||||
this(theme, drawable, ALIGN_BOTTOM);
|
||||
}
|
||||
// public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) {
|
||||
// this(theme, drawable, ALIGN_BOTTOM);
|
||||
// }
|
||||
|
||||
public AsyncDrawableSpan(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull AsyncDrawable drawable,
|
||||
@Alignment int alignment) {
|
||||
this(theme, drawable, alignment, false);
|
||||
}
|
||||
// public AsyncDrawableSpan(
|
||||
// @NonNull MarkwonTheme theme,
|
||||
// @NonNull AsyncDrawable drawable,
|
||||
// @Alignment int alignment) {
|
||||
// this(theme, drawable, alignment, false);
|
||||
// }
|
||||
|
||||
public AsyncDrawableSpan(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@ -137,7 +139,7 @@ public class AsyncDrawableSpan extends ReplacementSpan {
|
||||
// will it make sense to have additional background/borders for an image replacement?
|
||||
// let's focus on main functionality and then think of it
|
||||
|
||||
final float textY = CanvasUtils.textCenterY(top, bottom, paint);
|
||||
final float textY = textCenterY(top, bottom, paint);
|
||||
if (replacementTextIsLink) {
|
||||
theme.applyLinkStyle(paint);
|
||||
}
|
||||
@ -150,4 +152,9 @@ public class AsyncDrawableSpan extends ReplacementSpan {
|
||||
public AsyncDrawable getDrawable() {
|
||||
return drawable;
|
||||
}
|
||||
|
||||
private static float textCenterY(int top, int bottom, @NonNull Paint paint) {
|
||||
// @since 1.1.1 it's `top +` and not `bottom -`
|
||||
return (int) (top + ((bottom - top) / 2) - ((paint.descent() + paint.ascent()) / 2.F + .5F));
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -1,25 +1,22 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.FloatRange;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.Px;
|
||||
import android.support.annotation.Size;
|
||||
import android.text.TextPaint;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import ru.noties.markwon.tasklist.TaskListDrawable;
|
||||
import ru.noties.markwon.utils.ColorUtils;
|
||||
import ru.noties.markwon.utils.Dip;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class MarkwonTheme {
|
||||
@ -77,36 +74,14 @@ public class MarkwonTheme {
|
||||
@NonNull
|
||||
public static Builder builderWithDefaults(@NonNull Context context) {
|
||||
|
||||
// by default we will be using link color for the checkbox color
|
||||
// & window background as a checkMark color
|
||||
final int linkColor = resolve(context, android.R.attr.textColorLink);
|
||||
final int backgroundColor = resolve(context, android.R.attr.colorBackground);
|
||||
|
||||
// before 1.0.5 build had `linkColor` set, but in order for spans to use default link color
|
||||
// set directly in widget (or any caller), we should not pass it here
|
||||
|
||||
final Dip dip = new Dip(context);
|
||||
final Dip dip = Dip.create(context);
|
||||
return new Builder()
|
||||
.codeMultilineMargin(dip.toPx(8))
|
||||
.blockMargin(dip.toPx(24))
|
||||
.blockQuoteWidth(dip.toPx(4))
|
||||
.bulletListItemStrokeWidth(dip.toPx(1))
|
||||
.headingBreakHeight(dip.toPx(1))
|
||||
.thematicBreakHeight(dip.toPx(4))
|
||||
.tableCellPadding(dip.toPx(4))
|
||||
.tableBorderWidth(dip.toPx(1))
|
||||
.taskListDrawable(new TaskListDrawable(linkColor, linkColor, backgroundColor));
|
||||
}
|
||||
|
||||
private static int resolve(Context context, @AttrRes int attr) {
|
||||
final TypedValue typedValue = new TypedValue();
|
||||
final int attrs[] = new int[]{attr};
|
||||
final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs);
|
||||
try {
|
||||
return typedArray.getColor(0, 0);
|
||||
} finally {
|
||||
typedArray.recycle();
|
||||
}
|
||||
.thematicBreakHeight(dip.toPx(4));
|
||||
}
|
||||
|
||||
protected static final int BLOCK_QUOTE_DEF_COLOR_ALPHA = 25;
|
||||
@ -126,10 +101,6 @@ public class MarkwonTheme {
|
||||
|
||||
protected static final int THEMATIC_BREAK_DEF_ALPHA = 25;
|
||||
|
||||
protected static final int TABLE_BORDER_DEF_ALPHA = 75;
|
||||
|
||||
protected static final int TABLE_ODD_ROW_DEF_ALPHA = 22;
|
||||
|
||||
protected final int linkColor;
|
||||
|
||||
// used in quote, lists
|
||||
@ -197,30 +168,6 @@ public class MarkwonTheme {
|
||||
// by default paint.strokeWidth
|
||||
protected final int thematicBreakHeight;
|
||||
|
||||
// by default 0
|
||||
protected final int tableCellPadding;
|
||||
|
||||
// by default paint.color * TABLE_BORDER_DEF_ALPHA
|
||||
protected final int tableBorderColor;
|
||||
|
||||
protected final int tableBorderWidth;
|
||||
|
||||
// by default paint.color * TABLE_ODD_ROW_DEF_ALPHA
|
||||
protected final int tableOddRowBackgroundColor;
|
||||
|
||||
// @since 1.1.1
|
||||
// by default no background
|
||||
protected final int tableEventRowBackgroundColor;
|
||||
|
||||
// @since 1.1.1
|
||||
// by default no background
|
||||
protected final int tableHeaderRowBackgroundColor;
|
||||
|
||||
// drawable that will be used to render checkbox (should be stateful)
|
||||
// TaskListDrawable can be used
|
||||
@Deprecated
|
||||
protected final Drawable taskListDrawable;
|
||||
|
||||
protected MarkwonTheme(@NonNull Builder builder) {
|
||||
this.linkColor = builder.linkColor;
|
||||
this.blockMargin = builder.blockMargin;
|
||||
@ -243,13 +190,6 @@ public class MarkwonTheme {
|
||||
this.scriptTextSizeRatio = builder.scriptTextSizeRatio;
|
||||
this.thematicBreakColor = builder.thematicBreakColor;
|
||||
this.thematicBreakHeight = builder.thematicBreakHeight;
|
||||
this.tableCellPadding = builder.tableCellPadding;
|
||||
this.tableBorderColor = builder.tableBorderColor;
|
||||
this.tableBorderWidth = builder.tableBorderWidth;
|
||||
this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor;
|
||||
this.tableEventRowBackgroundColor = builder.tableEvenRowBackgroundColor;
|
||||
this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor;
|
||||
this.taskListDrawable = builder.taskListDrawable;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -468,71 +408,6 @@ public class MarkwonTheme {
|
||||
}
|
||||
}
|
||||
|
||||
public int tableCellPadding() {
|
||||
return tableCellPadding;
|
||||
}
|
||||
|
||||
public int tableBorderWidth(@NonNull Paint paint) {
|
||||
final int out;
|
||||
if (tableBorderWidth == -1) {
|
||||
out = (int) (paint.getStrokeWidth() + .5F);
|
||||
} else {
|
||||
out = tableBorderWidth;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public void applyTableBorderStyle(@NonNull Paint paint) {
|
||||
|
||||
final int color;
|
||||
if (tableBorderColor == 0) {
|
||||
color = ColorUtils.applyAlpha(paint.getColor(), TABLE_BORDER_DEF_ALPHA);
|
||||
} else {
|
||||
color = tableBorderColor;
|
||||
}
|
||||
|
||||
paint.setColor(color);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
|
||||
public void applyTableOddRowStyle(@NonNull Paint paint) {
|
||||
final int color;
|
||||
if (tableOddRowBackgroundColor == 0) {
|
||||
color = ColorUtils.applyAlpha(paint.getColor(), TABLE_ODD_ROW_DEF_ALPHA);
|
||||
} else {
|
||||
color = tableOddRowBackgroundColor;
|
||||
}
|
||||
paint.setColor(color);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.1.1
|
||||
*/
|
||||
public void applyTableEvenRowStyle(@NonNull Paint paint) {
|
||||
// by default to background to even row
|
||||
paint.setColor(tableEventRowBackgroundColor);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.1.1
|
||||
*/
|
||||
public void applyTableHeaderRowStyle(@NonNull Paint paint) {
|
||||
paint.setColor(tableHeaderRowBackgroundColor);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a Drawable to be used as a checkbox indication in task lists
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated
|
||||
public Drawable getTaskListDrawable() {
|
||||
return taskListDrawable;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class Builder {
|
||||
|
||||
@ -557,13 +432,6 @@ public class MarkwonTheme {
|
||||
private float scriptTextSizeRatio;
|
||||
private int thematicBreakColor;
|
||||
private int thematicBreakHeight = -1;
|
||||
private int tableCellPadding;
|
||||
private int tableBorderColor;
|
||||
private int tableBorderWidth = -1;
|
||||
private int tableOddRowBackgroundColor;
|
||||
private int tableEvenRowBackgroundColor; // @since 1.1.1
|
||||
private int tableHeaderRowBackgroundColor; // @since 1.1.1
|
||||
private Drawable taskListDrawable;
|
||||
|
||||
Builder() {
|
||||
}
|
||||
@ -590,11 +458,6 @@ public class MarkwonTheme {
|
||||
this.scriptTextSizeRatio = theme.scriptTextSizeRatio;
|
||||
this.thematicBreakColor = theme.thematicBreakColor;
|
||||
this.thematicBreakHeight = theme.thematicBreakHeight;
|
||||
this.tableCellPadding = theme.tableCellPadding;
|
||||
this.tableBorderColor = theme.tableBorderColor;
|
||||
this.tableBorderWidth = theme.tableBorderWidth;
|
||||
this.tableOddRowBackgroundColor = theme.tableOddRowBackgroundColor;
|
||||
this.taskListDrawable = theme.taskListDrawable;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -742,81 +605,10 @@ public class MarkwonTheme {
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableCellPadding(@Px int tableCellPadding) {
|
||||
this.tableCellPadding = tableCellPadding;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableBorderColor(@ColorInt int tableBorderColor) {
|
||||
this.tableBorderColor = tableBorderColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableBorderWidth(@Px int tableBorderWidth) {
|
||||
this.tableBorderWidth = tableBorderWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableOddRowBackgroundColor(@ColorInt int tableOddRowBackgroundColor) {
|
||||
this.tableOddRowBackgroundColor = tableOddRowBackgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.1.1
|
||||
*/
|
||||
@NonNull
|
||||
public Builder tableEvenRowBackgroundColor(@ColorInt int tableEvenRowBackgroundColor) {
|
||||
this.tableEvenRowBackgroundColor = tableEvenRowBackgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.1.1
|
||||
*/
|
||||
@NonNull
|
||||
public Builder tableHeaderRowBackgroundColor(@ColorInt int tableHeaderRowBackgroundColor) {
|
||||
this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplied Drawable must be stateful ({@link Drawable#isStateful()} returns true). If a task
|
||||
* is marked as done, then this drawable will be updated with an {@code int[] { android.R.attr.state_checked }}
|
||||
* as the state, otherwise an empty array will be used. This library provides a ready to be
|
||||
* used Drawable: {@link TaskListDrawable}
|
||||
*
|
||||
* @param taskListDrawable Drawable to be used as the task list indication (checkbox)
|
||||
* @see TaskListDrawable
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated
|
||||
public Builder taskListDrawable(@NonNull Drawable taskListDrawable) {
|
||||
this.taskListDrawable = taskListDrawable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonTheme build() {
|
||||
return new MarkwonTheme(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Dip {
|
||||
|
||||
private final float density;
|
||||
|
||||
Dip(@NonNull Context context) {
|
||||
this.density = context.getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
int toPx(int dp) {
|
||||
return (int) (dp * density + .5F);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
189
markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java
Normal file
189
markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon.spans;
|
||||
package ru.noties.markwon.table;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Canvas;
|
||||
@ -61,22 +61,22 @@ public class TableRowSpan extends ReplacementSpan {
|
||||
}
|
||||
}
|
||||
|
||||
private final MarkwonTheme theme;
|
||||
private final TableTheme theme;
|
||||
private final List<Cell> cells;
|
||||
private final List<StaticLayout> layouts;
|
||||
private final TextPaint textPaint;
|
||||
private final boolean header;
|
||||
private final boolean odd;
|
||||
|
||||
private final Rect rect = ObjectsPool.rect();
|
||||
private final Paint paint = ObjectsPool.paint();
|
||||
private final Rect rect = new Rect();
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
private int width;
|
||||
private int height;
|
||||
private Invalidator invalidator;
|
||||
|
||||
public TableRowSpan(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull TableTheme theme,
|
||||
@NonNull List<Cell> cells,
|
||||
boolean header,
|
||||
boolean odd) {
|
||||
@ -272,8 +272,7 @@ public class TableRowSpan extends ReplacementSpan {
|
||||
return out;
|
||||
}
|
||||
|
||||
public TableRowSpan invalidator(Invalidator invalidator) {
|
||||
public void invalidator(@Nullable Invalidator invalidator) {
|
||||
this.invalidator = invalidator;
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
package ru.noties.markwon;
|
||||
package ru.noties.markwon.table;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import ru.noties.markwon.renderer.R;
|
||||
import ru.noties.markwon.spans.TableRowSpan;
|
||||
|
||||
abstract class TableRowsScheduler {
|
||||
|
||||
@ -57,6 +57,7 @@ abstract class TableRowsScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Object[] extract(@NonNull TextView view) {
|
||||
final Object[] out;
|
||||
final CharSequence text = view.getText();
|
164
markwon/src/main/java/ru/noties/markwon/table/TableTheme.java
Normal file
164
markwon/src/main/java/ru/noties/markwon/table/TableTheme.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
package ru.noties.markwon.tasklist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
@ -13,6 +16,11 @@ import ru.noties.markwon.MarkwonVisitor;
|
||||
public class TaskListPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
/**
|
||||
* Supplied Drawable must be stateful ({@link Drawable#isStateful()} returns true). If a task
|
||||
* is marked as done, then this drawable will be updated with an {@code int[] { android.R.attr.state_checked }}
|
||||
* as the state, otherwise an empty array will be used. This library provides a ready to be
|
||||
* used Drawable: {@link TaskListDrawable}
|
||||
*
|
||||
* @see TaskListDrawable
|
||||
*/
|
||||
@NonNull
|
||||
@ -22,8 +30,13 @@ public class TaskListPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
public static TaskListPlugin create(@NonNull Context context) {
|
||||
// resolve link color and background color
|
||||
return null;
|
||||
|
||||
// by default we will be using link color for the checkbox color
|
||||
// & window background as a checkMark color
|
||||
final int linkColor = resolve(context, android.R.attr.textColorLink);
|
||||
final int backgroundColor = resolve(context, android.R.attr.colorBackground);
|
||||
|
||||
return new TaskListPlugin(new TaskListDrawable(linkColor, linkColor, backgroundColor));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -90,4 +103,15 @@ public class TaskListPlugin extends AbstractMarkwonPlugin {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int resolve(Context context, @AttrRes int attr) {
|
||||
final TypedValue typedValue = new TypedValue();
|
||||
final int attrs[] = new int[]{attr};
|
||||
final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs);
|
||||
try {
|
||||
return typedArray.getColor(0, 0);
|
||||
} finally {
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
29
markwon/src/main/java/ru/noties/markwon/utils/Dip.java
Normal file
29
markwon/src/main/java/ru/noties/markwon/utils/Dip.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import ru.noties.markwon.SyntaxHighlight;
|
||||
import ru.noties.markwon.UrlProcessor;
|
||||
import ru.noties.markwon.html.api.MarkwonHtmlParser;
|
||||
import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.LinkSpan;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
|
||||
|
@ -30,7 +30,7 @@ import java.util.Set;
|
||||
import ix.Ix;
|
||||
import ix.IxFunction;
|
||||
import ix.IxPredicate;
|
||||
import ru.noties.markwon.spans.TableRowSpan;
|
||||
import ru.noties.markwon.table.TableRowSpan;
|
||||
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST;
|
||||
|
@ -11,10 +11,10 @@ import java.util.Map;
|
||||
import ru.noties.markwon.SpannableFactory;
|
||||
import ru.noties.markwon.renderer.ImageSize;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.LinkSpan;
|
||||
import ru.noties.markwon.spans.MarkwonTheme;
|
||||
import ru.noties.markwon.spans.TableRowSpan;
|
||||
import ru.noties.markwon.table.TableRowSpan;
|
||||
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST;
|
||||
|
@ -18,6 +18,6 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(':markwon')
|
||||
implementation project(':markwon-image-loader')
|
||||
// implementation project(':markwon-image-loader')
|
||||
implementation 'ru.noties:jlatexmath-android:0.1.0'
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
rootProject.name = 'MarkwonProject'
|
||||
include ':app', ':markwon', ':markwon-image-loader', ':markwon-view', ':sample-custom-extension', ':sample-latex-math',
|
||||
include ':app', ':markwon', ':markwon-view', ':sample-custom-extension', ':sample-latex-math', ':markwon-image-svg', ':markwon-image-gif',
|
||||
':markwon-syntax-highlight', ':markwon-html-parser-api', ':markwon-html-parser-impl'
|
||||
|
Loading…
x
Reference in New Issue
Block a user