Introduce trimming html text logic
This commit is contained in:
parent
617a1c8d8f
commit
019ed61e69
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22)
|
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22)
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22)
|
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22)
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax%22)
|
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22)
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22)
|
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22)
|
||||||
|
|
||||||
**Markwon** is a library for Android that renders markdown as system-native Spannables. It gives ability to display markdown in all TextView widgets (**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Notifications**, **Toasts**, etc. <u>**No WebView is required**</u>. Library provides reasonable defaults for display style of markdown but also gives all the means to tweak the appearance if desired. All markdown features are supported (including limited support for inlined HTML code, markdown tables and images).
|
**Markwon** is a library for Android that renders markdown as system-native Spannables. It gives ability to display markdown in all TextView widgets (**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Notifications**, **Toasts**, etc. <u>**No WebView is required**</u>. Library provides reasonable defaults for display style of markdown but also gives all the means to tweak the appearance if desired. All markdown features are supported (including limited support for inlined HTML code, markdown tables and images).
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package ru.noties.markwon.html.impl;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
abstract class AppendableUtils {
|
||||||
|
|
||||||
|
static void appendQuietly(@NonNull Appendable appendable, char c) {
|
||||||
|
try {
|
||||||
|
appendable.append(c);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void appendQuietly(@NonNull Appendable appendable, @NonNull CharSequence cs) {
|
||||||
|
try {
|
||||||
|
appendable.append(cs);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void appendQuietly(@NonNull Appendable appendable, @NonNull CharSequence cs, int start, int end) {
|
||||||
|
try {
|
||||||
|
appendable.append(cs, start, end);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppendableUtils() {
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@ import android.support.annotation.NonNull;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -26,6 +25,8 @@ import ru.noties.markwon.html.impl.jsoup.parser.ParseErrorList;
|
|||||||
import ru.noties.markwon.html.impl.jsoup.parser.Token;
|
import ru.noties.markwon.html.impl.jsoup.parser.Token;
|
||||||
import ru.noties.markwon.html.impl.jsoup.parser.Tokeniser;
|
import ru.noties.markwon.html.impl.jsoup.parser.Tokeniser;
|
||||||
|
|
||||||
|
import static ru.noties.markwon.html.impl.AppendableUtils.appendQuietly;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@ -38,7 +39,7 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static MarkwonHtmlParserImpl create(@NonNull HtmlEmptyTagReplacement inlineTagReplacement) {
|
public static MarkwonHtmlParserImpl create(@NonNull HtmlEmptyTagReplacement inlineTagReplacement) {
|
||||||
return new MarkwonHtmlParserImpl(inlineTagReplacement);
|
return new MarkwonHtmlParserImpl(inlineTagReplacement, TrimmingAppender.create());
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
|
||||||
@ -57,9 +58,6 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
private static final String TAG_PARAGRAPH = "p";
|
private static final String TAG_PARAGRAPH = "p";
|
||||||
private static final String TAG_LIST_ITEM = "li";
|
private static final String TAG_LIST_ITEM = "li";
|
||||||
|
|
||||||
// todo: make it configurable
|
|
||||||
// private static final String IMG_REPLACEMENT = "\uFFFC";
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
INLINE_TAGS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
INLINE_TAGS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
||||||
"a", "abbr", "acronym",
|
"a", "abbr", "acronym",
|
||||||
@ -113,12 +111,19 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
|
|
||||||
private final HtmlEmptyTagReplacement emptyTagReplacement;
|
private final HtmlEmptyTagReplacement emptyTagReplacement;
|
||||||
|
|
||||||
|
private final TrimmingAppender trimmingAppender;
|
||||||
|
|
||||||
private final List<HtmlTagImpl.InlineImpl> inlineTags = new ArrayList<>(0);
|
private final List<HtmlTagImpl.InlineImpl> inlineTags = new ArrayList<>(0);
|
||||||
|
|
||||||
private HtmlTagImpl.BlockImpl currentBlock = HtmlTagImpl.BlockImpl.root();
|
private HtmlTagImpl.BlockImpl currentBlock = HtmlTagImpl.BlockImpl.root();
|
||||||
|
|
||||||
MarkwonHtmlParserImpl(@NonNull HtmlEmptyTagReplacement replacement) {
|
private boolean isInsidePreTag;
|
||||||
|
|
||||||
|
MarkwonHtmlParserImpl(
|
||||||
|
@NonNull HtmlEmptyTagReplacement replacement,
|
||||||
|
@NonNull TrimmingAppender trimmingAppender) {
|
||||||
this.emptyTagReplacement = replacement;
|
this.emptyTagReplacement = replacement;
|
||||||
|
this.trimmingAppender = trimmingAppender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -237,7 +242,7 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
final String replacement = emptyTagReplacement.replace(startTag);
|
final String replacement = emptyTagReplacement.replace(startTag);
|
||||||
if (replacement != null
|
if (replacement != null
|
||||||
&& replacement.length() > 0) {
|
&& replacement.length() > 0) {
|
||||||
append(output, replacement);
|
appendQuietly(output, replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the thing is: we will keep this inline tag in the list,
|
// the thing is: we will keep this inline tag in the list,
|
||||||
@ -276,7 +281,7 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
// it must be closed here not matter what we are as here we _assume_
|
// it must be closed here not matter what we are as here we _assume_
|
||||||
// that it's a block tag
|
// that it's a block tag
|
||||||
currentBlock.closeAt(output.length());
|
currentBlock.closeAt(output.length());
|
||||||
append(output, "\n");
|
appendQuietly(output, '\n');
|
||||||
currentBlock = currentBlock.parent;
|
currentBlock = currentBlock.parent;
|
||||||
} else if (TAG_LIST_ITEM.equals(name)
|
} else if (TAG_LIST_ITEM.equals(name)
|
||||||
&& TAG_LIST_ITEM.equals(currentBlock.name)) {
|
&& TAG_LIST_ITEM.equals(currentBlock.name)) {
|
||||||
@ -286,6 +291,7 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isBlockTag(name)) {
|
if (isBlockTag(name)) {
|
||||||
|
isInsidePreTag = "pre".equals(name);
|
||||||
ensureNewLine(output);
|
ensureNewLine(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,7 +304,7 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
final String replacement = emptyTagReplacement.replace(startTag);
|
final String replacement = emptyTagReplacement.replace(startTag);
|
||||||
if (replacement != null
|
if (replacement != null
|
||||||
&& replacement.length() > 0) {
|
&& replacement.length() > 0) {
|
||||||
append(output, replacement);
|
appendQuietly(output, replacement);
|
||||||
}
|
}
|
||||||
block.closeAt(output.length());
|
block.closeAt(output.length());
|
||||||
}
|
}
|
||||||
@ -321,10 +327,14 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
final HtmlTagImpl.BlockImpl block = findOpenBlockTag(endTag.normalName);
|
final HtmlTagImpl.BlockImpl block = findOpenBlockTag(endTag.normalName);
|
||||||
if (block != null) {
|
if (block != null) {
|
||||||
|
|
||||||
|
if ("pre".equals(name)) {
|
||||||
|
isInsidePreTag = false;
|
||||||
|
}
|
||||||
|
|
||||||
block.closeAt(output.length());
|
block.closeAt(output.length());
|
||||||
|
|
||||||
if (TAG_PARAGRAPH.equals(name)) {
|
if (TAG_PARAGRAPH.equals(name)) {
|
||||||
append(output, "\n");
|
appendQuietly(output, '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentBlock = block.parent;
|
this.currentBlock = block.parent;
|
||||||
@ -335,18 +345,14 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
@NonNull T output,
|
@NonNull T output,
|
||||||
@NonNull Token.Character character) {
|
@NonNull Token.Character character) {
|
||||||
|
|
||||||
// the thing here is: if it's a script tag that we are inside -> we must not treat this
|
// there are tags: BUTTON, INPUT, SELECT, SCRIPT, TEXTAREA, STYLE
|
||||||
// as the text to append... should we even care about this? how many people are
|
// that might have character data that we do not want to display
|
||||||
// going to include freaking script tags as html inline?
|
|
||||||
//
|
|
||||||
// so tags are: BUTTON, INPUT, SELECT, SCRIPT, TEXTAREA
|
|
||||||
//
|
|
||||||
// actually we must decide it here: should we append freaking characters for these _bad_
|
|
||||||
// tags or not, as later we won't be able to change it and/or allow modification (as
|
|
||||||
// all indexes will be affected with this)
|
|
||||||
|
|
||||||
// for now: ignore the inline context
|
if (isInsidePreTag) {
|
||||||
append(output, character.getData());
|
appendQuietly(output, character.getData());
|
||||||
|
} else {
|
||||||
|
trimmingAppender.append(output, character.getData());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void appendBlockChild(@NonNull HtmlTagImpl.BlockImpl parent, @NonNull HtmlTagImpl.BlockImpl child) {
|
protected void appendBlockChild(@NonNull HtmlTagImpl.BlockImpl parent, @NonNull HtmlTagImpl.BlockImpl child) {
|
||||||
@ -400,20 +406,11 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
|
|||||||
return BLOCK_TAGS.contains(name);
|
return BLOCK_TAGS.contains(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static void append(@NonNull Appendable appendable, @NonNull CharSequence text) {
|
|
||||||
try {
|
|
||||||
appendable.append(text);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// _must_ not happen
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static <T extends Appendable & CharSequence> void ensureNewLine(@NonNull T output) {
|
protected static <T extends Appendable & CharSequence> void ensureNewLine(@NonNull T output) {
|
||||||
final int length = output.length();
|
final int length = output.length();
|
||||||
if (length > 0
|
if (length > 0
|
||||||
&& '\n' != output.charAt(length - 1)) {
|
&& '\n' != output.charAt(length - 1)) {
|
||||||
append(output, "\n");
|
appendQuietly(output, '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
package ru.noties.markwon.html.impl;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import static ru.noties.markwon.html.impl.AppendableUtils.appendQuietly;
|
||||||
|
|
||||||
|
abstract class TrimmingAppender {
|
||||||
|
|
||||||
|
abstract <T extends Appendable & CharSequence> void append(
|
||||||
|
@NonNull T output,
|
||||||
|
@NonNull String data
|
||||||
|
);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
static TrimmingAppender create() {
|
||||||
|
return new Impl();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Impl extends TrimmingAppender {
|
||||||
|
|
||||||
|
// if data is fully empty (consists of white spaces) -> do not add anything
|
||||||
|
// leading ws:
|
||||||
|
// - trim to one space (if at all present) append to output only if previous is ws
|
||||||
|
// trailing ws:
|
||||||
|
// - if present trim to single space
|
||||||
|
|
||||||
|
@Override
|
||||||
|
<T extends Appendable & CharSequence> void append(
|
||||||
|
@NonNull T output,
|
||||||
|
@NonNull String data
|
||||||
|
) {
|
||||||
|
|
||||||
|
final int startLength = output.length();
|
||||||
|
|
||||||
|
char c;
|
||||||
|
|
||||||
|
boolean previousIsWhiteSpace = false;
|
||||||
|
|
||||||
|
for (int i = 0, length = data.length(); i < length; i++) {
|
||||||
|
|
||||||
|
c = data.charAt(i);
|
||||||
|
|
||||||
|
if (Character.isWhitespace(c)) {
|
||||||
|
previousIsWhiteSpace = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousIsWhiteSpace) {
|
||||||
|
// validate that output has ws as last char
|
||||||
|
final int outputLength = output.length();
|
||||||
|
if (outputLength > 0
|
||||||
|
&& !Character.isWhitespace(output.charAt(outputLength - 1))) {
|
||||||
|
appendQuietly(output, ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousIsWhiteSpace = false;
|
||||||
|
appendQuietly(output, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// additionally check if previousIsWhiteSpace is true (if data ended with ws)
|
||||||
|
// BUT only if we have added something (otherwise the whole data is empty (white))
|
||||||
|
if (previousIsWhiteSpace && (startLength < output.length())) {
|
||||||
|
appendQuietly(output, ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,7 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
// all inline tags are correctly parsed
|
// all inline tags are correctly parsed
|
||||||
|
|
||||||
// a simple replacement that will return tag name as replacement (for this test purposes)
|
// a simple replacement that will return tag name as replacement (for this test purposes)
|
||||||
final MarkwonHtmlParserImpl impl = new MarkwonHtmlParserImpl(new HtmlEmptyTagReplacement() {
|
final MarkwonHtmlParserImpl impl = MarkwonHtmlParserImpl.create(new HtmlEmptyTagReplacement() {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String replace(@NonNull Token.StartTag startTag) {
|
public String replace(@NonNull Token.StartTag startTag) {
|
||||||
@ -95,7 +95,7 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
"img", "input"
|
"img", "input"
|
||||||
);
|
);
|
||||||
|
|
||||||
final MarkwonHtmlParserImpl impl = new MarkwonHtmlParserImpl(new HtmlEmptyTagReplacement() {
|
final MarkwonHtmlParserImpl impl = MarkwonHtmlParserImpl.create(new HtmlEmptyTagReplacement() {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String replace(@NonNull Token.StartTag startTag) {
|
public String replace(@NonNull Token.StartTag startTag) {
|
||||||
@ -140,7 +140,7 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
@Test
|
@Test
|
||||||
public void blockVoidTags() {
|
public void blockVoidTags() {
|
||||||
|
|
||||||
final MarkwonHtmlParserImpl impl = new MarkwonHtmlParserImpl(new HtmlEmptyTagReplacement() {
|
final MarkwonHtmlParserImpl impl = MarkwonHtmlParserImpl.create(new HtmlEmptyTagReplacement() {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String replace(@NonNull Token.StartTag startTag) {
|
public String replace(@NonNull Token.StartTag startTag) {
|
||||||
@ -209,7 +209,7 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
"FiveFiveFiveFiveFive"
|
"FiveFiveFiveFiveFive"
|
||||||
);
|
);
|
||||||
|
|
||||||
final MarkwonHtmlParserImpl impl = new MarkwonHtmlParserImpl(new HtmlEmptyTagReplacement() {
|
final MarkwonHtmlParserImpl impl = MarkwonHtmlParserImpl.create(new HtmlEmptyTagReplacement() {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String replace(@NonNull Token.StartTag startTag) {
|
public String replace(@NonNull Token.StartTag startTag) {
|
||||||
@ -277,7 +277,7 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
"video"
|
"video"
|
||||||
);
|
);
|
||||||
|
|
||||||
final MarkwonHtmlParserImpl impl = new MarkwonHtmlParserImpl(new HtmlEmptyTagReplacement() {
|
final MarkwonHtmlParserImpl impl = MarkwonHtmlParserImpl.create(new HtmlEmptyTagReplacement() {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String replace(@NonNull Token.StartTag startTag) {
|
public String replace(@NonNull Token.StartTag startTag) {
|
||||||
@ -328,7 +328,7 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
@Test
|
@Test
|
||||||
public void multipleFragmentsContinuation() {
|
public void multipleFragmentsContinuation() {
|
||||||
|
|
||||||
final MarkwonHtmlParserImpl impl = new MarkwonHtmlParserImpl(new HtmlEmptyTagReplacement());
|
final MarkwonHtmlParserImpl impl = MarkwonHtmlParserImpl.create(new HtmlEmptyTagReplacement());
|
||||||
|
|
||||||
final StringBuilder output = new StringBuilder();
|
final StringBuilder output = new StringBuilder();
|
||||||
|
|
||||||
@ -424,7 +424,7 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
final String html = "<div-1>1<div-2>2<div-3>hello!</div-1>";
|
final String html = "<div-1>1<div-2>2<div-3>hello!</div-1>";
|
||||||
impl.processFragment(output, html);
|
impl.processFragment(output, html);
|
||||||
|
|
||||||
assertEquals("12hello!", output.toString());
|
assertEquals(output.toString(), "12hello!", output.toString());
|
||||||
|
|
||||||
final CaptureBlockTagsAction action = new CaptureBlockTagsAction();
|
final CaptureBlockTagsAction action = new CaptureBlockTagsAction();
|
||||||
impl.flushBlockTags(output.length(), action);
|
impl.flushBlockTags(output.length(), action);
|
||||||
@ -473,6 +473,8 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
final StringBuilder output = new StringBuilder();
|
final StringBuilder output = new StringBuilder();
|
||||||
impl.processFragment(output, "<DiV><I>italic <eM>emphasis</Em> italic</i></dIv>");
|
impl.processFragment(output, "<DiV><I>italic <eM>emphasis</Em> italic</i></dIv>");
|
||||||
|
|
||||||
|
System.out.printf("output: `%s`%n", output);
|
||||||
|
|
||||||
final CaptureInlineTagsAction inlineTagsAction = new CaptureInlineTagsAction();
|
final CaptureInlineTagsAction inlineTagsAction = new CaptureInlineTagsAction();
|
||||||
final CaptureBlockTagsAction blockTagsAction = new CaptureBlockTagsAction();
|
final CaptureBlockTagsAction blockTagsAction = new CaptureBlockTagsAction();
|
||||||
|
|
||||||
@ -778,6 +780,30 @@ public class MarkwonHtmlParserImplTest {
|
|||||||
assertEquals(0, blockTagsAction.tags.size());
|
assertEquals(0, blockTagsAction.tags.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void blockTagNewLine() {
|
||||||
|
|
||||||
|
// we should make sure that a block tag will have a new line for it's
|
||||||
|
// content (white spaces before should be ignored)
|
||||||
|
|
||||||
|
final MarkwonHtmlParserImpl impl = MarkwonHtmlParserImpl.create();
|
||||||
|
final String html = "<ul>" +
|
||||||
|
" <li>ul-first" +
|
||||||
|
" <li>ul-second" +
|
||||||
|
" <ol>" +
|
||||||
|
" <li>ol-first" +
|
||||||
|
" <li>ol-second" +
|
||||||
|
" </ol>" +
|
||||||
|
" <li>ul-third" +
|
||||||
|
"</ul>";
|
||||||
|
|
||||||
|
final StringBuilder output = new StringBuilder();
|
||||||
|
impl.processFragment(output, html);
|
||||||
|
|
||||||
|
final String[] split = output.toString().split("\n");
|
||||||
|
assertEquals(Arrays.toString(split), 5, split.length);
|
||||||
|
}
|
||||||
|
|
||||||
private static class CaptureTagsAction<T> implements MarkwonHtmlParser.FlushAction<T> {
|
private static class CaptureTagsAction<T> implements MarkwonHtmlParser.FlushAction<T> {
|
||||||
|
|
||||||
boolean called;
|
boolean called;
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package ru.noties.markwon.html.impl;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@Config(manifest = Config.NONE)
|
||||||
|
public class TrimmingAppenderTest {
|
||||||
|
|
||||||
|
private TrimmingAppender.Impl impl;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
impl = new TrimmingAppender.Impl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void singlePart() {
|
||||||
|
final String input = " html body \n\ndiv hey ";
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
impl.append(builder, input);
|
||||||
|
assertEquals("html body div hey ", builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multiParts() {
|
||||||
|
final String[] inputs = {
|
||||||
|
"\n\n\n\n\nhtml\t body\n\ndiv ",
|
||||||
|
" span and go"
|
||||||
|
};
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
for (String input : inputs) {
|
||||||
|
impl.append(builder, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("html body div span and go", builder.toString());
|
||||||
|
}
|
||||||
|
}
|
@ -22,11 +22,20 @@ import java.util.Iterator;
|
|||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
public class SpannableBuilder implements Appendable, CharSequence {
|
public class SpannableBuilder implements Appendable, CharSequence {
|
||||||
|
|
||||||
// do not implement CharSequence (or any of Spanned interfaces)
|
|
||||||
|
|
||||||
// we will be using SpannableStringBuilder anyway as a backing store
|
public static void setSpans(@NonNull SpannableBuilder builder, @Nullable Object spans, int start, int end) {
|
||||||
// as it has tight connection with system (implements some hidden methods, etc)
|
if (spans != null) {
|
||||||
// private final SpannableStringBuilder builder;
|
if (spans.getClass().isArray()) {
|
||||||
|
for (Object o : ((Object[]) spans)) {
|
||||||
|
builder.setSpan(o, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private final StringBuilder builder;
|
private final StringBuilder builder;
|
||||||
|
|
||||||
// actually we might be just using ArrayList
|
// actually we might be just using ArrayList
|
||||||
|
@ -13,6 +13,7 @@ import ru.noties.markwon.SpannableConfiguration;
|
|||||||
import ru.noties.markwon.html.api.MarkwonHtmlParser;
|
import ru.noties.markwon.html.api.MarkwonHtmlParser;
|
||||||
import ru.noties.markwon.renderer.html2.tag.EmphasisHandler;
|
import ru.noties.markwon.renderer.html2.tag.EmphasisHandler;
|
||||||
import ru.noties.markwon.renderer.html2.tag.LinkHandler;
|
import ru.noties.markwon.renderer.html2.tag.LinkHandler;
|
||||||
|
import ru.noties.markwon.renderer.html2.tag.ListHandler;
|
||||||
import ru.noties.markwon.renderer.html2.tag.StrikeHandler;
|
import ru.noties.markwon.renderer.html2.tag.StrikeHandler;
|
||||||
import ru.noties.markwon.renderer.html2.tag.StrongEmphasisHandler;
|
import ru.noties.markwon.renderer.html2.tag.StrongEmphasisHandler;
|
||||||
import ru.noties.markwon.renderer.html2.tag.SubScriptHandler;
|
import ru.noties.markwon.renderer.html2.tag.SubScriptHandler;
|
||||||
@ -36,10 +37,16 @@ public abstract class MarkwonHtmlRenderer {
|
|||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static MarkwonHtmlRenderer create() {
|
public static MarkwonHtmlRenderer create() {
|
||||||
|
return builderWithDefaults().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static Builder builderWithDefaults() {
|
||||||
|
|
||||||
final EmphasisHandler emphasisHandler = new EmphasisHandler();
|
final EmphasisHandler emphasisHandler = new EmphasisHandler();
|
||||||
final StrongEmphasisHandler strongEmphasisHandler = new StrongEmphasisHandler();
|
final StrongEmphasisHandler strongEmphasisHandler = new StrongEmphasisHandler();
|
||||||
final StrikeHandler strikeHandler = new StrikeHandler();
|
final StrikeHandler strikeHandler = new StrikeHandler();
|
||||||
|
final ListHandler listHandler = new ListHandler();
|
||||||
|
|
||||||
return builder()
|
return builder()
|
||||||
.handler("i", emphasisHandler)
|
.handler("i", emphasisHandler)
|
||||||
@ -55,7 +62,8 @@ public abstract class MarkwonHtmlRenderer {
|
|||||||
.handler("s", strikeHandler)
|
.handler("s", strikeHandler)
|
||||||
.handler("strike", strikeHandler)
|
.handler("strike", strikeHandler)
|
||||||
.handler("a", new LinkHandler())
|
.handler("a", new LinkHandler())
|
||||||
.build();
|
.handler("ul", listHandler)
|
||||||
|
.handler("ol", listHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -2,7 +2,6 @@ package ru.noties.markwon.renderer.html2;
|
|||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.Spanned;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -36,7 +35,7 @@ class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer {
|
|||||||
for (HtmlTag.Inline inline : tags) {
|
for (HtmlTag.Inline inline : tags) {
|
||||||
handler = tagHandler(inline.name());
|
handler = tagHandler(inline.name());
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
setSpans(builder, handler.getSpans(configuration, inline), inline.start(), inline.end());
|
handler.handle(configuration, builder, inline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,7 +48,7 @@ class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer {
|
|||||||
for (HtmlTag.Block block : tags) {
|
for (HtmlTag.Block block : tags) {
|
||||||
handler = tagHandler(block.name());
|
handler = tagHandler(block.name());
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
setSpans(builder, handler.getSpans(configuration, block), block.start(), block.end());
|
handler.handle(configuration, builder, block);
|
||||||
} else {
|
} else {
|
||||||
// see if any of children can be handled
|
// see if any of children can be handled
|
||||||
apply(block.children());
|
apply(block.children());
|
||||||
@ -66,16 +65,4 @@ class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer {
|
|||||||
public TagHandler tagHandler(@NonNull String tagName) {
|
public TagHandler tagHandler(@NonNull String tagName) {
|
||||||
return tagHandlers.get(tagName);
|
return tagHandlers.get(tagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setSpans(@NonNull SpannableBuilder builder, @Nullable Object spans, int start, int end) {
|
|
||||||
if (spans != null) {
|
|
||||||
if (spans.getClass().isArray()) {
|
|
||||||
for (Object o : ((Object[]) spans)) {
|
|
||||||
builder.setSpan(o, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
builder.setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import android.support.annotation.Nullable;
|
|||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
import ru.noties.markwon.html.api.HtmlTag;
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
public class EmphasisHandler implements TagHandler {
|
public class EmphasisHandler extends SimpleTagHandler {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
||||||
|
@ -7,7 +7,7 @@ import android.text.TextUtils;
|
|||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
import ru.noties.markwon.html.api.HtmlTag;
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
public class LinkHandler implements TagHandler {
|
public class LinkHandler extends SimpleTagHandler {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
package ru.noties.markwon.renderer.html2.tag;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import ru.noties.markwon.SpannableBuilder;
|
||||||
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
|
public class ListHandler implements TagHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(
|
||||||
|
@NonNull SpannableConfiguration configuration,
|
||||||
|
@NonNull SpannableBuilder builder,
|
||||||
|
@NonNull HtmlTag tag) {
|
||||||
|
|
||||||
|
if (!tag.isBlock()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final HtmlTag.Block block = tag.getAsBlock();
|
||||||
|
final boolean ol = "ol".equals(block.name());
|
||||||
|
final boolean ul = "ul".equals(block.name());
|
||||||
|
|
||||||
|
if (!ol && !ul) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int number = 1;
|
||||||
|
final int bulletLevel = currentBulletListLevel(block);
|
||||||
|
|
||||||
|
Object spans;
|
||||||
|
|
||||||
|
for (HtmlTag.Block child : block.children()) {
|
||||||
|
|
||||||
|
visitChildren(configuration, builder, child);
|
||||||
|
|
||||||
|
if ("li".equals(child.name())) {
|
||||||
|
// insert list item here
|
||||||
|
if (ol) {
|
||||||
|
spans = configuration.factory().orderedListItem(
|
||||||
|
configuration.theme(),
|
||||||
|
number++
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
spans = configuration.factory().bulletListItem(
|
||||||
|
configuration.theme(),
|
||||||
|
bulletLevel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SpannableBuilder.setSpans(builder, spans, child.start(), child.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitChildren(
|
||||||
|
@NonNull SpannableConfiguration configuration,
|
||||||
|
@NonNull SpannableBuilder builder,
|
||||||
|
@NonNull HtmlTag.Block block) {
|
||||||
|
|
||||||
|
TagHandler handler;
|
||||||
|
|
||||||
|
for (HtmlTag.Block child : block.children()) {
|
||||||
|
handler = configuration.htmlRenderer().tagHandler(child.name());
|
||||||
|
if (handler != null) {
|
||||||
|
handler.handle(configuration, builder, child);
|
||||||
|
} else {
|
||||||
|
visitChildren(configuration, builder, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int currentBulletListLevel(@NonNull HtmlTag.Block block) {
|
||||||
|
int level = 0;
|
||||||
|
while ((block = block.parent()) != null) {
|
||||||
|
if ("ul".equals(block.name())
|
||||||
|
|| "ol".equals(block.name())) {
|
||||||
|
level += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
}
|
@ -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.SpannableBuilder;
|
||||||
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
|
public abstract class SimpleTagHandler implements TagHandler {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public abstract Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(@NonNull SpannableConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) {
|
||||||
|
final Object spans = getSpans(configuration, tag);
|
||||||
|
if (spans != null) {
|
||||||
|
SpannableBuilder.setSpans(builder, spans, tag.start(), tag.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import android.support.annotation.Nullable;
|
|||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
import ru.noties.markwon.html.api.HtmlTag;
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
public class StrikeHandler implements TagHandler {
|
public class StrikeHandler extends SimpleTagHandler {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
||||||
|
@ -6,7 +6,7 @@ import android.support.annotation.Nullable;
|
|||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
import ru.noties.markwon.html.api.HtmlTag;
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
public class StrongEmphasisHandler implements TagHandler {
|
public class StrongEmphasisHandler extends SimpleTagHandler {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
||||||
|
@ -6,7 +6,7 @@ import android.support.annotation.Nullable;
|
|||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
import ru.noties.markwon.html.api.HtmlTag;
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
public class SubScriptHandler implements TagHandler {
|
public class SubScriptHandler extends SimpleTagHandler {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
||||||
|
@ -6,7 +6,7 @@ import android.support.annotation.Nullable;
|
|||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
import ru.noties.markwon.html.api.HtmlTag;
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
public class SuperScriptHandler implements TagHandler {
|
public class SuperScriptHandler extends SimpleTagHandler {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
package ru.noties.markwon.renderer.html2.tag;
|
package ru.noties.markwon.renderer.html2.tag;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
|
import ru.noties.markwon.SpannableBuilder;
|
||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
import ru.noties.markwon.html.api.HtmlTag;
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
public interface TagHandler {
|
public interface TagHandler {
|
||||||
|
|
||||||
@Nullable
|
void handle(
|
||||||
Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag);
|
@NonNull SpannableConfiguration configuration,
|
||||||
|
@NonNull SpannableBuilder builder,
|
||||||
|
@NonNull HtmlTag tag
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import android.support.annotation.Nullable;
|
|||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
import ru.noties.markwon.html.api.HtmlTag;
|
import ru.noties.markwon.html.api.HtmlTag;
|
||||||
|
|
||||||
public class UnderlineHandler implements TagHandler {
|
public class UnderlineHandler extends SimpleTagHandler {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user