Feature/white space (#63)

* Deal with white spaces by checking for next node

* Update documentation about remove trimWhiteSpaceEnd

* Update app module layout
This commit is contained in:
Dimitry 2018-09-04 16:42:40 +03:00 committed by GitHub
parent b5c87d957e
commit 3ef4dc61fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 124 additions and 145 deletions

View File

@ -158,8 +158,6 @@ Or leave it empty and use the [link text itself].
Inline `code` has `back-ticks around` it. Inline `code` has `back-ticks around` it.
<sup>*</sup>*Please note, that syntax highlighting is supported but library provides no means to do it automatically*
```javascript ```javascript
var s = "JavaScript syntax highlighting"; var s = "JavaScript syntax highlighting";
alert(s); alert(s);

View File

@ -96,7 +96,6 @@ public class MarkdownRenderer {
.codeTextColor(prism4jTheme.textColor()) .codeTextColor(prism4jTheme.textColor())
.build()) .build())
.factory(new GifAwareSpannableFactory(gifPlaceholder)) .factory(new GifAwareSpannableFactory(gifPlaceholder))
.trimWhiteSpaceEnd(false)
.build(); .build();
final long start = SystemClock.uptimeMillis(); final long start = SystemClock.uptimeMillis();

View File

@ -8,13 +8,16 @@
android:id="@+id/scroll_view" android:id="@+id/scroll_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="16dip"
android:clipToPadding="false"
android:clipChildren="false"
android:scrollbarStyle="outsideOverlay"
android:layout_marginTop="?android:attr/actionBarSize"> android:layout_marginTop="?android:attr/actionBarSize">
<TextView <TextView
android:id="@+id/text" android:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dip"
android:lineSpacingExtra="2dip" android:lineSpacingExtra="2dip"
android:textSize="16sp" android:textSize="16sp"
tools:context="ru.noties.markwon.MainActivity" tools:context="ru.noties.markwon.MainActivity"

View File

@ -183,25 +183,6 @@ If not provided explicitly, default `false` value will be used.
<Link name="commonmark-spec#soft-break" displayName="Commonmark specification" /> <Link name="commonmark-spec#soft-break" displayName="Commonmark specification" />
## Trim white space from end <Badge text="2.0.0" />
`trimWhiteSpaceEnd` option controls whether or not to trim white spaces from the
end of a document.
```java
SpannableConfiguration.builder(context)
.trimWhiteSpaceEnd(boolean)
.build();
```
If not provided explicitly, default `true` value will be used.
:::tip Before <Badge text="2.0.0" />
Before `2.0.0` version this functionality was _implicitly_ included in
`SpannableBuilder#text` method. This is no longer true and now `SpannableBuilder`
does not trim white spaces (which was by default and non-configurable)
:::
## HTML <Badge text="2.0.0" /> ## HTML <Badge text="2.0.0" />
### Parser ### Parser

View File

@ -61,7 +61,6 @@ public class SpannableBuilder implements Appendable, CharSequence {
} }
public SpannableBuilder(@NonNull CharSequence cs) { public SpannableBuilder(@NonNull CharSequence cs) {
// this.builder = new SpannableStringBuilderImpl(cs.toString());
this.builder = new StringBuilder(cs); this.builder = new StringBuilder(cs);
copySpans(0, cs); copySpans(0, cs);
} }
@ -198,58 +197,6 @@ public class SpannableBuilder implements Appendable, CharSequence {
return builder.toString(); return builder.toString();
} }
/**
* Moved from {@link #text()} method
*
* @since 2.0.0
*/
public void trimWhiteSpaceEnd() {
// now, let's remove trailing & leading newLines (so small amounts of text are displayed correctly)
// @since 1.0.2
int length = builder.length();
if (length > 0) {
length = builder.length();
int amount = 0;
for (int i = length - 1; i >= 0; i--) {
if (Character.isWhitespace(builder.charAt(i))) {
amount += 1;
} else {
break;
}
}
if (amount > 0) {
final int newLength = length - amount;
builder.replace(newLength, length, "");
// additionally we should apply new length to the spans (otherwise
// sometimes system cannot handle spans with length greater than total length
// which causes some internal failure (no exceptions, no logs) in Layout class
// and no markdown is displayed at all
if (spans.size() > 0) {
Span span;
final Iterator<Span> iterator = spans.iterator();
while (iterator.hasNext()) {
span = iterator.next();
// if span start is greater than newLength, then remove it... one should
// not use white space for spanning resulting text
if (span.start > newLength) {
iterator.remove();
} else if (span.end > newLength) {
span.end = newLength;
}
}
}
}
}
}
@NonNull @NonNull
public CharSequence text() { public CharSequence text() {
// @since 2.0.0 redirects this call to `#spannableStringBuilder()` // @since 2.0.0 redirects this call to `#spannableStringBuilder()`

View File

@ -3,7 +3,6 @@ package ru.noties.markwon;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.renderer.ImageSizeResolverDef; import ru.noties.markwon.renderer.ImageSizeResolverDef;
@ -34,7 +33,6 @@ public class SpannableConfiguration {
private final ImageSizeResolver imageSizeResolver; private final ImageSizeResolver imageSizeResolver;
private final SpannableFactory factory; // @since 1.1.0 private final SpannableFactory factory; // @since 1.1.0
private final boolean softBreakAddsNewLine; // @since 1.1.1 private final boolean softBreakAddsNewLine; // @since 1.1.1
private final boolean trimWhiteSpaceEnd; // @since 2.0.0
private final MarkwonHtmlParser htmlParser; // @since 2.0.0 private final MarkwonHtmlParser htmlParser; // @since 2.0.0
private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0
private final boolean htmlAllowNonClosedTags; // @since 2.0.0 private final boolean htmlAllowNonClosedTags; // @since 2.0.0
@ -48,7 +46,6 @@ public class SpannableConfiguration {
this.imageSizeResolver = builder.imageSizeResolver; this.imageSizeResolver = builder.imageSizeResolver;
this.factory = builder.factory; this.factory = builder.factory;
this.softBreakAddsNewLine = builder.softBreakAddsNewLine; this.softBreakAddsNewLine = builder.softBreakAddsNewLine;
this.trimWhiteSpaceEnd = builder.trimWhiteSpaceEnd;
this.htmlParser = builder.htmlParser; this.htmlParser = builder.htmlParser;
this.htmlRenderer = builder.htmlRenderer; this.htmlRenderer = builder.htmlRenderer;
this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags; this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags;
@ -98,13 +95,6 @@ public class SpannableConfiguration {
return softBreakAddsNewLine; return softBreakAddsNewLine;
} }
/**
* @since 2.0.0
*/
public boolean trimWhiteSpaceEnd() {
return trimWhiteSpaceEnd;
}
/** /**
* @since 2.0.0 * @since 2.0.0
*/ */
@ -140,7 +130,6 @@ public class SpannableConfiguration {
private ImageSizeResolver imageSizeResolver; private ImageSizeResolver imageSizeResolver;
private SpannableFactory factory; // @since 1.1.0 private SpannableFactory factory; // @since 1.1.0
private boolean softBreakAddsNewLine; // @since 1.1.1 private boolean softBreakAddsNewLine; // @since 1.1.1
private boolean trimWhiteSpaceEnd = true; // @since 2.0.0
private MarkwonHtmlParser htmlParser; // @since 2.0.0 private MarkwonHtmlParser htmlParser; // @since 2.0.0
private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0
private boolean htmlAllowNonClosedTags; // @since 2.0.0 private boolean htmlAllowNonClosedTags; // @since 2.0.0
@ -210,18 +199,6 @@ public class SpannableConfiguration {
return this; return this;
} }
/**
* Will trim white space(s) from the end from resulting text.
* By default `true`
*
* @since 2.0.0
*/
@NonNull
public Builder trimWhiteSpaceEnd(boolean trimWhiteSpaceEnd) {
this.trimWhiteSpaceEnd = trimWhiteSpaceEnd;
return this;
}
/** /**
* @since 2.0.0 * @since 2.0.0
*/ */

View File

@ -6,6 +6,7 @@ import android.support.annotation.Nullable;
import org.commonmark.ext.gfm.strikethrough.Strikethrough; import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.ext.gfm.tables.TableBody; import org.commonmark.ext.gfm.tables.TableBody;
import org.commonmark.ext.gfm.tables.TableCell; import org.commonmark.ext.gfm.tables.TableCell;
import org.commonmark.ext.gfm.tables.TableHead;
import org.commonmark.ext.gfm.tables.TableRow; import org.commonmark.ext.gfm.tables.TableRow;
import org.commonmark.node.AbstractVisitor; import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.BlockQuote; import org.commonmark.node.BlockQuote;
@ -79,10 +80,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
super.visit(document); super.visit(document);
configuration.htmlRenderer().render(configuration, builder, htmlParser); configuration.htmlRenderer().render(configuration, builder, htmlParser);
if (configuration.trimWhiteSpaceEnd()) {
builder.trimWhiteSpaceEnd();
}
} }
@Override @Override
@ -122,11 +119,13 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
blockQuoteIndent -= 1; blockQuoteIndent -= 1;
if (hasNext(blockQuote)) {
newLine(); newLine();
if (blockQuoteIndent == 0) { if (blockQuoteIndent == 0) {
builder.append('\n'); builder.append('\n');
} }
} }
}
@Override @Override
public void visit(Code code) { public void visit(Code code) {
@ -145,7 +144,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
@Override @Override
public void visit(FencedCodeBlock fencedCodeBlock) { public void visit(FencedCodeBlock fencedCodeBlock) {
// @since 1.0.4 // @since 1.0.4
visitCodeBlock(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral()); visitCodeBlock(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock);
} }
/** /**
@ -153,7 +152,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
*/ */
@Override @Override
public void visit(IndentedCodeBlock indentedCodeBlock) { public void visit(IndentedCodeBlock indentedCodeBlock) {
visitCodeBlock(null, indentedCodeBlock.getLiteral()); visitCodeBlock(null, indentedCodeBlock.getLiteral(), indentedCodeBlock);
} }
/** /**
@ -161,7 +160,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
* @param code content of a code block * @param code content of a code block
* @since 1.0.4 * @since 1.0.4
*/ */
private void visitCodeBlock(@Nullable String info, @NonNull String code) { private void visitCodeBlock(@Nullable String info, @NonNull String code, @NonNull Node node) {
newLine(); newLine();
final int length = builder.length(); final int length = builder.length();
@ -172,13 +172,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
configuration.syntaxHighlight() configuration.syntaxHighlight()
.highlight(info, code) .highlight(info, code)
); );
builder.append('\u00a0').append('\n');
newLine();
builder.append('\u00a0');
setSpan(length, factory.code(theme, true)); setSpan(length, factory.code(theme, true));
if (hasNext(node)) {
newLine(); newLine();
builder.append('\n'); builder.append('\n');
} }
}
@Override @Override
public void visit(BulletList bulletList) { public void visit(BulletList bulletList) {
@ -191,13 +195,18 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
} }
private void visitList(Node node) { private void visitList(Node node) {
newLine(); newLine();
visitChildren(node); visitChildren(node);
if (hasNext(node)) {
newLine(); newLine();
if (listLevel == 0 && blockQuoteIndent == 0) { if (listLevel == 0 && blockQuoteIndent == 0) {
builder.append('\n'); builder.append('\n');
} }
} }
}
@Override @Override
public void visit(ListItem listItem) { public void visit(ListItem listItem) {
@ -230,8 +239,10 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
blockQuoteIndent -= 1; blockQuoteIndent -= 1;
listLevel -= 1; listLevel -= 1;
if (hasNext(listItem)) {
newLine(); newLine();
} }
}
@Override @Override
public void visit(ThematicBreak thematicBreak) { public void visit(ThematicBreak thematicBreak) {
@ -243,9 +254,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
setSpan(length, factory.thematicBreak(theme)); setSpan(length, factory.thematicBreak(theme));
if (hasNext(thematicBreak)) {
newLine(); newLine();
builder.append('\n'); builder.append('\n');
} }
}
@Override @Override
public void visit(Heading heading) { public void visit(Heading heading) {
@ -256,11 +269,12 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(heading); visitChildren(heading);
setSpan(length, factory.heading(theme, heading.getLevel())); setSpan(length, factory.heading(theme, heading.getLevel()));
if (hasNext(heading)) {
newLine(); newLine();
// after heading we add another line anyway (no additional checks) // after heading we add another line anyway (no additional checks)
builder.append('\n'); builder.append('\n');
} }
}
@Override @Override
public void visit(SoftLineBreak softLineBreak) { public void visit(SoftLineBreak softLineBreak) {
@ -282,12 +296,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
*/ */
@Override @Override
public void visit(CustomBlock customBlock) { public void visit(CustomBlock customBlock) {
if (customBlock instanceof TaskListBlock) { if (customBlock instanceof TaskListBlock) {
blockQuoteIndent += 1; blockQuoteIndent += 1;
visitChildren(customBlock); visitChildren(customBlock);
blockQuoteIndent -= 1; blockQuoteIndent -= 1;
if (hasNext(customBlock)) {
newLine(); newLine();
builder.append('\n'); builder.append('\n');
}
} else { } else {
super.visit(customBlock); super.visit(customBlock);
} }
@ -316,7 +335,9 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done())); setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done()));
if (hasNext(customNode)) {
newLine(); newLine();
}
blockQuoteIndent -= listItem.indent(); blockQuoteIndent -= listItem.indent();
@ -330,18 +351,37 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final boolean handled; final boolean handled;
if (node instanceof TableBody) { if (node instanceof TableBody) {
visitChildren(node); visitChildren(node);
tableRows = 0; tableRows = 0;
handled = true; handled = true;
if (hasNext(node)) {
newLine(); newLine();
builder.append('\n'); builder.append('\n');
} else if (node instanceof TableRow) { }
} else if (node instanceof TableRow || node instanceof TableHead) {
final int length = builder.length(); final int length = builder.length();
visitChildren(node); visitChildren(node);
if (pendingTableRow != null) { if (pendingTableRow != null) {
// @since 2.0.0
// we cannot rely on hasNext(TableHead) as it's not reliable
// we must apply new line manually and then exclude it from tableRow span
final boolean addNewLine;
{
final int builderLength = builder.length();
addNewLine = builderLength > 0
&& '\n' != builder.charAt(builderLength - 1);
}
if (addNewLine) {
builder.append('\n');
}
// @since 1.0.4 Replace table char with non-breakable space // @since 1.0.4 Replace table char with non-breakable space
// we need this because if table is at the end of the text, then it will be // we need this because if table is at the end of the text, then it will be
// trimmed from the final result // trimmed from the final result
@ -357,12 +397,13 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
? 0 ? 0
: tableRows + 1; : tableRows + 1;
setSpan(length, span); setSpan(addNewLine ? length + 1 : length, span);
newLine();
pendingTableRow = null; pendingTableRow = null;
} }
handled = true; handled = true;
} else if (node instanceof TableCell) { } else if (node instanceof TableCell) {
final TableCell cell = (TableCell) node; final TableCell cell = (TableCell) node;
@ -383,11 +424,13 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
} else { } else {
handled = false; handled = false;
} }
return handled; return handled;
} }
@Override @Override
public void visit(Paragraph paragraph) { public void visit(Paragraph paragraph) {
final boolean inTightList = isInTightList(paragraph); final boolean inTightList = isInTightList(paragraph);
if (!inTightList) { if (!inTightList) {
@ -400,9 +443,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
// @since 1.1.1 apply paragraph span // @since 1.1.1 apply paragraph span
setSpan(length, factory.paragraph(inTightList)); setSpan(length, factory.paragraph(inTightList));
if (!inTightList) { if (hasNext(paragraph) && !inTightList) {
newLine(); newLine();
if (blockQuoteIndent == 0) { if (blockQuoteIndent == 0) {
builder.append('\n'); builder.append('\n');
} }
@ -518,4 +560,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
} }
return out; return out;
} }
/**
* @since 2.0.0
*/
protected static boolean hasNext(@NonNull Node node) {
return node.getNext() != null;
}
} }

View File

@ -61,7 +61,7 @@ public class SpannableMarkdownVisitorTest {
} }
// assert that the whole thing is processed // assert that the whole thing is processed
assertEquals(stringBuilder.length(), index); assertEquals("`" + stringBuilder + "`", stringBuilder.length(), index);
final Object[] spans = stringBuilder.getSpans(0, stringBuilder.length(), Object.class); final Object[] spans = stringBuilder.getSpans(0, stringBuilder.length(), Object.class);
final int length = spans != null final int length = spans != null

View File

@ -5,7 +5,6 @@ import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
import ix.Ix; import ix.Ix;
@ -68,7 +67,10 @@ abstract class TestValidator {
} }
} }
assertEquals(text, builder.subSequence(index, index + text.length()).toString()); assertEquals(
String.format("text: %s, position: {%d-%d}", text, index, index + text.length()),
text,
builder.subSequence(index, index + text.length()).toString());
return index + text.length(); return index + text.length();
} }
@ -106,6 +108,7 @@ abstract class TestValidator {
.filter(new IxPredicate<TestSpan>() { .filter(new IxPredicate<TestSpan>() {
@Override @Override
public boolean test(TestSpan testSpan) { public boolean test(TestSpan testSpan) {
// in case of nested spans with the same name (lists) // in case of nested spans with the same name (lists)
// we also must validate attributes // we also must validate attributes
// and thus we are moving most of assertions to this filter method // and thus we are moving most of assertions to this filter method
@ -175,11 +178,16 @@ abstract class TestValidator {
if (builder.length() > 0) { if (builder.length() > 0) {
builder.append(", "); builder.append(", ");
} }
builder builder
.append("{name: '").append(testSpan.name()).append('\'') .append("{name: '").append(testSpan.name()).append('\'')
.append(", position{").append(start).append(", ").append(end).append('}') .append(", position{").append(start).append(", ").append(end).append('}');
.append(", attributes: ").append(testSpan.attributes())
.append('}'); if (testSpan.attributes().size() > 0) {
builder.append(", attributes: ").append(testSpan.attributes());
}
builder.append('}');
} }
spansText = builder.toString(); spansText = builder.toString();
} }

View File

@ -0,0 +1,17 @@
input: |-
```java
final String s = null;
```
```html
<html></html>
```
```
nothing here
```
output:
- code-block: "final String s = null;"
- "\n\n"
- code-block: "<html></html>"
- "\n\n"
- code-block: "nothing here"

View File

@ -21,8 +21,8 @@ output:
- text: " " - text: " "
- s: "strike" - s: "strike"
- text: " down\n\n" - text: " down\n\n"
- blockquote: "Some quote here!\n" - blockquote: "Some quote here!"
- text: "\n" - text: "\n\n"
- h1: "Header 1" - h1: "Header 1"
- text: "\n\n" - text: "\n\n"
- h2: "Header 2" - h2: "Header 2"

View File

@ -10,8 +10,8 @@ output:
- ul: - ul:
- ul: "Second" - ul: "Second"
level: 1 level: 1
- text: "\n"
level: 0 level: 0
- text: "\n"
- ul: - ul:
- ul: - ul:
- ul: "Third" - ul: "Third"