diff --git a/app-sample/samples.json b/app-sample/samples.json
index 7be52b51..b3bfbcca 100644
--- a/app-sample/samples.json
+++ b/app-sample/samples.json
@@ -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",
"id": "20210118165230",
diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/InspectHtmlTextSample.kt b/app-sample/src/main/java/io/noties/markwon/app/samples/html/InspectHtmlTextSample.kt
new file mode 100644
index 00000000..bf34a2b6
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/InspectHtmlTextSample.kt
@@ -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 = """
+
lorem ipsum
+ https://www.youtube.com/watch?v=abcdefgh
+ """.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 = setOf("div")
+
+ companion object {
+ const val CUSTOM_CLASS = "custom-youtube-player"
+ }
+ }
+}
\ No newline at end of file
diff --git a/markwon-editor/src/test/java/io/noties/markwon/editor/MarkwonEditorUtilsTest.java b/markwon-editor/src/test/java/io/noties/markwon/editor/MarkwonEditorUtilsTest.java
index 2aa56b95..425517dd 100644
--- a/markwon-editor/src/test/java/io/noties/markwon/editor/MarkwonEditorUtilsTest.java
+++ b/markwon-editor/src/test/java/io/noties/markwon/editor/MarkwonEditorUtilsTest.java
@@ -94,6 +94,28 @@ public class MarkwonEditorUtilsTest {
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(
@NonNull String input,
@Nullable Match match,
diff --git a/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderImpl.java b/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderImpl.java
index e227cc6d..56a7add1 100644
--- a/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderImpl.java
+++ b/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderImpl.java
@@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
@@ -77,12 +78,6 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
@NonNull
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() {
@Override
public void run() {
@@ -113,17 +108,26 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
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) {
- mediaDecoder = defaultMediaDecoder;
- }
+ if (mediaDecoder == null) {
+ mediaDecoder = defaultMediaDecoder;
+ }
- if (mediaDecoder != null) {
- drawable = mediaDecoder.decode(withDecodingNeeded.contentType(), withDecodingNeeded.inputStream());
- } else {
- // throw that no media decoder is found
- throw new IllegalStateException("No media-decoder is found: " + destination);
+ if (mediaDecoder != null) {
+ drawable = mediaDecoder.decode(withDecodingNeeded.contentType(), withDecodingNeeded.inputStream());
+ } else {
+ // throw that no media decoder is found
+ 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 {
drawable = imageItem.getAsWithResult().result();
diff --git a/markwon-image/src/main/java/io/noties/markwon/image/DefaultDownScalingMediaDecoder.java b/markwon-image/src/main/java/io/noties/markwon/image/DefaultDownScalingMediaDecoder.java
index 2d7d49de..9ac9729a 100644
--- a/markwon-image/src/main/java/io/noties/markwon/image/DefaultDownScalingMediaDecoder.java
+++ b/markwon-image/src/main/java/io/noties/markwon/image/DefaultDownScalingMediaDecoder.java
@@ -33,141 +33,156 @@ import java.util.Collections;
*/
public class DefaultDownScalingMediaDecoder extends MediaDecoder {
- /**
- * Values {@code <= 0} are ignored, a dimension is considered to be not restrained any limit in such case
- */
- @NonNull
- public static DefaultDownScalingMediaDecoder create(int maxWidth, int 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);
+ /**
+ * Values {@code <= 0} are ignored, a dimension is considered to be not restrained any limit in such case
+ */
+ @NonNull
+ public static DefaultDownScalingMediaDecoder create(int maxWidth, int maxHeight) {
+ return create(Resources.getSystem(), maxWidth, maxHeight);
}
- final OutputStream outputStream;
- try {
- outputStream = new BufferedOutputStream(new FileOutputStream(file, false));
- } catch (FileNotFoundException e) {
- throw new IllegalStateException(e);
+ @NonNull
+ public static DefaultDownScalingMediaDecoder create(
+ @NonNull Resources resources,
+ int maxWidth,
+ int maxHeight
+ ) {
+ return new DefaultDownScalingMediaDecoder(resources, maxWidth, maxHeight);
}
- 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
- }
+ 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;
}
- 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
- private static InputStream readFile(@NonNull File file) {
- try {
- return new BufferedInputStream(new FileInputStream(file));
- } catch (FileNotFoundException e) {
- throw new IllegalStateException(e);
- }
- }
+ final File file = writeToTempFile(inputStream);
+ try {
- // 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 BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
- final boolean hasMaxWidth = maxWidth > 0;
- final boolean hasMaxHeight = maxHeight > 0;
+ // initial result when obtaining bounds is discarded
+ decode(file, options);
- 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;
+ options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
+ options.inJustDecodeBounds = false;
+
+ final Bitmap bitmap = decode(file, options);
+
+ return new BitmapDrawable(resources, bitmap);
+ } finally {
+ // we no longer need the temporary file
+ //noinspection ResultOfMethodCallIgnored
+ file.delete();
+ }
}
- 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) {
- int inSampleSize = 1;
- final int half = actual / 2;
- while ((half / inSampleSize) > max) {
- inSampleSize *= 2;
+ final OutputStream outputStream;
+ try {
+ outputStream = new BufferedOutputStream(new FileOutputStream(file, false));
+ } catch (FileNotFoundException e) {
+ 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
- @Override
- public Collection supportedTypes() {
- return Collections.emptySet();
- }
+ @Nullable
+ private static Bitmap decode(@NonNull File file, @NonNull BitmapFactory.Options options) {
+ final InputStream is = readFile(file);
+ // 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 supportedTypes() {
+ return Collections.emptySet();
+ }
}