From 7881420cbd78b3ef65fc0a677e105d8bc117ef72 Mon Sep 17 00:00:00 2001
From: Dimitry Ivanov <mail@dimitryivanov.ru>
Date: Fri, 24 Aug 2018 23:10:47 +0300
Subject: [PATCH] Add html file test

---
 .../html/impl/MarkwonHtmlParserImpl.java      |  23 +++-
 .../html/impl/MarkwonHtmlParserImplTest.java  |  24 ++++
 .../renderer/html2/MarkwonHtmlRenderer.java   |   9 +-
 .../renderer/html2/tag/HeadingHandler.java    |  22 ++++
 .../renderer/html2/tag/LinkHandler.java       |   2 +-
 .../visitor/SpannableMarkdownVisitorTest.java |  65 ++++++++++-
 .../markwon/renderer/visitor/TestFactory.java |   2 +-
 markwon/src/test/resources/tests/html.yaml    | 105 ++++++++++++++++++
 .../src/test/resources/tests/single-img.yaml  |   2 +-
 9 files changed, 244 insertions(+), 10 deletions(-)
 create mode 100644 markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java
 create mode 100644 markwon/src/test/resources/tests/html.yaml

diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java b/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java
index 791147d2..86f985a2 100644
--- a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java
+++ b/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java
@@ -119,9 +119,9 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
 
     private boolean isInsidePreTag;
 
-    private Tokeniser tokeniser;
-
-    private CharacterReader reader;
+    // the thing is: we ensure a new line BEFORE block tag
+    // but not after, so another tag will be placed on the same line (which is wrong)
+    private boolean previousIsBlock;
 
 
     MarkwonHtmlParserImpl(
@@ -242,6 +242,8 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
 
         final HtmlTagImpl.InlineImpl inline = new HtmlTagImpl.InlineImpl(name, output.length(), extractAttributes(startTag));
 
+        ensureNewLineIfPreviousWasBlock(output);
+
         if (isVoidTag(name)
                 || startTag.selfClosing) {
 
@@ -305,6 +307,8 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
         if (isBlockTag(name)) {
             isInsidePreTag = "pre".equals(name);
             ensureNewLine(output);
+        } else {
+            ensureNewLineIfPreviousWasBlock(output);
         }
 
         final int start = output.length();
@@ -350,6 +354,11 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
 
             block.closeAt(output.length());
 
+            // if it's empty -> we do no care about if it's block or not
+            if (!block.isEmpty()) {
+                previousIsBlock = isBlockTag(block.name);
+            }
+
             if (TAG_PARAGRAPH.equals(name)) {
                 appendQuietly(output, '\n');
             }
@@ -368,6 +377,7 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
         if (isInsidePreTag) {
             appendQuietly(output, character.getData());
         } else {
+            ensureNewLineIfPreviousWasBlock(output);
             trimmingAppender.append(output, character.getData());
         }
     }
@@ -410,6 +420,13 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
         return blockTag;
     }
 
+    protected <T extends Appendable & CharSequence> void ensureNewLineIfPreviousWasBlock(@NonNull T output) {
+        if (previousIsBlock) {
+            ensureNewLine(output);
+            previousIsBlock = false;
+        }
+    }
+
     // name here must lower case
     protected static boolean isInlineTag(@NonNull String name) {
         return INLINE_TAGS.contains(name);
diff --git a/markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java b/markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java
index 3edcfeb1..ad0669b3 100644
--- a/markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java
+++ b/markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java
@@ -833,6 +833,30 @@ public class MarkwonHtmlParserImplTest {
         });
     }
 
+    @Test
+    public void newLineAfterBlockTag() {
+
+        final MarkwonHtmlParserImpl impl = MarkwonHtmlParserImpl.create();
+        final StringBuilder output = new StringBuilder();
+
+        final String[] fragments = {
+                "<h1>head #1</h1>just text",
+                "<h2>head #2</h2><span>in span tag</span>",
+                "<h3>head #3</h3><custom-tag>in custom-tag</custom-tag>"
+        };
+
+        for (String fragment: fragments) {
+            impl.processFragment(output, fragment);
+        }
+
+        final String expected = "" +
+                "head #1\njust text\n" +
+                "head #2\nin span tag\n" +
+                "head #3\nin custom-tag";
+
+        assertEquals(expected, output.toString());
+    }
+
     private static class CaptureTagsAction<T> implements MarkwonHtmlParser.FlushAction<T> {
 
         boolean called;
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 338ab211..bd69445a 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
@@ -13,6 +13,7 @@ import ru.noties.markwon.SpannableConfiguration;
 import ru.noties.markwon.html.api.MarkwonHtmlParser;
 import ru.noties.markwon.renderer.html2.tag.BlockquoteHandler;
 import ru.noties.markwon.renderer.html2.tag.EmphasisHandler;
+import ru.noties.markwon.renderer.html2.tag.HeadingHandler;
 import ru.noties.markwon.renderer.html2.tag.ImageHandler;
 import ru.noties.markwon.renderer.html2.tag.LinkHandler;
 import ru.noties.markwon.renderer.html2.tag.ListHandler;
@@ -69,7 +70,13 @@ public abstract class MarkwonHtmlRenderer {
                 .handler("ul", listHandler)
                 .handler("ol", listHandler)
                 .handler("img", ImageHandler.create())
-                .handler("blockquote", new BlockquoteHandler());
+                .handler("blockquote", new BlockquoteHandler())
+                .handler("h1", new HeadingHandler(1))
+                .handler("h2", new HeadingHandler(2))
+                .handler("h3", new HeadingHandler(3))
+                .handler("h4", new HeadingHandler(4))
+                .handler("h5", new HeadingHandler(5))
+                .handler("h6", new HeadingHandler(6));
     }
 
     @NonNull
diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java
new file mode 100644
index 00000000..e2138b05
--- /dev/null
+++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java
@@ -0,0 +1,22 @@
+package ru.noties.markwon.renderer.html2.tag;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import ru.noties.markwon.SpannableConfiguration;
+import ru.noties.markwon.html.api.HtmlTag;
+
+public class HeadingHandler extends SimpleTagHandler {
+
+    private final int level;
+
+    public HeadingHandler(int level) {
+        this.level = level;
+    }
+
+    @Nullable
+    @Override
+    public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
+        return configuration.factory().heading(configuration.theme(), level);
+    }
+}
diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java
index faa952bc..134874b9 100644
--- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java
+++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java
@@ -11,7 +11,7 @@ public class LinkHandler extends SimpleTagHandler {
     @Nullable
     @Override
     public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
-        final String destination = tag.attributes().get("src");
+        final String destination = tag.attributes().get("href");
         if (!TextUtils.isEmpty(destination)) {
             return configuration.factory().link(
                     configuration.theme(),
diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java
index c47bccdf..ca9de641 100644
--- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java
+++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java
@@ -1,15 +1,20 @@
 package ru.noties.markwon.renderer.visitor;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.text.SpannableStringBuilder;
 
 import org.commonmark.node.Node;
+import org.junit.ComparisonFailure;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.ParameterizedRobolectricTestRunner;
 import org.robolectric.annotation.Config;
 
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
 
 import ix.Ix;
 import ix.IxPredicate;
@@ -55,13 +60,16 @@ public class SpannableMarkdownVisitorTest {
 
         final SpannableStringBuilder stringBuilder = builder.spannableStringBuilder();
 
-        System.out.printf("%n%s%n", stringBuilder);
+        System.out.printf("%s: %s%n", file, Arrays.toString(stringBuilder.getSpans(0, stringBuilder.length(), Object.class)));
 
         int index = 0;
 
         for (TestNode testNode : data.output()) {
             index = validate(stringBuilder, index, testNode);
         }
+
+        // assert that the whole thing is processed
+        assertEquals(stringBuilder.length(), index);
     }
 
     private int validate(@NonNull SpannableStringBuilder builder, int index, @NonNull TestNode node) {
@@ -103,6 +111,8 @@ public class SpannableMarkdownVisitorTest {
 
         final String info = node.toString();
 
+        System.out.printf("%s: %s%n", file, builder.subSequence(index, out));
+
         // we can possibly have parent spans here, should filter them
         final Object[] spans = builder.getSpans(index, out, Object.class);
         assertTrue(info, spans != null);
@@ -123,14 +133,18 @@ public class SpannableMarkdownVisitorTest {
                 })
                 .first(null);
 
-        assertNotNull(info, testSpan);
+        assertNotNull(
+                format("info: %s, spans: %s", info, Arrays.toString(spans)),
+                testSpan
+        );
 
         assertEquals(info, span.name(), testSpan.name());
-        assertEquals(info, span.attributes(), testSpan.attributes());
+        assertMapEquals(info, span.attributes(), testSpan.attributes());
 
         return out;
     }
 
+    @SuppressWarnings("ConstantConditions")
     @NonNull
     private SpannableConfiguration configuration(@NonNull TestConfig config) {
 
@@ -147,4 +161,49 @@ public class SpannableMarkdownVisitorTest {
                 .factory(factory)
                 .build();
     }
+
+    private static void assertMapEquals(
+            @NonNull String message,
+            @NonNull Map<String, String> expected,
+            @NonNull Map<String, String> actual) {
+        boolean result = true;
+        if (expected.size() == actual.size()) {
+            for (Map.Entry<String, String> entry : expected.entrySet()) {
+                if (!actual.containsKey(entry.getKey())
+                        || !equals(entry.getValue(), actual.get(entry.getKey()))) {
+                    result = false;
+                    break;
+                }
+            }
+        }
+        if (!result) {
+            final Comparator<Map.Entry<String, String>> comparator = new Comparator<Map.Entry<String, String>>() {
+                @Override
+                public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
+                    return o1.getKey().compareTo(o2.getKey());
+                }
+            };
+            final String e = Ix.from(expected.entrySet())
+                    .orderBy(comparator)
+                    .toList()
+                    .toString();
+            final String a = Ix.from(actual.entrySet())
+                    .orderBy(comparator)
+                    .toList()
+                    .toString();
+            throw new ComparisonFailure(message, e, a);
+        }
+    }
+
+    private static boolean equals(@Nullable Object o1, @Nullable Object o2) {
+        return o1 != null
+                ? o1.equals(o2)
+                : o2 == null;
+
+    }
+
+    @NonNull
+    private static String format(@NonNull String message, Object... args) {
+        return String.format(message, args);
+    }
 }
\ No newline at end of file
diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java
index e87c6f34..89a0f646 100644
--- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java
+++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java
@@ -186,7 +186,7 @@ class TestFactory implements SpannableFactory {
         final int length = pairs.length;
         final Map<String, String> map = new HashMap<>(length);
         for (Pair pair : pairs) {
-            map.put(pair.key, String.valueOf(pair.value));
+            map.put(pair.key, pair.value == null ? null : String.valueOf(pair.value));
         }
         return map;
     }
diff --git a/markwon/src/test/resources/tests/html.yaml b/markwon/src/test/resources/tests/html.yaml
new file mode 100644
index 00000000..d9f966f1
--- /dev/null
+++ b/markwon/src/test/resources/tests/html.yaml
@@ -0,0 +1,105 @@
+input: |-
+  <h1>html</h1>
+  <h2>emphasis</h2>
+  <i>i</i><em>em</em><cite>cite</cite><dfn>dfn</dfn>
+  <h2>strong-emphasis</h2>
+  <b>b</b><strong>strong</strong>
+  <h2>super-script</h2>
+  <sup>sup</sup>
+  <h2>sub-script</h2>
+  <sub>sub</sub>
+  <h2>underline</h2>
+  <u>u</u><ins>ins</ins>
+  <h2>strike</h2>
+  <s>s</s><del>del</del>
+  <h2>link</h2>
+  <a href="a://href">a</a>
+  <h2>unordered-list</h2>
+  <ul><li>ul1<li>ul2</ul>
+  <h2>ordered-list</h2>
+  <ol><li>ol1<li>ol2</ol>
+  <h2>image</h2>
+  <img src="img://src" alt="img">
+  <h2>blockquote</h2>
+  <blockquote>blockquote</blockquote>
+  <h3>3</h3>
+  <h4>4</h4>
+  <h5>5</h5>
+  <h6>6</h6>
+
+config:
+  use-html: true
+
+output:
+  - h1: "html"
+  - text: "\n"
+  - h2: "emphasis"
+  - text: "\n"
+  - i: "i"
+  - i: "em"
+  - i: "cite"
+  - i: "dfn"
+  - text: "\n"
+  - h2: "strong-emphasis"
+  - text: "\n"
+  - b: "b"
+  - b: "strong"
+  - text: "\n"
+  - h2: "super-script"
+  - text: "\n"
+  - sup: "sup"
+  - text: "\n"
+  - h2: "sub-script"
+  - text: "\n"
+  - sub: "sub"
+  - text: "\n"
+  - h2: "underline"
+  - text: "\n"
+  - u: "u"
+  - u: "ins"
+  - text: "\n"
+  - h2: "strike"
+  - text: "\n"
+  - s: "s"
+  - s: "del"
+  - text: "\n"
+  - h2: "link"
+  - text: "\n"
+  - a: "a"
+    attrs:
+      href: "a://href"
+  - text: "\n"
+  - h2: "unordered-list"
+  - text: "\n"
+  - ul: "ul1"
+  - text: "\n"
+  - ul: "ul2"
+  - text: "\n"
+  - h2: "ordered-list"
+  - text: "\n"
+  - ol: "ol1"
+    attrs:
+      start: 1
+  - text: "\n"
+  - ol: "ol2"
+    attrs:
+      start: 2
+  - text: "\n"
+  - h2: "image"
+  - text: "\n"
+  - img: "img"
+    attrs:
+      src: "img://src"
+  - text: "\n"
+  - h2: "blockquote"
+  - text: "\n"
+  - blockquote: "blockquote"
+  - text: "\n"
+  - h3: "3"
+  - text: "\n"
+  - h4: "4"
+  - text: "\n"
+  - h5: "5"
+  - text: "\n"
+  - h6: "6"
+
diff --git a/markwon/src/test/resources/tests/single-img.yaml b/markwon/src/test/resources/tests/single-img.yaml
index 4ff5a02c..fd5a1343 100644
--- a/markwon/src/test/resources/tests/single-img.yaml
+++ b/markwon/src/test/resources/tests/single-img.yaml
@@ -3,6 +3,6 @@ input: "![image](#href)"
 output:
   - img: "image"
     attrs:
-      scr: "#href"
+      src: "#href"
       imageSize: null
       replacementTextIsLink: false
\ No newline at end of file