Add CssInlineStyleParser

This commit is contained in:
Dimitry Ivanov 2018-08-19 14:10:27 +03:00
parent 23b95e70b9
commit f1a38fca0f
6 changed files with 477 additions and 1 deletions

View File

@ -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'
]
}

View File

@ -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 {

View File

@ -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<CssProperty> parse(@NonNull String inlineStyle);
@NonNull
public static CssInlineStyleParser create() {
return new Impl();
}
static class Impl extends CssInlineStyleParser {
@NonNull
@Override
public Iterable<CssProperty> parse(@NonNull String inlineStyle) {
return new CssIterable(inlineStyle);
}
private static class CssIterable implements Iterable<CssProperty> {
private final String input;
CssIterable(@NonNull String input) {
this.input = input;
}
@NonNull
@Override
public Iterator<CssProperty> iterator() {
return new CssIterator();
}
private class CssIterator implements Iterator<CssProperty> {
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);
}
}
}
}
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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<CssProperty> list = listProperties(input);
assertEquals(1, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@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<CssProperty> list = listProperties(input);
assertEquals(2, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key1", cssProperty.key());
assertEquals("value1", cssProperty.value());
}
});
with(list.get(1), new TestUtils.Action<CssProperty>() {
@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<CssProperty> list = listProperties(input);
assertEquals(1, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@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<CssProperty> list = listProperties(input);
assertEquals(1, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@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<CssProperty> list = listProperties(input);
assertEquals(2, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key1", cssProperty.key());
assertEquals("value1", cssProperty.value());
}
});
with(list.get(1), new TestUtils.Action<CssProperty>() {
@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<CssProperty> 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<CssProperty> list = listProperties(input);
assertEquals(1, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@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<CssProperty> 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<CssProperty> list = listProperties(input);
assertEquals(2, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key1", cssProperty.key());
assertEquals("value1", cssProperty.value());
}
});
with(list.get(1), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key3", cssProperty.key());
assertEquals("value3", cssProperty.value());
}
});
}
@Test
public void css_functions() {
final Map<String, String> map = new HashMap<String, String>() {{
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<String, String> 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<CssProperty> listProperties(@NonNull String input) {
return Ix.from(impl.parse(input))
.map(new IxFunction<CssProperty, CssProperty>() {
@Override
public CssProperty apply(CssProperty cssProperty) {
return cssProperty.mutate();
}
})
.toList();
}
}

View File

@ -0,0 +1,17 @@
package ru.noties.markwon.test;
import android.support.annotation.NonNull;
public abstract class TestUtils {
public interface Action<T> {
void apply(@NonNull T t);
}
public static <T> void with(@NonNull T t, @NonNull Action<T> action) {
action.apply(t);
}
private TestUtils() {
}
}