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 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 = { + "

head #1

just text", + "

head #2

in span tag", + "

head #3

in 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 implements MarkwonHtmlParser.FlushAction { 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 expected, + @NonNull Map actual) { + boolean result = true; + if (expected.size() == actual.size()) { + for (Map.Entry entry : expected.entrySet()) { + if (!actual.containsKey(entry.getKey()) + || !equals(entry.getValue(), actual.get(entry.getKey()))) { + result = false; + break; + } + } + } + if (!result) { + final Comparator> comparator = new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry 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 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: |- +

html

+

emphasis

+ iemcitedfn +

strong-emphasis

+ bstrong +

super-script

+ sup +

sub-script

+ sub +

underline

+ uins +

strike

+ sdel +

link

+ a +

unordered-list

+
  • ul1
  • ul2
+

ordered-list

+
  1. ol1
  2. ol2
+

image

+ img +

blockquote

+
blockquote
+

3

+

4

+
5
+
6
+ +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