Deal with white spaces by checking for next node
This commit is contained in:
		
							parent
							
								
									a39450ed56
								
							
						
					
					
						commit
						20a814862e
					
				| @ -96,7 +96,6 @@ public class MarkdownRenderer { | ||||
|                                 .codeTextColor(prism4jTheme.textColor()) | ||||
|                                 .build()) | ||||
|                         .factory(new GifAwareSpannableFactory(gifPlaceholder)) | ||||
|                         .trimWhiteSpaceEnd(false) | ||||
|                         .build(); | ||||
| 
 | ||||
|                 final long start = SystemClock.uptimeMillis(); | ||||
|  | ||||
| @ -61,7 +61,6 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
|     } | ||||
| 
 | ||||
|     public SpannableBuilder(@NonNull CharSequence cs) { | ||||
| //        this.builder = new SpannableStringBuilderImpl(cs.toString()); | ||||
|         this.builder = new StringBuilder(cs); | ||||
|         copySpans(0, cs); | ||||
|     } | ||||
| @ -198,58 +197,6 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
|         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 | ||||
|     public CharSequence text() { | ||||
|         // @since 2.0.0 redirects this call to `#spannableStringBuilder()` | ||||
|  | ||||
| @ -3,7 +3,6 @@ package ru.noties.markwon; | ||||
| import android.content.Context; | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import ru.noties.markwon.html.api.HtmlTag; | ||||
| import ru.noties.markwon.html.api.MarkwonHtmlParser; | ||||
| import ru.noties.markwon.renderer.ImageSizeResolver; | ||||
| import ru.noties.markwon.renderer.ImageSizeResolverDef; | ||||
| @ -34,7 +33,6 @@ public class SpannableConfiguration { | ||||
|     private final ImageSizeResolver imageSizeResolver; | ||||
|     private final SpannableFactory factory; // @since 1.1.0 | ||||
|     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 MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 | ||||
|     private final boolean htmlAllowNonClosedTags; // @since 2.0.0 | ||||
| @ -48,7 +46,6 @@ public class SpannableConfiguration { | ||||
|         this.imageSizeResolver = builder.imageSizeResolver; | ||||
|         this.factory = builder.factory; | ||||
|         this.softBreakAddsNewLine = builder.softBreakAddsNewLine; | ||||
|         this.trimWhiteSpaceEnd = builder.trimWhiteSpaceEnd; | ||||
|         this.htmlParser = builder.htmlParser; | ||||
|         this.htmlRenderer = builder.htmlRenderer; | ||||
|         this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags; | ||||
| @ -98,13 +95,6 @@ public class SpannableConfiguration { | ||||
|         return softBreakAddsNewLine; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 2.0.0 | ||||
|      */ | ||||
|     public boolean trimWhiteSpaceEnd() { | ||||
|         return trimWhiteSpaceEnd; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 2.0.0 | ||||
|      */ | ||||
| @ -140,7 +130,6 @@ public class SpannableConfiguration { | ||||
|         private ImageSizeResolver imageSizeResolver; | ||||
|         private SpannableFactory factory; // @since 1.1.0 | ||||
|         private boolean softBreakAddsNewLine; // @since 1.1.1 | ||||
|         private boolean trimWhiteSpaceEnd = true; // @since 2.0.0 | ||||
|         private MarkwonHtmlParser htmlParser; // @since 2.0.0 | ||||
|         private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 | ||||
|         private boolean htmlAllowNonClosedTags; // @since 2.0.0 | ||||
| @ -210,18 +199,6 @@ public class SpannableConfiguration { | ||||
|             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 | ||||
|          */ | ||||
|  | ||||
| @ -6,6 +6,7 @@ import android.support.annotation.Nullable; | ||||
| import org.commonmark.ext.gfm.strikethrough.Strikethrough; | ||||
| import org.commonmark.ext.gfm.tables.TableBody; | ||||
| import org.commonmark.ext.gfm.tables.TableCell; | ||||
| import org.commonmark.ext.gfm.tables.TableHead; | ||||
| import org.commonmark.ext.gfm.tables.TableRow; | ||||
| import org.commonmark.node.AbstractVisitor; | ||||
| import org.commonmark.node.BlockQuote; | ||||
| @ -79,10 +80,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         super.visit(document); | ||||
| 
 | ||||
|         configuration.htmlRenderer().render(configuration, builder, htmlParser); | ||||
| 
 | ||||
|         if (configuration.trimWhiteSpaceEnd()) { | ||||
|             builder.trimWhiteSpaceEnd(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @ -122,11 +119,13 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
| 
 | ||||
|         blockQuoteIndent -= 1; | ||||
| 
 | ||||
|         if (hasNext(blockQuote)) { | ||||
|             newLine(); | ||||
|             if (blockQuoteIndent == 0) { | ||||
|                 builder.append('\n'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Code code) { | ||||
| @ -145,7 +144,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|     @Override | ||||
|     public void visit(FencedCodeBlock fencedCodeBlock) { | ||||
|         // @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 | ||||
|     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 | ||||
|      * @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(); | ||||
| 
 | ||||
|         final int length = builder.length(); | ||||
| @ -172,13 +172,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|                 configuration.syntaxHighlight() | ||||
|                         .highlight(info, code) | ||||
|         ); | ||||
|         builder.append('\u00a0').append('\n'); | ||||
| 
 | ||||
|         newLine(); | ||||
|         builder.append('\u00a0'); | ||||
| 
 | ||||
|         setSpan(length, factory.code(theme, true)); | ||||
| 
 | ||||
|         if (hasNext(node)) { | ||||
|             newLine(); | ||||
|             builder.append('\n'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(BulletList bulletList) { | ||||
| @ -191,13 +195,18 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|     } | ||||
| 
 | ||||
|     private void visitList(Node node) { | ||||
| 
 | ||||
|         newLine(); | ||||
| 
 | ||||
|         visitChildren(node); | ||||
| 
 | ||||
|         if (hasNext(node)) { | ||||
|             newLine(); | ||||
|             if (listLevel == 0 && blockQuoteIndent == 0) { | ||||
|                 builder.append('\n'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(ListItem listItem) { | ||||
| @ -230,8 +239,10 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         blockQuoteIndent -= 1; | ||||
|         listLevel -= 1; | ||||
| 
 | ||||
|         if (hasNext(listItem)) { | ||||
|             newLine(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(ThematicBreak thematicBreak) { | ||||
| @ -243,9 +254,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
| 
 | ||||
|         setSpan(length, factory.thematicBreak(theme)); | ||||
| 
 | ||||
|         if (hasNext(thematicBreak)) { | ||||
|             newLine(); | ||||
|             builder.append('\n'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Heading heading) { | ||||
| @ -256,11 +269,12 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         visitChildren(heading); | ||||
|         setSpan(length, factory.heading(theme, heading.getLevel())); | ||||
| 
 | ||||
|         if (hasNext(heading)) { | ||||
|             newLine(); | ||||
| 
 | ||||
|             // after heading we add another line anyway (no additional checks) | ||||
|             builder.append('\n'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(SoftLineBreak softLineBreak) { | ||||
| @ -282,12 +296,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|      */ | ||||
|     @Override | ||||
|     public void visit(CustomBlock customBlock) { | ||||
| 
 | ||||
|         if (customBlock instanceof TaskListBlock) { | ||||
|             blockQuoteIndent += 1; | ||||
|             visitChildren(customBlock); | ||||
|             blockQuoteIndent -= 1; | ||||
| 
 | ||||
|             if (hasNext(customBlock)) { | ||||
|                 newLine(); | ||||
|                 builder.append('\n'); | ||||
|             } | ||||
| 
 | ||||
|         } else { | ||||
|             super.visit(customBlock); | ||||
|         } | ||||
| @ -316,7 +335,9 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
| 
 | ||||
|             setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done())); | ||||
| 
 | ||||
|             if (hasNext(customNode)) { | ||||
|                 newLine(); | ||||
|             } | ||||
| 
 | ||||
|             blockQuoteIndent -= listItem.indent(); | ||||
| 
 | ||||
| @ -330,18 +351,37 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         final boolean handled; | ||||
| 
 | ||||
|         if (node instanceof TableBody) { | ||||
| 
 | ||||
|             visitChildren(node); | ||||
|             tableRows = 0; | ||||
|             handled = true; | ||||
| 
 | ||||
|             if (hasNext(node)) { | ||||
|                 newLine(); | ||||
|                 builder.append('\n'); | ||||
|         } else if (node instanceof TableRow) { | ||||
|             } | ||||
| 
 | ||||
|         } else if (node instanceof TableRow || node instanceof TableHead) { | ||||
| 
 | ||||
|             final int length = builder.length(); | ||||
| 
 | ||||
|             visitChildren(node); | ||||
| 
 | ||||
|             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 | ||||
|                 // we need this because if table is at the end of the text, then it will be | ||||
|                 // trimmed from the final result | ||||
| @ -357,12 +397,13 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|                         ? 0 | ||||
|                         : tableRows + 1; | ||||
| 
 | ||||
|                 setSpan(length, span); | ||||
|                 newLine(); | ||||
|                 setSpan(addNewLine ? length + 1 : length, span); | ||||
| 
 | ||||
|                 pendingTableRow = null; | ||||
|             } | ||||
| 
 | ||||
|             handled = true; | ||||
| 
 | ||||
|         } else if (node instanceof TableCell) { | ||||
| 
 | ||||
|             final TableCell cell = (TableCell) node; | ||||
| @ -383,11 +424,13 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         } else { | ||||
|             handled = false; | ||||
|         } | ||||
| 
 | ||||
|         return handled; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Paragraph paragraph) { | ||||
| 
 | ||||
|         final boolean inTightList = isInTightList(paragraph); | ||||
| 
 | ||||
|         if (!inTightList) { | ||||
| @ -400,9 +443,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         // @since 1.1.1 apply paragraph span | ||||
|         setSpan(length, factory.paragraph(inTightList)); | ||||
| 
 | ||||
|         if (!inTightList) { | ||||
|         if (hasNext(paragraph) && !inTightList) { | ||||
|             newLine(); | ||||
| 
 | ||||
|             if (blockQuoteIndent == 0) { | ||||
|                 builder.append('\n'); | ||||
|             } | ||||
| @ -518,4 +560,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         } | ||||
|         return out; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 2.0.0 | ||||
|      */ | ||||
|     protected static boolean hasNext(@NonNull Node node) { | ||||
|         return node.getNext() != null; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -61,7 +61,7 @@ public class SpannableMarkdownVisitorTest { | ||||
|         } | ||||
| 
 | ||||
|         // 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 int length = spans != null | ||||
|  | ||||
| @ -5,7 +5,6 @@ import android.support.annotation.Nullable; | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| 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(); | ||||
|             } | ||||
| @ -106,6 +108,7 @@ abstract class TestValidator { | ||||
|                     .filter(new IxPredicate<TestSpan>() { | ||||
|                         @Override | ||||
|                         public boolean test(TestSpan testSpan) { | ||||
| 
 | ||||
|                             // in case of nested spans with the same name (lists) | ||||
|                             // we also must validate attributes | ||||
|                             // and thus we are moving most of assertions to this filter method | ||||
| @ -170,16 +173,21 @@ abstract class TestValidator { | ||||
|                 spansText = "[]"; | ||||
|             } else { | ||||
|                 final StringBuilder builder = new StringBuilder(); | ||||
|                 for (Object o: spans) { | ||||
|                 for (Object o : spans) { | ||||
|                     final TestSpan testSpan = (TestSpan) o; | ||||
|                     if (builder.length() > 0) { | ||||
|                         builder.append(", "); | ||||
|                     } | ||||
| 
 | ||||
|                     builder | ||||
|                             .append("{name: '").append(testSpan.name()).append('\'') | ||||
|                             .append(", position{").append(start).append(", ").append(end).append('}') | ||||
|                             .append(", attributes: ").append(testSpan.attributes()) | ||||
|                             .append('}'); | ||||
|                             .append(", position{").append(start).append(", ").append(end).append('}'); | ||||
| 
 | ||||
|                     if (testSpan.attributes().size() > 0) { | ||||
|                         builder.append(", attributes: ").append(testSpan.attributes()); | ||||
|                     } | ||||
| 
 | ||||
|                     builder.append('}'); | ||||
|                 } | ||||
|                 spansText = builder.toString(); | ||||
|             } | ||||
|  | ||||
							
								
								
									
										17
									
								
								markwon/src/test/resources/tests/code-blocks.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								markwon/src/test/resources/tests/code-blocks.yaml
									
									
									
									
									
										Normal 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" | ||||
| @ -21,8 +21,8 @@ output: | ||||
|   - text: " " | ||||
|   - s: "strike" | ||||
|   - text: " down\n\n" | ||||
|   - blockquote: "Some quote here!\n" | ||||
|   - text: "\n" | ||||
|   - blockquote: "Some quote here!" | ||||
|   - text: "\n\n" | ||||
|   - h1: "Header 1" | ||||
|   - text: "\n\n" | ||||
|   - h2: "Header 2" | ||||
|  | ||||
| @ -10,8 +10,8 @@ output: | ||||
|   - ul: | ||||
|     - ul: "Second" | ||||
|       level: 1 | ||||
|     - text: "\n" | ||||
|     level: 0 | ||||
|   - text: "\n" | ||||
|   - ul: | ||||
|     - ul: | ||||
|       - ul: "Third" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov