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(); + } }