diff --git a/README.md b/README.md
index bb5433a6..9a08d469 100644
--- a/README.md
+++ b/README.md
@@ -110,24 +110,19 @@ Lorem ipsum dolor sit amet
### H.T.M.L.
OKA424342Y
-
-
+### 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
-
-
-
-Hello
-
-Hello
-
-Hello
-
-Hello
-
-Hello
-
-Hello
+|head #1| head #2|
+|---|---|
+| content | content |
+| content | content |
+| content | content |
+| content | content |
[1]: https://github.com
diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java
index ba311cbd..8b5b10dc 100644
--- a/app/src/main/java/ru/noties/markwon/MainActivity.java
+++ b/app/src/main/java/ru/noties/markwon/MainActivity.java
@@ -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) {
diff --git a/build.gradle b/build.gradle
index ece44916..5391065a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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'
diff --git a/library-renderer/build.gradle b/library-renderer/build.gradle
index 7d224d7f..3215ca12 100644
--- a/library-renderer/build.gradle
+++ b/library-renderer/build.gradle
@@ -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'
}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/Markwon.java b/library-renderer/src/main/java/ru/noties/markwon/Markwon.java
index 739f98a5..e76deb38 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/Markwon.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/Markwon.java
@@ -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() {
}
}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/TableRowsScheduler.java b/library-renderer/src/main/java/ru/noties/markwon/TableRowsScheduler.java
new file mode 100644
index 00000000..ac931df1
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/TableRowsScheduler.java
@@ -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() {
+ }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
index 70fef6a6..ba64fdf9 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
@@ -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 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;
diff --git a/library-renderer/src/main/java/ru/noties/markwon/spans/SpannableTheme.java b/library-renderer/src/main/java/ru/noties/markwon/spans/SpannableTheme.java
index c25878ce..b8e4bfa6 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/spans/SpannableTheme.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/spans/SpannableTheme.java
@@ -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))
diff --git a/library-renderer/src/main/java/ru/noties/markwon/spans/TableRowSpan.java b/library-renderer/src/main/java/ru/noties/markwon/spans/TableRowSpan.java
new file mode 100644
index 00000000..f5fc26f4
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/spans/TableRowSpan.java
@@ -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 cells;
+ private final List 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 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;
+ }
+}
| |