From 24b95e2ffb5756eb380dace4ab33698f24cd3568 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 25 Dec 2018 15:08:28 +0300 Subject: [PATCH] Table parsing and improvements for recycler artifact --- .../ru/noties/markwon/ext/tables/Table.java | 220 +++++++++++++++ .../markwon/ext/tables/TablePlugin.java | 27 +- .../markwon/recycler/MarkwonAdapter.java | 23 +- .../markwon/recycler/MarkwonAdapterImpl.java | 31 +-- .../markwon/recycler/MarkwonRecycler.java | 4 - ...{SimpleNodeEntry.java => SimpleEntry.java} | 8 +- .../layout/markwon_adapter_simple_entry.xml | 9 + .../java/ru/noties/markwon/MarkwonImpl.java | 2 +- .../recycler/MarkwonRecyclerActivity.java | 5 +- .../extension/recycler/TableNodeEntry.java | 255 ++---------------- .../main/res/layout/adapter_default_entry.xml | 5 +- 11 files changed, 315 insertions(+), 274 deletions(-) create mode 100644 markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java delete mode 100644 markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java rename markwon-recycler/src/main/java/ru/noties/markwon/recycler/{SimpleNodeEntry.java => SimpleEntry.java} (90%) create mode 100644 markwon-recycler/src/main/res/layout/markwon_adapter_simple_entry.xml rename markwon-recycler/src/main/res/layout/adapter_simple_entry.xml => sample-custom-extension/src/main/res/layout/adapter_default_entry.xml (80%) diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java new file mode 100644 index 00000000..85e1cfa4 --- /dev/null +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java @@ -0,0 +1,220 @@ +package ru.noties.markwon.ext.tables; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spanned; + +import org.commonmark.ext.gfm.tables.TableBlock; +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.CustomNode; +import org.commonmark.node.Node; + +import java.util.ArrayList; +import java.util.List; + +import ru.noties.markwon.Markwon; + +/** + * A class to parse TableBlock and return a data-structure that is not dependent + * on commonmark-java table extension. Can be useful when rendering tables require special + * handling (multiple views, specific table view) for example when used with `markwon-recycler` artifact + * + * @see #parse(Markwon, TableBlock) + * @since 3.0.0 + */ +public class Table { + + /** + * Factory method to obtain an instance of {@link Table} + * + * @param markwon Markwon + * @param tableBlock TableBlock to parse + * @return parsed {@link Table} or null + */ + @Nullable + public static Table parse(@NonNull Markwon markwon, @NonNull TableBlock tableBlock) { + + final Table table; + + final ParseVisitor visitor = new ParseVisitor(markwon); + tableBlock.accept(visitor); + final List rows = visitor.rows(); + + if (rows == null) { + table = null; + } else { + table = new Table(rows); + } + + return table; + } + + public static class Row { + + private final boolean isHeader; + private final List columns; + + public Row( + boolean isHeader, + @NonNull List columns) { + this.isHeader = isHeader; + this.columns = columns; + } + + public boolean header() { + return isHeader; + } + + @NonNull + public List columns() { + return columns; + } + + @Override + public String toString() { + return "Row{" + + "isHeader=" + isHeader + + ", columns=" + columns + + '}'; + } + } + + public static class Column { + + private final Alignment alignment; + private final Spanned content; + + public Column(@NonNull Alignment alignment, @NonNull Spanned content) { + this.alignment = alignment; + this.content = content; + } + + @NonNull + public Alignment alignment() { + return alignment; + } + + @NonNull + public Spanned content() { + return content; + } + + @Override + public String toString() { + return "Column{" + + "alignment=" + alignment + + ", content=" + content + + '}'; + } + } + + public enum Alignment { + LEFT, + CENTER, + RIGHT + } + + private final List rows; + + public Table(@NonNull List rows) { + this.rows = rows; + } + + @NonNull + public List rows() { + return rows; + } + + @Override + public String toString() { + return "Table{" + + "rows=" + rows + + '}'; + } + + static class ParseVisitor extends AbstractVisitor { + + private final Markwon markwon; + + private List rows; + + private List pendingRow; + private boolean pendingRowIsHeader; + + ParseVisitor(@NonNull Markwon markwon) { + this.markwon = markwon; + } + + @Nullable + public List rows() { + return rows; + } + + @Override + public void visit(CustomNode customNode) { + + if (customNode instanceof TableCell) { + + final TableCell cell = (TableCell) customNode; + + final Node firstChild = cell.getFirstChild(); + + // need to investigate why... (most likely initial node is modified by someone) + if (firstChild != null) { + + if (pendingRow == null) { + pendingRow = new ArrayList<>(2); + } + + // let's TRY to not visit this node but instead try to render its first child + + pendingRow.add(new Table.Column(alignment(cell.getAlignment()), markwon.render(firstChild))); + pendingRowIsHeader = cell.isHeader(); + } + + return; + } + + if (customNode instanceof TableHead + || customNode instanceof TableRow) { + + visitChildren(customNode); + + // this can happen, ignore such row + if (pendingRow != null && pendingRow.size() > 0) { + + if (rows == null) { + rows = new ArrayList<>(2); + } + + rows.add(new Table.Row(pendingRowIsHeader, pendingRow)); + } + + pendingRow = null; + pendingRowIsHeader = false; + + return; + } + + visitChildren(customNode); + } + + @NonNull + private static Table.Alignment alignment(@NonNull TableCell.Alignment alignment) { + final Table.Alignment out; + if (TableCell.Alignment.RIGHT == alignment) { + out = Table.Alignment.RIGHT; + } else if (TableCell.Alignment.CENTER == alignment) { + out = Table.Alignment.CENTER; + } else { + out = Table.Alignment.LEFT; + } + return out; + } + } + + +} diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java index d9220b01..ae547435 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -33,10 +33,11 @@ public class TablePlugin extends AbstractMarkwonPlugin { return new TablePlugin(tableTheme); } - private final TableTheme tableTheme; + private final TableVisitor visitor; + @SuppressWarnings("WeakerAccess") TablePlugin(@NonNull TableTheme tableTheme) { - this.tableTheme = tableTheme; + this.visitor = new TableVisitor(tableTheme); } @Override @@ -46,7 +47,12 @@ public class TablePlugin extends AbstractMarkwonPlugin { @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - TableVisitor.configure(tableTheme, builder); + visitor.configure(builder); + } + + @Override + public void beforeRender(@NonNull Node node) { + visitor.clear(); } @Override @@ -61,18 +67,23 @@ public class TablePlugin extends AbstractMarkwonPlugin { private static class TableVisitor { - static void configure(@NonNull TableTheme tableTheme, @NonNull MarkwonVisitor.Builder builder) { - new TableVisitor(tableTheme, builder); - } - private final TableTheme tableTheme; private List pendingTableRow; private boolean tableRowIsHeader; private int tableRows; - private TableVisitor(@NonNull TableTheme tableTheme, @NonNull MarkwonVisitor.Builder builder) { + TableVisitor(@NonNull TableTheme tableTheme) { this.tableTheme = tableTheme; + } + + void clear() { + pendingTableRow = null; + tableRowIsHeader = false; + tableRows = 0; + } + + void configure(@NonNull MarkwonVisitor.Builder builder) { builder .on(TableBody.class, new MarkwonVisitor.NodeVisitor() { @Override diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java index c5334879..92f22605 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java @@ -15,7 +15,25 @@ import java.util.List; import ru.noties.markwon.Markwon; -// each node block will be rendered by a simple TextView, but we can provide own entries for each block +/** + * Class to display markdown in a RecyclerView. It is done by extracting root blocks from a + * parsed markdown document and rendering each block in a standalone RecyclerView entry. Provides + * ability to customize rendering of blocks. For example display certain blocks in a horizontal + * scrolling container or display tables in a specific widget designed for it ({@link Builder#include(Class, Entry)}). + *

+ * By default each node will be rendered in a TextView provided by this artifact. It has no styling + * information and thus must be replaced with your own layout ({@link Builder#defaultEntry(int)} or + * {@link Builder#defaultEntry(Entry)}). + * + * @see #builder() + * @see #create() + * @see #create(int) + * @see #create(Entry) + * @see #setMarkdown(Markwon, String) + * @see #setParsedMarkdown(Markwon, Node) + * @see #setParsedMarkdown(Markwon, List) + * @since 3.0.0 + */ public abstract class MarkwonAdapter extends RecyclerView.Adapter { @NonNull @@ -59,6 +77,9 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter { @NonNull diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java index f95cf129..5a8d2fa6 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java @@ -1,7 +1,6 @@ package ru.noties.markwon.recycler; import android.support.annotation.NonNull; -import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -25,6 +24,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { private Markwon markwon; private List nodes; + @SuppressWarnings("WeakerAccess") MarkwonAdapterImpl( @NonNull SparseArray> entries, @NonNull Entry defaultEntry, @@ -67,9 +67,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { layoutInflater = LayoutInflater.from(parent.getContext()); } - final Entry entry = viewType == 0 - ? defaultEntry - : entries.get(viewType); + final Entry entry = getEntry(viewType); return entry.createHolder(layoutInflater, parent); } @@ -80,9 +78,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { final Node node = nodes.get(position); final int viewType = getNodeViewType(node.getClass()); - final Entry entry = viewType == 0 - ? defaultEntry - : entries.get(viewType); + final Entry entry = getEntry(viewType); entry.bindHolder(markwon, holder, node); } @@ -94,6 +90,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { : 0; } + @SuppressWarnings("unused") @NonNull public List getItems() { return nodes != null @@ -110,12 +107,11 @@ class MarkwonAdapterImpl extends MarkwonAdapter { public long getItemId(int position) { final Node node = nodes.get(position); final int type = getNodeViewType(node.getClass()); - final Entry entry = type == 0 - ? defaultEntry - : entries.get(type); + final Entry entry = getEntry(type); return entry.id(node); } + @SuppressWarnings("WeakerAccess") public int getNodeViewType(@NonNull Class node) { // if has registered -> then return it, else 0 final int hash = node.hashCode(); @@ -125,6 +121,13 @@ class MarkwonAdapterImpl extends MarkwonAdapter { return 0; } + @NonNull + private Entry getEntry(int viewType) { + return viewType == 0 + ? defaultEntry + : entries.get(viewType); + } + static class BuilderImpl implements Builder { private final SparseArray> entries = new SparseArray<>(3); @@ -154,7 +157,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { @Override public Builder defaultEntry(int layoutResId) { //noinspection unchecked - this.defaultEntry = (Entry) (Entry) new SimpleNodeEntry(layoutResId); + this.defaultEntry = (Entry) (Entry) new SimpleEntry(layoutResId); return this; } @@ -171,7 +174,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { if (defaultEntry == null) { //noinspection unchecked - defaultEntry = (Entry) (Entry) new SimpleNodeEntry(); + defaultEntry = (Entry) (Entry) new SimpleEntry(); } if (reducer == null) { @@ -190,7 +193,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { final List list = new ArrayList<>(); -// // we will extract all blocks that are direct children of Document + // we will extract all blocks that are direct children of Document Node node = root.getFirstChild(); Node temp; @@ -201,8 +204,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter { node = temp; } - Log.e("NODES", list.toString()); - return list; } } diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java deleted file mode 100644 index 2244f37b..00000000 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java +++ /dev/null @@ -1,4 +0,0 @@ -package ru.noties.markwon.recycler; - -public class MarkwonRecycler { -} diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java similarity index 90% rename from markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java rename to markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java index e5d64590..d04a7477 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java @@ -17,7 +17,7 @@ import java.util.Map; import ru.noties.markwon.Markwon; -public class SimpleNodeEntry implements MarkwonAdapter.Entry { +public class SimpleEntry implements MarkwonAdapter.Entry { private static final NoCopySpannableFactory FACTORY = new NoCopySpannableFactory(); @@ -26,11 +26,11 @@ public class SimpleNodeEntry implements MarkwonAdapter.Entry + \ No newline at end of file diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index d9b74a25..3cf105b5 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -65,7 +65,7 @@ class MarkwonImpl extends Markwon { final Spanned spanned = visitor.builder().spannableStringBuilder(); - // clear render props and builder after rending + // clear render props and builder after rendering visitor.clear(); return spanned; diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java index 4ae85cc5..34d07ec8 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java @@ -29,7 +29,7 @@ import ru.noties.markwon.html.HtmlPlugin; import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.svg.SvgPlugin; import ru.noties.markwon.recycler.MarkwonAdapter; -import ru.noties.markwon.recycler.SimpleNodeEntry; +import ru.noties.markwon.recycler.SimpleEntry; import ru.noties.markwon.sample.extension.R; import ru.noties.markwon.urlprocessor.UrlProcessor; import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; @@ -48,8 +48,9 @@ public class MarkwonRecyclerActivity extends Activity { setContentView(R.layout.activity_recycler); final MarkwonAdapter adapter = MarkwonAdapter.builder() - .include(FencedCodeBlock.class, new SimpleNodeEntry(R.layout.adapter_fenced_code_block)) + .include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block)) .include(TableBlock.class, new TableNodeEntry()) + .defaultEntry(new SimpleEntry(R.layout.adapter_default_entry)) .build(); final RecyclerView recyclerView = findViewById(R.id.recycler_view); diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java index 31c05783..c8c7b67b 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java @@ -1,48 +1,26 @@ package ru.noties.markwon.sample.extension.recycler; import android.support.annotation.NonNull; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TableLayout; -import android.widget.TableRow; -import android.widget.TextView; import org.commonmark.ext.gfm.tables.TableBlock; -import org.commonmark.node.AbstractVisitor; -import org.commonmark.node.BlockQuote; -import org.commonmark.node.BulletList; -import org.commonmark.node.Code; -import org.commonmark.node.CustomBlock; -import org.commonmark.node.CustomNode; -import org.commonmark.node.Document; -import org.commonmark.node.Emphasis; -import org.commonmark.node.FencedCodeBlock; -import org.commonmark.node.HardLineBreak; -import org.commonmark.node.Heading; -import org.commonmark.node.HtmlBlock; -import org.commonmark.node.HtmlInline; -import org.commonmark.node.Image; -import org.commonmark.node.IndentedCodeBlock; -import org.commonmark.node.Link; -import org.commonmark.node.ListItem; -import org.commonmark.node.Node; -import org.commonmark.node.OrderedList; -import org.commonmark.node.Paragraph; -import org.commonmark.node.SoftLineBreak; -import org.commonmark.node.StrongEmphasis; -import org.commonmark.node.Text; -import org.commonmark.node.ThematicBreak; -import ru.noties.debug.Debug; +import java.util.HashMap; +import java.util.Map; + import ru.noties.markwon.Markwon; +import ru.noties.markwon.ext.tables.Table; import ru.noties.markwon.recycler.MarkwonAdapter; import ru.noties.markwon.sample.extension.R; // do not use in real applications, this is just a showcase public class TableNodeEntry implements MarkwonAdapter.Entry { + private final Map cache = new HashMap<>(2); + @NonNull @Override public TableNodeHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { @@ -52,77 +30,23 @@ public class TableNodeEntry implements MarkwonAdapter.Entry \ No newline at end of file + android:textColor="#000" + android:textSize="16sp" /> \ No newline at end of file