Working with tables
This commit is contained in:
		
							parent
							
								
									37bf9f79db
								
							
						
					
					
						commit
						392f53c133
					
				
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							| @ -110,24 +110,19 @@ Lorem ipsum dolor sit amet | ||||
| ### H.T.M.L. | ||||
| <b>O</b><i>K<s>A</s><sup>42<sup>43<sub><b>42</b></sub></sup></sup><u>Y</u></i> | ||||
| 
 | ||||
| <img src="h" /> <img src="h"> | ||||
| <img src="h" alt="alt text"> | ||||
| ### Tables | ||||
| Header #1 | Header #2 | Header #3 | ||||
| ---: | :---: | :--- | ||||
| content | content | content | ||||
| long long long skjfs fgjsdfhj sf `dfk df` | sdsd,fklsdfklsdfklsdfkl sdfkl dsfjksdf sjkf jksdfjksdf sjkdf sdfkjsdjkf sdkjfs fkjsf sdkjfs fkjsd fkjsdf skjdf sdkjf skjfs fkjs fkjsdf jskdf sdjkf sjdkf sdkjf skjf sdkjf sdkjf sdfkjsd fkjsd fkjsdf sdkjfsjk dfkjsdf sdkjfs | yeah | ||||
| 
 | ||||
| <hr> | ||||
| 
 | ||||
| <hr /> | ||||
| 
 | ||||
| <h1>Hello</h1> | ||||
| 
 | ||||
| <h2>Hello</h2> | ||||
| 
 | ||||
| <h3>Hello</h3> | ||||
| 
 | ||||
| <h4>Hello</h4> | ||||
| 
 | ||||
| <h5>Hello</h5> | ||||
| 
 | ||||
| <h6>Hello</h6> | ||||
| |head #1| head #2| | ||||
| |---|---| | ||||
| | content | content | | ||||
| | content | content | | ||||
| | content | content | | ||||
| | content | content | | ||||
| 
 | ||||
| 
 | ||||
| [1]: https://github.com | ||||
|  | ||||
| @ -60,7 +60,7 @@ public class MainActivity extends Activity { | ||||
| 
 | ||||
|         markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { | ||||
|             @Override | ||||
|             public void apply(String text) { | ||||
|             public void apply(final String text) { | ||||
|                 markdownRenderer.render(MainActivity.this, uri(), text, new MarkdownRenderer.MarkdownReadyListener() { | ||||
|                     @Override | ||||
|                     public void onMarkdownReady(CharSequence markdown) { | ||||
|  | ||||
| @ -32,6 +32,7 @@ ext { | ||||
|     final def commonMarkVersion = '0.9.0' | ||||
|     COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion" | ||||
|     COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion" | ||||
|     COMMON_MARK_TABLE = "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion" | ||||
| 
 | ||||
|     ANDROID_SVG = 'com.caverock:androidsvg:1.2.1' | ||||
|     ANDROID_GIF = 'pl.droidsonroids.gif:android-gif-drawable:1.2.7' | ||||
|  | ||||
| @ -17,4 +17,7 @@ dependencies { | ||||
|     compile SUPPORT_ANNOTATIONS | ||||
|     compile COMMON_MARK | ||||
|     compile COMMON_MARK_STRIKETHROUGHT | ||||
|     compile COMMON_MARK_TABLE | ||||
| 
 | ||||
|     compile 'ru.noties:debug:3.0.0@jar' | ||||
| } | ||||
|  | ||||
| @ -8,10 +8,11 @@ import android.text.method.LinkMovementMethod; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; | ||||
| import org.commonmark.ext.gfm.tables.TablesExtension; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.util.Collections; | ||||
| import java.util.Arrays; | ||||
| 
 | ||||
| import ru.noties.markwon.renderer.SpannableRenderer; | ||||
| 
 | ||||
| @ -20,7 +21,7 @@ public abstract class Markwon { | ||||
| 
 | ||||
|     public static Parser createParser() { | ||||
|         return new Parser.Builder() | ||||
|                 .extensions(Collections.singleton(StrikethroughExtension.create())) | ||||
|                 .extensions(Arrays.asList(StrikethroughExtension.create(), TablesExtension.create())) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
| @ -40,6 +41,7 @@ public abstract class Markwon { | ||||
|     public static void setText(@NonNull TextView view, CharSequence text) { | ||||
| 
 | ||||
|         unscheduleDrawables(view); | ||||
|         unscheduleTableRows(view); | ||||
| 
 | ||||
|         // update movement method (for links to be clickable) | ||||
|         view.setMovementMethod(LinkMovementMethod.getInstance()); | ||||
| @ -47,6 +49,7 @@ public abstract class Markwon { | ||||
| 
 | ||||
|         // schedule drawables (dynamic drawables that can change bounds/animate will be correctly updated) | ||||
|         scheduleDrawables(view); | ||||
|         scheduleTableRows(view); | ||||
|     } | ||||
| 
 | ||||
|     // with default configuration | ||||
| @ -82,6 +85,14 @@ public abstract class Markwon { | ||||
|         DrawablesScheduler.unschedule(view); | ||||
|     } | ||||
| 
 | ||||
|     public static void scheduleTableRows(@NonNull TextView view) { | ||||
|         TableRowsScheduler.schedule(view); | ||||
|     } | ||||
| 
 | ||||
|     public static void unscheduleTableRows(@NonNull TextView view) { | ||||
|         TableRowsScheduler.unschedule(view); | ||||
|     } | ||||
| 
 | ||||
|     private Markwon() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,64 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.Spanned; | ||||
| import android.text.TextUtils; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import ru.noties.markwon.spans.TableRowSpan; | ||||
| 
 | ||||
| abstract class TableRowsScheduler { | ||||
| 
 | ||||
|     static void schedule(@NonNull final TextView view) { | ||||
|         final Object[] spans = extract(view); | ||||
|         if (spans != null | ||||
|                 && spans.length > 0) { | ||||
|             view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { | ||||
|                 @Override | ||||
|                 public void onViewAttachedToWindow(View v) { | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onViewDetachedFromWindow(View v) { | ||||
|                     unschedule(view); | ||||
|                     view.removeOnAttachStateChangeListener(this); | ||||
|                 } | ||||
|             }); | ||||
|             final TableRowSpan.Invalidator invalidator = new TableRowSpan.Invalidator() { | ||||
|                 @Override | ||||
|                 public void invalidate() { | ||||
|                     view.setText(view.getText()); | ||||
|                 } | ||||
|             }; | ||||
|             for (Object span : spans) { | ||||
|                 ((TableRowSpan) span).invalidator(invalidator); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static void unschedule(@NonNull TextView view) { | ||||
|         final Object[] spans = extract(view); | ||||
|         if (spans != null | ||||
|                 && spans.length > 0) { | ||||
|             for (Object span : spans) { | ||||
|                 ((TableRowSpan) span).invalidator(null); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static Object[] extract(@NonNull TextView view) { | ||||
|         final Object[] out; | ||||
|         final CharSequence text = view.getText(); | ||||
|         if (!TextUtils.isEmpty(text) && text instanceof Spanned) { | ||||
|             out = ((Spanned) text).getSpans(0, text.length(), TableRowSpan.class); | ||||
|         } else { | ||||
|             out = null; | ||||
|         } | ||||
|         return out; | ||||
|     } | ||||
| 
 | ||||
|     private TableRowsScheduler() { | ||||
|     } | ||||
| } | ||||
| @ -7,6 +7,9 @@ import android.text.TextUtils; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| 
 | ||||
| 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.TableRow; | ||||
| import org.commonmark.node.AbstractVisitor; | ||||
| import org.commonmark.node.BlockQuote; | ||||
| import org.commonmark.node.BulletList; | ||||
| @ -31,8 +34,11 @@ import org.commonmark.node.Text; | ||||
| import org.commonmark.node.ThematicBreak; | ||||
| 
 | ||||
| import java.util.ArrayDeque; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Deque; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import ru.noties.debug.Debug; | ||||
| import ru.noties.markwon.SpannableConfiguration; | ||||
| import ru.noties.markwon.renderer.html.SpannableHtmlParser; | ||||
| import ru.noties.markwon.spans.AsyncDrawable; | ||||
| @ -45,6 +51,7 @@ import ru.noties.markwon.spans.HeadingSpan; | ||||
| import ru.noties.markwon.spans.LinkSpan; | ||||
| import ru.noties.markwon.spans.OrderedListItemSpan; | ||||
| import ru.noties.markwon.spans.StrongEmphasisSpan; | ||||
| import ru.noties.markwon.spans.TableRowSpan; | ||||
| import ru.noties.markwon.spans.ThematicBreakSpan; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| @ -259,17 +266,85 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     private List<TableRowSpan.Cell> pendingTableRow; | ||||
|     private boolean tableRowIsHeader; | ||||
|     private int tableRows; | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(CustomNode customNode) { | ||||
| 
 | ||||
| //        Log.e(null, String.valueOf(customNode)); | ||||
| 
 | ||||
|         if (customNode instanceof Strikethrough) { | ||||
| 
 | ||||
|             final int length = builder.length(); | ||||
|             visitChildren(customNode); | ||||
|             setSpan(length, new StrikethroughSpan()); | ||||
|         } else { | ||||
| 
 | ||||
|         } else if (!handleTableNodes(customNode)) { | ||||
|             super.visit(customNode); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private boolean handleTableNodes(CustomNode node) { | ||||
| 
 | ||||
|         final boolean handled; | ||||
| 
 | ||||
|         Debug.i(node); | ||||
| 
 | ||||
|         if (node instanceof TableBody) { | ||||
|             visitChildren(node); | ||||
|             tableRows = 0; | ||||
|             handled = true; | ||||
|             newLine(); | ||||
|             builder.append('\n'); | ||||
|         } else if (node instanceof TableRow) { | ||||
| 
 | ||||
|             final int length = builder.length(); | ||||
|             visitChildren(node); | ||||
| 
 | ||||
|             if (pendingTableRow != null) { | ||||
|                 builder.append(' '); | ||||
| 
 | ||||
|                 final TableRowSpan span = new TableRowSpan( | ||||
|                         configuration.theme(), | ||||
|                         pendingTableRow, | ||||
|                         tableRowIsHeader, | ||||
|                         tableRows % 2 == 1 | ||||
|                 ); | ||||
| 
 | ||||
|                 setSpan(length, span); | ||||
|                 newLine(); | ||||
|                 pendingTableRow = null; | ||||
|             } | ||||
| 
 | ||||
|             handled = true; | ||||
|         } else if (node instanceof TableCell) { | ||||
| 
 | ||||
|             final TableCell cell = (TableCell) node; | ||||
|             final int length = builder.length(); | ||||
|             visitChildren(cell); | ||||
|             if (pendingTableRow == null) { | ||||
|                 pendingTableRow = new ArrayList<>(2); | ||||
|             } | ||||
|             pendingTableRow.add(new TableRowSpan.Cell( | ||||
|                     tableCellAlignment(cell.getAlignment()), | ||||
|                     builder.subSequence(length, builder.length()) | ||||
|             )); | ||||
|             builder.replace(length, builder.length(), ""); | ||||
| 
 | ||||
|             tableRowIsHeader = cell.isHeader(); | ||||
|             tableRows = tableRowIsHeader | ||||
|                     ? 0 | ||||
|                     : tableRows + 1; | ||||
| 
 | ||||
|             handled = true; | ||||
|         } else { | ||||
|             handled = false; | ||||
|         } | ||||
|         return handled; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Paragraph paragraph) { | ||||
| 
 | ||||
| @ -402,6 +477,27 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @TableRowSpan.Alignment | ||||
|     private static int tableCellAlignment(TableCell.Alignment alignment) { | ||||
|         final int out; | ||||
|         if (alignment != null) { | ||||
|             switch (alignment) { | ||||
|                 case CENTER: | ||||
|                     out = TableRowSpan.ALIGN_CENTER; | ||||
|                     break; | ||||
|                 case RIGHT: | ||||
|                     out = TableRowSpan.ALIGN_RIGHT; | ||||
|                     break; | ||||
|                 default: | ||||
|                     out = TableRowSpan.ALIGN_LEFT; | ||||
|                     break; | ||||
|             } | ||||
|         } else { | ||||
|             out = TableRowSpan.ALIGN_LEFT; | ||||
|         } | ||||
|         return out; | ||||
|     } | ||||
| 
 | ||||
|     private static class HtmlInlineItem { | ||||
| 
 | ||||
|         final SpannableHtmlParser.Tag tag; | ||||
|  | ||||
| @ -16,7 +16,7 @@ public class SpannableTheme { | ||||
| 
 | ||||
|     // this method should be used if TextView is known beforehand | ||||
|     // it will correctly measure the `space` char and set it as `codeMultilineMargin` | ||||
|     // otherwise this value must be set explicitly ( | ||||
|     // otherwise this value must be set explicitly | ||||
|     public static SpannableTheme create(@NonNull TextView textView) { | ||||
|         return builderWithDefaults(textView.getContext()) | ||||
|                 .codeMultilineMargin((int) (textView.getPaint().measureText("\u00a0") + .5F)) | ||||
|  | ||||
| @ -0,0 +1,217 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| import android.support.annotation.IntDef; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.Layout; | ||||
| import android.text.StaticLayout; | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.ReplacementSpan; | ||||
| 
 | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class TableRowSpan extends ReplacementSpan { | ||||
| 
 | ||||
|     public static final int ALIGN_LEFT = 0; | ||||
|     public static final int ALIGN_CENTER = 1; | ||||
|     public static final int ALIGN_RIGHT = 2; | ||||
| 
 | ||||
|     @IntDef(value = {ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT}) | ||||
|     @Retention(RetentionPolicy.SOURCE) | ||||
|     public @interface Alignment { | ||||
|     } | ||||
| 
 | ||||
|     public interface Invalidator { | ||||
|         void invalidate(); | ||||
|     } | ||||
| 
 | ||||
|     public static class Cell { | ||||
| 
 | ||||
|         final int alignment; | ||||
|         final CharSequence text; | ||||
| 
 | ||||
|         public Cell(@Alignment int alignment, CharSequence text) { | ||||
|             this.alignment = alignment; | ||||
|             this.text = text; | ||||
|         } | ||||
| 
 | ||||
|         @Alignment | ||||
|         public int alignment() { | ||||
|             return alignment; | ||||
|         } | ||||
| 
 | ||||
|         public CharSequence text() { | ||||
|             return text; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public String toString() { | ||||
|             return "Cell{" + | ||||
|                     "alignment=" + alignment + | ||||
|                     ", text=" + text + | ||||
|                     '}'; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private final SpannableTheme theme; | ||||
|     private final List<Cell> cells; | ||||
|     private final List<StaticLayout> layouts; | ||||
|     private final TextPaint textPaint; | ||||
|     private final boolean header; | ||||
|     private final boolean odd; | ||||
| 
 | ||||
|     private int width; | ||||
|     private int height; | ||||
|     private Invalidator invalidator; | ||||
| 
 | ||||
|     public TableRowSpan( | ||||
|             @NonNull SpannableTheme theme, | ||||
|             @NonNull List<Cell> cells, | ||||
|             boolean header, | ||||
|             boolean odd) { | ||||
|         this.theme = theme; | ||||
|         this.cells = cells; | ||||
|         this.layouts = new ArrayList<>(cells.size()); | ||||
|         this.textPaint = new TextPaint(); | ||||
|         this.header = header; | ||||
|         this.odd = odd; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getSize( | ||||
|             @NonNull Paint paint, | ||||
|             CharSequence text, | ||||
|             @IntRange(from = 0) int start, | ||||
|             @IntRange(from = 0) int end, | ||||
|             @Nullable Paint.FontMetricsInt fm) { | ||||
| 
 | ||||
|         // it's our absolute requirement to have width of the canvas here... because, well, it changes | ||||
|         // the way we draw text. So, if we do not know the width of canvas we cannot correctly measure our text | ||||
| 
 | ||||
|         if (layouts.size() > 0) { | ||||
| 
 | ||||
|             if (fm != null) { | ||||
| 
 | ||||
|                 int max = 0; | ||||
|                 for (StaticLayout layout : layouts) { | ||||
|                     final int height = layout.getHeight(); | ||||
|                     if (height > max) { | ||||
|                         max = height; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 height = max; | ||||
| 
 | ||||
|                 fm.ascent = -max; | ||||
|                 fm.descent = 0; | ||||
| 
 | ||||
|                 fm.top = fm.ascent; | ||||
|                 fm.bottom = 0; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return width; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void draw( | ||||
|             @NonNull Canvas canvas, | ||||
|             CharSequence text, | ||||
|             @IntRange(from = 0) int start, | ||||
|             @IntRange(from = 0) int end, | ||||
|             float x, | ||||
|             int top, | ||||
|             int y, | ||||
|             int bottom, | ||||
|             @NonNull Paint paint) { | ||||
| 
 | ||||
|         if (recreateLayouts(canvas.getWidth())) { | ||||
|             width = canvas.getWidth(); | ||||
|             textPaint.set(paint); | ||||
|             makeNewLayouts(); | ||||
|         } | ||||
| 
 | ||||
|         int maxHeight = 0; | ||||
| 
 | ||||
|         StaticLayout layout; | ||||
|         for (int i = 0, size = layouts.size(); i < size; i++) { | ||||
|             layout = layouts.get(i); | ||||
|             final int save = canvas.save(); | ||||
|             try { | ||||
| 
 | ||||
|                 canvas.translate(x + (i * layout.getWidth()), top); | ||||
|                 layout.draw(canvas); | ||||
| 
 | ||||
|                 if (layout.getHeight() > maxHeight) { | ||||
|                     maxHeight = layout.getHeight(); | ||||
|                 } | ||||
| 
 | ||||
|             } finally { | ||||
|                 canvas.restoreToCount(save); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (height != maxHeight) { | ||||
|             if (invalidator != null) { | ||||
|                 invalidator.invalidate(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private boolean recreateLayouts(int newWidth) { | ||||
|         return width != newWidth; | ||||
|     } | ||||
| 
 | ||||
|     private void makeNewLayouts() { | ||||
| 
 | ||||
|         if (header) { | ||||
|             textPaint.setFakeBoldText(true); | ||||
|         } | ||||
| 
 | ||||
|         final int w = width / cells.size(); | ||||
| 
 | ||||
|         this.layouts.clear(); | ||||
|         Cell cell; | ||||
|         StaticLayout layout; | ||||
|         for (int i = 0, size = cells.size(); i < size; i++) { | ||||
|             cell = cells.get(i); | ||||
|             layout = new StaticLayout( | ||||
|                     cell.text, | ||||
|                     textPaint, | ||||
|                     w, | ||||
|                     alignment(cell.alignment), | ||||
|                     1.F, | ||||
|                     .0F, | ||||
|                     false | ||||
|             ); | ||||
|             layouts.add(layout); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static Layout.Alignment alignment(@Alignment int alignment) { | ||||
|         final Layout.Alignment out; | ||||
|         switch (alignment) { | ||||
|             case ALIGN_CENTER: | ||||
|                 out = Layout.Alignment.ALIGN_CENTER; | ||||
|                 break; | ||||
|             case ALIGN_RIGHT: | ||||
|                 out = Layout.Alignment.ALIGN_OPPOSITE; | ||||
|                 break; | ||||
|             default: | ||||
|                 out = Layout.Alignment.ALIGN_NORMAL; | ||||
|                 break; | ||||
|         } | ||||
|         return out; | ||||
|     } | ||||
| 
 | ||||
|     public TableRowSpan invalidator(Invalidator invalidator) { | ||||
|         this.invalidator = invalidator; | ||||
|         return this; | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov