From 2d9c80d5197e37eed47af7238da21c94153d5cce Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 19 Aug 2018 15:20:21 +0300 Subject: [PATCH] Add ImageHandler (img tag) --- .../renderer/html2/MarkwonHtmlRenderer.java | 2 +- .../renderer/html2/tag/ImageHandler.java | 29 ++- .../html2/tag/ImageSizeParserImpl.java | 110 +++++++++++ .../html2/tag/ImageSizeParserImplTest.java | 186 ++++++++++++++++++ 4 files changed, 318 insertions(+), 9 deletions(-) create mode 100644 markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImpl.java create mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java index 5910eb5a..8405854b 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java @@ -67,7 +67,7 @@ public abstract class MarkwonHtmlRenderer { .handler("a", new LinkHandler()) .handler("ul", listHandler) .handler("ol", listHandler) - .handler("img", new ImageHandler()); + .handler("img", ImageHandler.create()); } @NonNull diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java index 6a65de7b..ed5f7f3f 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java @@ -9,9 +9,26 @@ import java.util.Map; import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.html.api.HtmlTag; import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.renderer.html2.CssInlineStyleParser; public class ImageHandler extends SimpleTagHandler { + interface ImageSizeParser { + @Nullable + ImageSize parse(@NonNull Map attributes); + } + + @NonNull + public static ImageHandler create() { + return new ImageHandler(new ImageSizeParserImpl(CssInlineStyleParser.create())); + } + + private final ImageSizeParser imageSizeParser; + + ImageHandler(@NonNull ImageSizeParser imageSizeParser) { + this.imageSizeParser = imageSizeParser; + } + @Nullable @Override public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { @@ -26,21 +43,17 @@ public class ImageHandler extends SimpleTagHandler { // todo: replacement text is link... as we are not at block level // and cannot inspect the parent of this node... (img and a are both inlines) + // + // but we can look and see if we are inside a LinkSpan (will have to extend TagHandler + // to obtain an instance SpannableBuilder for inspection) return configuration.factory().image( configuration.theme(), destination, configuration.asyncDrawableLoader(), configuration.imageSizeResolver(), - parseImageSize(attributes), + imageSizeParser.parse(tag.attributes()), false ); } - - @Nullable - private static ImageSize parseImageSize(@NonNull Map attributes) { - // strictly speaking percents when specified directly on an attribute - // are not part of the HTML spec (I couldn't find any reference) - return null; - } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImpl.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImpl.java new file mode 100644 index 00000000..56ad13c0 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImpl.java @@ -0,0 +1,110 @@ +package ru.noties.markwon.renderer.html2.tag; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; + +import java.util.Map; + +import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.renderer.html2.CssInlineStyleParser; +import ru.noties.markwon.renderer.html2.CssProperty; + +class ImageSizeParserImpl implements ImageHandler.ImageSizeParser { + + private final CssInlineStyleParser inlineStyleParser; + + ImageSizeParserImpl(@NonNull CssInlineStyleParser inlineStyleParser) { + this.inlineStyleParser = inlineStyleParser; + } + + @Override + public ImageSize parse(@NonNull Map attributes) { + + // strictly speaking percents when specified directly on an attribute + // are not part of the HTML spec (I couldn't find any reference) + + ImageSize.Dimension width = null; + ImageSize.Dimension height = null; + + // okay, let's first check styles + final String style = attributes.get("style"); + + if (!TextUtils.isEmpty(style)) { + + String key; + + for (CssProperty cssProperty : inlineStyleParser.parse(style)) { + + key = cssProperty.key(); + + if ("width".equals(key)) { + width = dimension(cssProperty.value()); + } else if ("height".equals(key)) { + height = dimension(cssProperty.value()); + } + + if (width != null + && height != null) { + break; + } + } + } + + if (width != null + && height != null) { + return new ImageSize(width, height); + } + + // check tag attributes + if (width == null) { + width = dimension(attributes.get("width")); + } + + if (height == null) { + height = dimension(attributes.get("height")); + } + + if (width == null + && height == null) { + return null; + } + + return new ImageSize(width, height); + } + + @Nullable + @VisibleForTesting + ImageSize.Dimension dimension(@Nullable String value) { + + if (TextUtils.isEmpty(value)) { + return null; + } + + final int length = value.length(); + + for (int i = length - 1; i > -1; i--) { + + if (Character.isDigit(value.charAt(i))) { + + try { + final float val = Float.parseFloat(value.substring(0, i + 1)); + final String unit; + if (i == length - 1) { + // no unit info + unit = null; + } else { + unit = value.substring(i + 1, length); + } + return new ImageSize.Dimension(val, unit); + } catch (NumberFormatException e) { + // value cannot not be represented as a float + return null; + } + } + } + + return null; + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java new file mode 100644 index 00000000..23d7eb93 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java @@ -0,0 +1,186 @@ +package ru.noties.markwon.renderer.html2.tag; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +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.Collections; +import java.util.HashMap; +import java.util.Map; + +import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.renderer.html2.CssInlineStyleParser; + +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 ImageSizeParserImplTest { + + private static final float DELTA = 1e-7F; + + private ImageSizeParserImpl impl; + + @Before + public void before() { + impl = new ImageSizeParserImpl(CssInlineStyleParser.create()); + } + + @Test + public void nothing() { + assertNull(impl.parse(Collections.emptyMap())); + } + + @Test + public void width_height_from_style() { + + final String style = "width: 123; height: 321"; + + assertImageSize( + new ImageSize(dimension(123, null), dimension(321, null)), + impl.parse(Collections.singletonMap("style", style)) + ); + } + + @Test + public void style_has_higher_priority_width() { + + // if property is found in styles, do not lookup raw attribute + final Map attributes = new HashMap() {{ + put("style", "width: 43"); + put("width", "991"); + }}; + + assertImageSize( + new ImageSize(dimension(43, null), null), + impl.parse(attributes) + ); + } + + @Test + public void style_has_higher_priority_height() { + + // if property is found in styles, do not lookup raw attribute + final Map attributes = new HashMap() {{ + put("style", "height: 177"); + put("height", "8"); + }}; + + assertImageSize( + new ImageSize(null, dimension(177, null)), + impl.parse(attributes) + ); + } + + @Test + public void width_style_height_attributes() { + + final Map attributes = new HashMap() {{ + put("style", "width: 99"); + put("height", "7"); + }}; + + assertImageSize( + new ImageSize(dimension(99, null), dimension(7, null)), + impl.parse(attributes) + ); + } + + @Test + public void height_style_width_attributes() { + + final Map attributes = new HashMap() {{ + put("style", "height: 15"); + put("width", "88"); + }}; + + assertImageSize( + new ImageSize(dimension(88, null), dimension(15, null)), + impl.parse(attributes) + ); + } + + @Test + public void non_empty_styles_width_height_attributes() { + + final Map attributes = new HashMap() {{ + put("style", "key1: value1; width0: 123; height0: 99"); + put("width", "40"); + put("height", "77"); + }}; + + assertImageSize( + new ImageSize(dimension(40, null), dimension(77, null)), + impl.parse(attributes) + ); + } + + @Test + public void dimension_units() { + + final Map map = new HashMap() {{ + put("100", dimension(100, null)); + put("100%", dimension(100, "%")); + put("1%", dimension(1, "%")); + put("0.2em", dimension(0.2F, "em")); + put("155px", dimension(155, "px")); + put("67blah", dimension(67, "blah")); + put("-1", dimension(-1, null)); + put("-0.01pt", dimension(-0.01F, "pt")); + }}; + + for (Map.Entry entry : map.entrySet()) { + assertDimension(entry.getKey(), entry.getValue(), impl.dimension(entry.getKey())); + } + } + + @Test + public void bad_dimension() { + + final String[] dimensions = { + "calc(5px + 10rem)", + "whataver6", + "165 165", + "!@#$%^&*(%" + }; + + for (String dimension: dimensions) { + assertNull(dimension, impl.dimension(dimension)); + } + } + + private static void assertImageSize(@Nullable ImageSize expected, @Nullable ImageSize actual) { + if (expected == null) { + assertNull(actual); + } else { + assertNotNull(actual); + assertDimension("width", expected.width, actual.width); + assertDimension("height", expected.height, actual.height); + } + } + + private static void assertDimension( + @NonNull String name, + @Nullable ImageSize.Dimension expected, + @Nullable ImageSize.Dimension actual) { + if (expected == null) { + assertNull(name, actual); + } else { + assertNotNull(name, actual); + assertEquals(name, expected.value, actual.value, DELTA); + assertEquals(name, expected.unit, actual.unit); + } + } + + @NonNull + private static ImageSize.Dimension dimension(float value, @Nullable String unit) { + return new ImageSize.Dimension(value, unit); + } +} \ No newline at end of file