Add ImageHandler (img tag)

This commit is contained in:
Dimitry Ivanov 2018-08-19 15:20:21 +03:00
parent f1a38fca0f
commit 2d9c80d519
4 changed files with 318 additions and 9 deletions

View File

@ -67,7 +67,7 @@ public abstract class MarkwonHtmlRenderer {
.handler("a", new LinkHandler()) .handler("a", new LinkHandler())
.handler("ul", listHandler) .handler("ul", listHandler)
.handler("ol", listHandler) .handler("ol", listHandler)
.handler("img", new ImageHandler()); .handler("img", ImageHandler.create());
} }
@NonNull @NonNull

View File

@ -9,9 +9,26 @@ import java.util.Map;
import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag; import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.html2.CssInlineStyleParser;
public class ImageHandler extends SimpleTagHandler { public class ImageHandler extends SimpleTagHandler {
interface ImageSizeParser {
@Nullable
ImageSize parse(@NonNull Map<String, String> 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 @Nullable
@Override @Override
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { 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 // 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) // 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( return configuration.factory().image(
configuration.theme(), configuration.theme(),
destination, destination,
configuration.asyncDrawableLoader(), configuration.asyncDrawableLoader(),
configuration.imageSizeResolver(), configuration.imageSizeResolver(),
parseImageSize(attributes), imageSizeParser.parse(tag.attributes()),
false false
); );
} }
@Nullable
private static ImageSize parseImageSize(@NonNull Map<String, String> 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;
}
} }

View File

@ -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<String, String> 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;
}
}

View File

@ -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.<String, String>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<String, String> attributes = new HashMap<String, String>() {{
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<String, String> attributes = new HashMap<String, String>() {{
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<String, String> attributes = new HashMap<String, String>() {{
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<String, String> attributes = new HashMap<String, String>() {{
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<String, String> attributes = new HashMap<String, String>() {{
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<String, ImageSize.Dimension> map = new HashMap<String, ImageSize.Dimension>() {{
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<String, ImageSize.Dimension> 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);
}
}