From f1a38fca0f995882b96fa0d787679202b351be0b Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 19 Aug 2018 14:10:27 +0300 Subject: [PATCH] Add CssInlineStyleParser --- build.gradle | 3 +- markwon/build.gradle | 6 + .../renderer/html2/CssInlineStyleParser.java | 171 +++++++++++++ .../markwon/renderer/html2/CssProperty.java | 42 +++ .../html2/CssInlineStyleParserTest.java | 239 ++++++++++++++++++ .../ru/noties/markwon/test/TestUtils.java | 17 ++ 6 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 markwon/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java create mode 100644 markwon/src/main/java/ru/noties/markwon/renderer/html2/CssProperty.java create mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/test/TestUtils.java diff --git a/build.gradle b/build.gradle index 83202024..e4df1c6d 100644 --- a/build.gradle +++ b/build.gradle @@ -78,6 +78,7 @@ ext { deps['test'] = [ 'junit' : 'junit:junit:4.12', - 'robolectric': 'org.robolectric:robolectric:3.8' + 'robolectric': 'org.robolectric:robolectric:3.8', + 'ix-java' : 'com.github.akarnokd:ixjava:1.0.0' ] } diff --git a/markwon/build.gradle b/markwon/build.gradle index ca7d529f..1db4a53d 100644 --- a/markwon/build.gradle +++ b/markwon/build.gradle @@ -24,6 +24,12 @@ dependencies { api it['commonmark-strikethrough'] api it['commonmark-table'] } + + deps['test'].with { + testImplementation it['junit'] + testImplementation it['robolectric'] + testImplementation it['ix-java'] + } } afterEvaluate { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java new file mode 100644 index 00000000..9670d018 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java @@ -0,0 +1,171 @@ +package ru.noties.markwon.renderer.html2; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +public abstract class CssInlineStyleParser { + + @NonNull + public abstract Iterable parse(@NonNull String inlineStyle); + + @NonNull + public static CssInlineStyleParser create() { + return new Impl(); + } + + static class Impl extends CssInlineStyleParser { + + @NonNull + @Override + public Iterable parse(@NonNull String inlineStyle) { + return new CssIterable(inlineStyle); + } + + private static class CssIterable implements Iterable { + + private final String input; + + CssIterable(@NonNull String input) { + this.input = input; + } + + @NonNull + @Override + public Iterator iterator() { + return new CssIterator(); + } + + private class CssIterator implements Iterator { + + private final CssProperty cssProperty = new CssProperty(); + + private final StringBuilder builder = new StringBuilder(); + + private final int length = input.length(); + + private int index; + + @Override + public boolean hasNext() { + + prepareNext(); + + return hasNextPrepared(); + } + + @Override + public CssProperty next() { + if (!hasNextPrepared()) { + throw new NoSuchElementException(); + } + return cssProperty; + } + + private void prepareNext() { + + // clear first + cssProperty.set("", ""); + + builder.setLength(0); + + String key = null; + String value = null; + + char c; + + boolean keyHasWhiteSpace = false; + + for (int i = index; i < length; i++) { + + c = input.charAt(i); + + // if we are building KEY, then when we encounter WS (white-space) we finish + // KEY and wait for the ':', if we do not find it and we find EOF or ';' + // we start creating KEY again after the ';' + + if (key == null) { + + if (':' == c) { + + // we have no key yet, but we might have started creating it already + if (builder.length() > 0) { + key = builder.toString().trim(); + } + + builder.setLength(0); + + } else { + // if by any chance we have here the ';' -> reset key and try to match next + if (';' == c) { + builder.setLength(0); + } else { + + // key cannot have WS gaps (but leading and trailing are OK) + if (Character.isWhitespace(c)) { + if (builder.length() > 0) { + keyHasWhiteSpace = true; + } + } else { + // if not a WS and we have found WS before, start a-new + // else append + if (keyHasWhiteSpace) { + // start new filling + builder.setLength(0); + builder.append(c); + // clear this flag + keyHasWhiteSpace = false; + } else { + builder.append(c); + } + } + } + } + } else if (value == null) { + + if (Character.isWhitespace(c)) { + if (builder.length() > 0) { + builder.append(c); + } + } else if (';' == c) { + + value = builder.toString().trim(); + builder.setLength(0); + + // check if we have valid values -> if yes -> return it + if (hasValues(key, value)) { + index = i + 1; + cssProperty.set(key, value); + return; + } + + } else { + builder.append(c); + } + } + } + + // here we must additionally check for EOF (we might be tracking value here) + if (key != null + && builder.length() > 0) { + value = builder.toString().trim(); + cssProperty.set(key, value); + index = length; + } + } + + private boolean hasNextPrepared() { + return hasValues(cssProperty.key(), cssProperty.value()); + } + + private boolean hasValues(@Nullable String key, @Nullable String value) { + return !TextUtils.isEmpty(key) + && !TextUtils.isEmpty(value); + } + } + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssProperty.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssProperty.java new file mode 100644 index 00000000..aa490361 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssProperty.java @@ -0,0 +1,42 @@ +package ru.noties.markwon.renderer.html2; + +import android.support.annotation.NonNull; + +public class CssProperty { + + private String key; + private String value; + + CssProperty() { + } + + void set(@NonNull String key, @NonNull String value) { + this.key = key; + this.value = value; + } + + @NonNull + public String key() { + return key; + } + + @NonNull + public String value() { + return value; + } + + @NonNull + public CssProperty mutate() { + final CssProperty cssProperty = new CssProperty(); + cssProperty.set(this.key, this.value); + return cssProperty; + } + + @Override + public String toString() { + return "CssProperty{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java new file mode 100644 index 00000000..4ba3fffb --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java @@ -0,0 +1,239 @@ +package ru.noties.markwon.renderer.html2; + +import android.support.annotation.NonNull; + +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.HashMap; +import java.util.List; +import java.util.Map; + +import ix.Ix; +import ix.IxFunction; +import ru.noties.markwon.test.TestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static ru.noties.markwon.test.TestUtils.with; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class CssInlineStyleParserTest { + + private CssInlineStyleParser.Impl impl; + + @Before + public void before() { + impl = new CssInlineStyleParser.Impl(); + } + + @Test + public void simple_single_pair() { + + final String input = "key: value;"; + + final List list = listProperties(input); + + assertEquals(1, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key", cssProperty.key()); + assertEquals("value", cssProperty.value()); + } + }); + } + + @Test + public void simple_two_pairs() { + + final String input = "key1: value1; key2: value2;"; + + final List list = listProperties(input); + + assertEquals(2, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key1", cssProperty.key()); + assertEquals("value1", cssProperty.value()); + } + }); + + with(list.get(1), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key2", cssProperty.key()); + assertEquals("value2", cssProperty.value()); + } + }); + } + + @Test + public void one_pair_eof() { + + final String input = "key: value"; + final List list = listProperties(input); + assertEquals(1, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key", cssProperty.key()); + assertEquals("value", cssProperty.value()); + } + }); + } + + @Test + public void one_pair_eof_whitespaces() { + + final String input = "key: value \n\n\t"; + final List list = listProperties(input); + assertEquals(1, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key", cssProperty.key()); + assertEquals("value", cssProperty.value()); + } + }); + } + + @Test + public void white_spaces() { + + final String input = "\n\n\n\t \t key1 \n\n\n\t : \n\n\n\n \t value1 \n\n\n\n ; \n key2\n : \n value2 \n ; "; + final List list = listProperties(input); + assertEquals(2, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key1", cssProperty.key()); + assertEquals("value1", cssProperty.value()); + } + }); + + with(list.get(1), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key2", cssProperty.key()); + assertEquals("value2", cssProperty.value()); + } + }); + } + + @Test + public void list_of_keys() { + + final String input = "key1 key2 key3 key4"; + final List list = listProperties(input); + + assertEquals(0, list.size()); + } + + @Test + public void list_of_keys_and_value() { + + final String input = "key1 key2 key3 key4: value4"; + final List list = listProperties(input); + assertEquals(1, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key4", cssProperty.key()); + assertEquals("value4", cssProperty.value()); + } + }); + } + + @Test + public void list_of_keys_separated_by_semi_colon() { + + final String input = "key1;key2;key3;key4;"; + final List list = listProperties(input); + assertEquals(0, list.size()); + } + + @Test + public void key_value_with_invalid_between() { + + final String input = "key1: value1; key2 key3: value3;"; + final List list = listProperties(input); + + assertEquals(2, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key1", cssProperty.key()); + assertEquals("value1", cssProperty.value()); + } + }); + + with(list.get(1), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key3", cssProperty.key()); + assertEquals("value3", cssProperty.value()); + } + }); + } + + @Test + public void css_functions() { + + final Map map = new HashMap() {{ + put("attr", "\" (\" attr(href) \")\""); + put("calc", "calc(100% - 100px)"); + put("cubic-bezier", "cubic-bezier(0.1, 0.7, 1.0, 0.1)"); + put("hsl", "hsl(120,100%,50%)"); + put("hsla", "hsla(120,100%,50%,0.3)"); + put("linear-gradient", "linear-gradient(red, yellow, blue)"); + put("radial-gradient", "radial-gradient(red, green, blue)"); + put("repeating-linear-gradient", "repeating-linear-gradient(red, yellow 10%, green 20%)"); + put("repeating-radial-gradient", "repeating-radial-gradient(red, yellow 10%, green 15%)"); + put("rgb", "rgb(255,0,0)"); + put("rgba", "rgba(255,0,0,0.3)"); + put("var", "var(--some-variable)"); + put("url", "url(\"url.gif\")"); + }}; + + final StringBuilder builder = new StringBuilder(); + for (Map.Entry entry: map.entrySet()) { + builder.append(entry.getKey()) + .append(':') + .append(entry.getValue()) + .append(';'); + } + + for (CssProperty cssProperty: impl.parse(builder.toString())) { + final String value = map.remove(cssProperty.key()); + assertNotNull(cssProperty.key(), value); + assertEquals(cssProperty.key(), value, cssProperty.value()); + } + + assertEquals(0, map.size()); + } + + @NonNull + private List listProperties(@NonNull String input) { + return Ix.from(impl.parse(input)) + .map(new IxFunction() { + @Override + public CssProperty apply(CssProperty cssProperty) { + return cssProperty.mutate(); + } + }) + .toList(); + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/test/TestUtils.java b/markwon/src/test/java/ru/noties/markwon/test/TestUtils.java new file mode 100644 index 00000000..4a8f3890 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/test/TestUtils.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; + +public abstract class TestUtils { + + public interface Action { + void apply(@NonNull T t); + } + + public static void with(@NonNull T t, @NonNull Action action) { + action.apply(t); + } + + private TestUtils() { + } +}