Failing editor tests

This commit is contained in:
Dimitry Ivanov 2021-02-01 17:09:13 +03:00
parent 3069432bc2
commit 646e708c82
5 changed files with 247 additions and 137 deletions

View File

@ -1,4 +1,16 @@
[ [
{
"javaClassName": "io.noties.markwon.app.samples.html.InspectHtmlTextSample",
"id": "20210201140501",
"title": "Inspect text",
"description": "Inspect text content of a `HTML` node",
"artifacts": [
"HTML"
],
"tags": [
"HTML"
]
},
{ {
"javaClassName": "io.noties.markwon.app.samples.image.HugeImageSample", "javaClassName": "io.noties.markwon.app.samples.image.HugeImageSample",
"id": "20210118165230", "id": "20210118165230",

View File

@ -0,0 +1,57 @@
package io.noties.markwon.app.samples.html
import android.text.style.URLSpan
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.app.sample.Tags
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.TagHandler
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
@MarkwonSampleInfo(
id = "20210201140501",
title = "Inspect text",
description = "Inspect text content of a `HTML` node",
artifacts = [MarkwonArtifact.HTML],
tags = [Tags.html]
)
class InspectHtmlTextSample : MarkwonTextViewSample() {
override fun render() {
val md = """
<p>lorem ipsum</p>
<div class="custom-youtube-player">https://www.youtube.com/watch?v=abcdefgh</div>
""".trimIndent()
val markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create {
it.addHandler(DivHandler())
})
.build()
markwon.setMarkdown(textView, md)
}
class DivHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val attr = tag.attributes()["class"] ?: return
if (attr.contains(CUSTOM_CLASS)) {
val text = visitor.builder().substring(tag.start(), tag.end())
visitor.builder().setSpan(
URLSpan(text),
tag.start(),
tag.end()
)
}
}
override fun supportedTags(): Collection<String> = setOf("div")
companion object {
const val CUSTOM_CLASS = "custom-youtube-player"
}
}
}

View File

@ -94,6 +94,28 @@ public class MarkwonEditorUtilsTest {
assertMatched(input, strike, "~~", 3, 11); assertMatched(input, strike, "~~", 3, 11);
} }
@Test
public void delimited_triple_asterisks() {
final String input = "***italic bold bold***";
final Match bold = findDelimited(input, 0, "**", "__");
final Match em = findDelimited(input, 0, "*", "_");
assertMatched(input, bold, "**", 0, input.length());
assertMatched(input, em, "*", 0, input.length());
}
@Test
public void delimited_triple_asterisks_2() {
final String input = "***italic bold* bold**";
final Match bold = findDelimited(input, 0, "**", "__");
final Match em = findDelimited(input, 0, "*", "_");
assertMatched(input, bold, "**", 0, input.length());
assertMatched(input, em, "*", 0, 15);
}
private static void assertMatched( private static void assertMatched(
@NonNull String input, @NonNull String input,
@Nullable Match match, @Nullable Match match,

View File

@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -77,12 +78,6 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
@NonNull @NonNull
private Future<?> execute(@NonNull final AsyncDrawable asyncDrawable) { private Future<?> execute(@NonNull final AsyncDrawable asyncDrawable) {
// todo: more efficient DefaultMediaDecoder... 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() { return executorService.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -113,17 +108,26 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
final ImageItem.WithDecodingNeeded withDecodingNeeded = imageItem.getAsWithDecodingNeeded(); final ImageItem.WithDecodingNeeded withDecodingNeeded = imageItem.getAsWithDecodingNeeded();
MediaDecoder mediaDecoder = mediaDecoders.get(withDecodingNeeded.contentType()); // @since $SNAPSHOT; close input stream
try {
MediaDecoder mediaDecoder = mediaDecoders.get(withDecodingNeeded.contentType());
if (mediaDecoder == null) { if (mediaDecoder == null) {
mediaDecoder = defaultMediaDecoder; mediaDecoder = defaultMediaDecoder;
} }
if (mediaDecoder != null) { if (mediaDecoder != null) {
drawable = mediaDecoder.decode(withDecodingNeeded.contentType(), withDecodingNeeded.inputStream()); drawable = mediaDecoder.decode(withDecodingNeeded.contentType(), withDecodingNeeded.inputStream());
} else { } else {
// throw that no media decoder is found // throw that no media decoder is found
throw new IllegalStateException("No media-decoder is found: " + destination); throw new IllegalStateException("No media-decoder is found: " + destination);
}
} finally {
try {
withDecodingNeeded.inputStream().close();
} catch (IOException e) {
Log.e("MARKWON-IMAGE", "Error closing inputStream", e);
}
} }
} else { } else {
drawable = imageItem.getAsWithResult().result(); drawable = imageItem.getAsWithResult().result();

View File

@ -33,141 +33,156 @@ import java.util.Collections;
*/ */
public class DefaultDownScalingMediaDecoder extends MediaDecoder { public class DefaultDownScalingMediaDecoder extends MediaDecoder {
/** /**
* Values {@code <= 0} are ignored, a dimension is considered to be not restrained any limit in such case * Values {@code <= 0} are ignored, a dimension is considered to be not restrained any limit in such case
*/ */
@NonNull @NonNull
public static DefaultDownScalingMediaDecoder create(int maxWidth, int maxHeight) { public static DefaultDownScalingMediaDecoder create(int maxWidth, int maxHeight) {
return create(Resources.getSystem(), maxWidth, maxHeight); return create(Resources.getSystem(), maxWidth, maxHeight);
}
@NonNull
public static DefaultDownScalingMediaDecoder create(
@NonNull Resources resources,
int maxWidth,
int maxHeight
) {
return new DefaultDownScalingMediaDecoder(resources, maxWidth, maxHeight);
}
private final Resources resources;
private final int maxWidth;
private final int maxHeight;
private DefaultDownScalingMediaDecoder(@NonNull Resources resources, int maxWidth, int maxHeight) {
this.resources = resources;
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
}
// https://android.jlelse.eu/loading-large-bitmaps-efficiently-in-android-66826cd4ad53
@NonNull
@Override
public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) {
final File file = writeToTempFile(inputStream);
try {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory
.decodeStream(readFile(file), null, options);
options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
options.inJustDecodeBounds = false;
final Bitmap bitmap = BitmapFactory
.decodeStream(readFile(file), null, options);
return new BitmapDrawable(resources, bitmap);
} finally {
// we no longer need the temporary file
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
@NonNull
private static File writeToTempFile(@NonNull InputStream inputStream) {
final File file;
try {
file = File.createTempFile("markwon", null);
} catch (IOException e) {
throw new IllegalStateException(e);
} }
final OutputStream outputStream; @NonNull
try { public static DefaultDownScalingMediaDecoder create(
outputStream = new BufferedOutputStream(new FileOutputStream(file, false)); @NonNull Resources resources,
} catch (FileNotFoundException e) { int maxWidth,
throw new IllegalStateException(e); int maxHeight
) {
return new DefaultDownScalingMediaDecoder(resources, maxWidth, maxHeight);
} }
final byte[] buffer = new byte[1024 * 8]; private final Resources resources;
int length; private final int maxWidth;
try { private final int maxHeight;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length); private DefaultDownScalingMediaDecoder(@NonNull Resources resources, int maxWidth, int maxHeight) {
} this.resources = resources;
} catch (IOException e) { this.maxWidth = maxWidth;
throw new IllegalStateException(e); this.maxHeight = maxHeight;
} finally {
try {
outputStream.close();
} catch (IOException e) {
// ignored
}
} }
return file; // https://android.jlelse.eu/loading-large-bitmaps-efficiently-in-android-66826cd4ad53
} @NonNull
@Override
public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) {
@NonNull final File file = writeToTempFile(inputStream);
private static InputStream readFile(@NonNull File file) { try {
try {
return new BufferedInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
throw new IllegalStateException(e);
}
}
// see: https://developer.android.com/topic/performance/graphics/load-bitmap.html#load-bitmap final BitmapFactory.Options options = new BitmapFactory.Options();
private static int calculateInSampleSize(@NonNull BitmapFactory.Options options, int maxWidth, int maxHeight) { options.inJustDecodeBounds = true;
final int w = options.outWidth;
final int h = options.outHeight;
final boolean hasMaxWidth = maxWidth > 0; // initial result when obtaining bounds is discarded
final boolean hasMaxHeight = maxHeight > 0; decode(file, options);
final int inSampleSize; options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
if (hasMaxWidth && hasMaxHeight) { options.inJustDecodeBounds = false;
// minimum of both
inSampleSize = Math.min(calculateInSampleSize(w, maxWidth), calculateInSampleSize(h, maxHeight)); final Bitmap bitmap = decode(file, options);
} else if (hasMaxWidth) {
inSampleSize = calculateInSampleSize(w, maxWidth); return new BitmapDrawable(resources, bitmap);
} else if (hasMaxHeight) { } finally {
inSampleSize = calculateInSampleSize(h, maxHeight); // we no longer need the temporary file
} else { //noinspection ResultOfMethodCallIgnored
// else no sampling, as we have no dimensions to base our calculations on file.delete();
inSampleSize = 1; }
} }
return inSampleSize; @NonNull
} private static File writeToTempFile(@NonNull InputStream inputStream) {
final File file;
try {
file = File.createTempFile("markwon", null);
} catch (IOException e) {
throw new IllegalStateException(e);
}
private static int calculateInSampleSize(int actual, int max) { final OutputStream outputStream;
int inSampleSize = 1; try {
final int half = actual / 2; outputStream = new BufferedOutputStream(new FileOutputStream(file, false));
while ((half / inSampleSize) > max) { } catch (FileNotFoundException e) {
inSampleSize *= 2; throw new IllegalStateException(e);
}
final byte[] buffer = new byte[1024 * 8];
int length;
try {
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
} catch (IOException e) {
throw new IllegalStateException(e);
} finally {
try {
outputStream.close();
} catch (IOException e) {
// ignored
}
}
return file;
} }
return inSampleSize;
}
@NonNull @Nullable
@Override private static Bitmap decode(@NonNull File file, @NonNull BitmapFactory.Options options) {
public Collection<String> supportedTypes() { final InputStream is = readFile(file);
return Collections.emptySet(); // not yet, still min SDK is 16
} try {
return BitmapFactory.decodeStream(is, null, options);
} finally {
try {
is.close();
} catch (IOException e) {
// ignored
}
}
}
@NonNull
private static InputStream readFile(@NonNull File file) {
try {
return new BufferedInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
throw new IllegalStateException(e);
}
}
// see: https://developer.android.com/topic/performance/graphics/load-bitmap.html#load-bitmap
private static int calculateInSampleSize(@NonNull BitmapFactory.Options options, int maxWidth, int maxHeight) {
final int w = options.outWidth;
final int h = options.outHeight;
final boolean hasMaxWidth = maxWidth > 0;
final boolean hasMaxHeight = maxHeight > 0;
final int inSampleSize;
if (hasMaxWidth && hasMaxHeight) {
// minimum of both
inSampleSize = Math.min(calculateInSampleSize(w, maxWidth), calculateInSampleSize(h, maxHeight));
} else if (hasMaxWidth) {
inSampleSize = calculateInSampleSize(w, maxWidth);
} else if (hasMaxHeight) {
inSampleSize = calculateInSampleSize(h, maxHeight);
} else {
// else no sampling, as we have no dimensions to base our calculations on
inSampleSize = 1;
}
return inSampleSize;
}
private static int calculateInSampleSize(int actual, int max) {
int inSampleSize = 1;
final int half = actual / 2;
while ((half / inSampleSize) > max) {
inSampleSize *= 2;
}
return inSampleSize;
}
@NonNull
@Override
public Collection<String> supportedTypes() {
return Collections.emptySet();
}
} }