diff --git a/docs/.vuepress/.artifacts.js b/docs/.vuepress/.artifacts.js
index 532f38d9..2a1c3f43 100644
--- a/docs/.vuepress/.artifacts.js
+++ b/docs/.vuepress/.artifacts.js
@@ -1,4 +1,4 @@
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
-const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
+const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"ru.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
export { artifacts };
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 29ab8971..85ebf762 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -60,6 +60,7 @@ module.exports = {
'/docs/v3/image/okhttp.md',
'/docs/v3/image/svg.md',
'/docs/v3/recycler/',
+ '/docs/v3/recycler-table/',
'/docs/v3/syntax-highlight/',
'/docs/v3/migration-2-3.md'
],
diff --git a/docs/.vuepress/public/assets/recycler-table-screenshot.png b/docs/.vuepress/public/assets/recycler-table-screenshot.png
new file mode 100644
index 00000000..f609c65f
Binary files /dev/null and b/docs/.vuepress/public/assets/recycler-table-screenshot.png differ
diff --git a/docs/docs/v3/ext-tables/README.md b/docs/docs/v3/ext-tables/README.md
index a728645c..fe149ed2 100644
--- a/docs/docs/v3/ext-tables/README.md
+++ b/docs/docs/v3/ext-tables/README.md
@@ -54,8 +54,11 @@ final Table table = Table.parse(Markwon, TableBlock);
myTableWidget.setTable(table);
```
-Unfortunately Markwon does not provide a widget that can be used for tables. But it does
-provide API that can be used to achieve desired result.
+:::tip
+To take advantage of this functionality and render tables without limitations (including
+horizontally scrollable layout when its contents exceed screen width), refer to [recycler-table](/docs/v3/recycler-table)
+module documentation that adds support for rendering `TableBlock` markdown node inside Android-native `TableLayout` widget.
+:::
## Theme
diff --git a/docs/docs/v3/ext-tasklist/README.md b/docs/docs/v3/ext-tasklist/README.md
index 7dde3d9a..3fb332f4 100644
--- a/docs/docs/v3/ext-tasklist/README.md
+++ b/docs/docs/v3/ext-tasklist/README.md
@@ -7,4 +7,140 @@ Adds support for GFM (Github-flavored markdown) task-lists:
```java
Markwon.builder(context)
.usePlugin(TaskListPlugin.create(context));
+```
+
+---
+
+Create a default instance of `TaskListPlugin` with `TaskListDrawable` initialized to use
+`android.R.attr.textColorLink` as primary color and `android.R.attr.colorBackground` as background
+```java
+TaskListPlugin.create(context);
+```
+
+---
+
+Create an instance of `TaskListPlugin` with exact color values to use:
+```java
+// obtain color values
+final int checkedFillColor = /* */;
+final int normalOutlineColor = /* */;
+final int checkMarkColor = /* */;
+
+TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor);
+```
+
+---
+
+Specify own drawable for a task list item:
+
+```java
+// obtain drawable
+final Drawable drawable = /* */;
+
+TaskListPlugin.create(drawable);
+```
+
+:::warning
+Please note that custom drawable for a task list item must correctly handle state
+in order to display done/not-done:
+
+```java
+public class MyTaskListDrawable extends Drawable {
+
+ private boolean isChecked;
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ // draw accordingly to the isChecked value
+ }
+
+ /* implementation omitted */
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ final boolean isChecked = contains(state, android.R.attr.state_checked);
+ final boolean result = this.isChecked != isChecked;
+ if (result) {
+ this.isChecked = isChecked;
+ }
+ return result;
+ }
+
+ private static boolean contains(@Nullable int[] states, int value) {
+ if (states != null) {
+ for (int state : states) {
+ if (state == value) {
+ // NB return here
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
+```
+:::
+
+## Task list mutation
+
+It is possible to mutate task list item state (toggle done/not-done). But note
+that `Markwon` won't handle state change internally by any means and this change
+is merely a visual one. If you need to persist state of a task list
+item change you have to implement it yourself. This should get your started:
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(TaskListPlugin.create(context))
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+
+ // obtain original SpanFactory set by TaskListPlugin
+ final SpanFactory origin = builder.getFactory(TaskListItem.class);
+ if (origin == null) {
+ // or throw, as it's a bit weird state and we expect
+ // this factory to be present
+ return;
+ }
+
+ builder.setFactory(TaskListItem.class, new SpanFactory() {
+ @Override
+ public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
+ // it's a bit non-secure behavior and we should validate
+ // the type of returned span first, but for the sake of brevity
+ // we skip this step
+ final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props);
+
+ if (span == null) {
+ // or throw
+ return null;
+ }
+
+ // return an array of spans
+ return new Object[]{
+ span,
+ new ClickableSpan() {
+ @Override
+ public void onClick(@NonNull View widget) {
+ // toggle VISUAL state
+ span.setDone(!span.isDone());
+
+ // do not forget to invalidate widget
+ widget.invalidate();
+
+ // execute your persistence logic
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint ds) {
+ // no-op, so appearance is not changed (otherwise
+ // task list item will look like a link)
+ }
+ }
+ };
+ }
+ });
+ }
+ })
+ .build();
```
\ No newline at end of file
diff --git a/docs/docs/v3/install.md b/docs/docs/v3/install.md
index 93b6de88..7f7184b7 100644
--- a/docs/docs/v3/install.md
+++ b/docs/docs/v3/install.md
@@ -10,62 +10,6 @@ next: /docs/v3/core/getting-started.md
-# Bundle
-If you wish to include all Markwon artifacts or add specific artifacts
-in a different manner than explicit gradle dependency definition, you can
-use `markwon-bundle.gradle` gradle script:
-
-*(in your `build.gradle`)*
-```groovy
-apply plugin: 'com.android.application'
-apply from: 'https://raw.githubusercontent.com/noties/Markwon/master/markwon-bundle.gradle'
-
-android { /* */ }
-
-ext.markwon = [
- 'version': '3.0.0',
- 'includeAll': true
-]
-
-dependencies { /* */ }
-```
-
-`markwon` object can have these properties:
-* `version` - (required) version of `Markwon`
-* `includeAll` - if _true_ will add all known Markwon artifacts. Can be used with `exclude`
-* * `exclude` - an array of artifacts to _exclude_ (cannot exclude `core`)
-* `artifacts` - an array of artifacts (can omit `core`, as it will be added implicitly anyway)
-
-If `includeAll` property is present and is `true`, then `artifacts` property won't be used.
-If there is no `includeAll` property or if it is `false`, `exclude` property won't be used.
-
-These 2 markwon objects are equal:
-
-```groovy
-// #1
-ext.markwon = [
- 'version': '3.0.0',
- 'artifacts': [
- 'ext-latex',
- 'ext-strikethrough',
- 'ext-tables',
- 'ext-tasklist',
- 'html',
- 'image-gif',
- 'image-okhttp',
- 'image-svg',
- 'recycler',
- 'syntax-highlight'
- ]
-]
-
-// #2
-ext.markwon = [
- 'version': '3.0.0',
- 'includeAll': true
-]
-```
-
## Snapshot
In order to use latest `SNAPSHOT` version add snapshot repository
diff --git a/docs/docs/v3/recycler-table/README.md b/docs/docs/v3/recycler-table/README.md
new file mode 100644
index 00000000..0cd90583
--- /dev/null
+++ b/docs/docs/v3/recycler-table/README.md
@@ -0,0 +1,90 @@
+# Recycler Table
+
+Artifact that provides [MarkwonAdapter.Entry](/docs/v3/recycler/) to render `TableBlock` inside
+Android-native `TableLayout` widget.
+
+
+
+* It's possible to wrap `TableLayout` inside a `HorizontalScrollView` to include all table content
+
+---
+
+Register instance of `TableEntry` with `MarkwonAdapter` to render TableBlocks:
+```java
+final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text)
+ .include(TableBlock.class, TableEntry.create(builder -> builder
+ .tableLayout(R.layout.adapter_table_block, R.id.table_layout)
+ .textLayoutIsRoot(R.layout.view_table_entry_cell)))
+ .build();
+```
+
+`TableEntry` requires at least 2 arguments:
+* `tableLayout` - layout with `TableLayout` inside
+* `textLayout` - layout with `TextView` inside (represents independent table cell)
+
+In case when required view is the root of layout specific builder methods can be used:
+* `tableLayoutIsRoot(int)`
+* `textLayoutIsRoot(int)`
+
+If your layouts have different structure (for example wrap a `TableView` inside a `HorizontalScrollView`)
+then you should use methods that accept ID of required view inside layout:
+* `tableLayout(int, int)`
+* `textLayout(int, int)`
+
+---
+
+To display `TableBlock` as a `TableLayout` specific `MarkwonPlugin` must be used: `TableEntryPlugin`.
+
+:::warning
+Do not use `TablePlugin` if you wish to display markdown tables via `TableEntry`. Use **TableEntryPlugin** instead
+:::
+
+`TableEntryPlugin` can reuse existing `TablePlugin` to make appearance of tables the same in both contexts:
+when rendering _natively_ in a TextView and when rendering in RecyclerView with TableEntry.
+
+* `TableEntryPlugin.create(Context)` - creates plugin with default `TableTheme`
+* `TableEntryPlugin.create(TableTheme)` - creates plugin with provided `TableTheme`
+* `TableEntryPlugin.create(TablePlugin.ThemeConfigure)` - creates plugin with theme configured by `ThemeConfigure`
+* `TableEntryPlugin.create(TablePlugin)` - creates plugin with `TableTheme` used in provided `TablePlugin`
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(TableEntryPlugin.create(context))
+ // other plugins
+ .build();
+```
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(TableEntryPlugin.create(builder -> builder
+ .tableBorderWidth(0)
+ .tableHeaderRowBackgroundColor(Color.RED)))
+ // other plugins
+ .build();
+```
+
+## Table with scrollable content
+
+To stretch table columns to fit the width of screen or to make table scrollable when content exceeds screen width
+this layout can be used:
+
+```xml
+
+
+
+
+
+```
\ No newline at end of file
diff --git a/markwon-bundle.gradle b/markwon-bundle.gradle
deleted file mode 100644
index 603404d6..00000000
--- a/markwon-bundle.gradle
+++ /dev/null
@@ -1,59 +0,0 @@
-// await project initialization and check for markwon object then
-// (so we do not have to force users to put `apply from` block at the bottom
-// of a build.gradle file)
-project.afterEvaluate {
-
- if (!project.hasProperty('markwon')) {
- throw new RuntimeException("No `markwon` property object is found. " +
- "Define it with `ext.markwon = [prop: value]`")
- }
-
- final def markwon = project.markwon
- if (!(markwon instanceof Map)) {
- throw new RuntimeException("`markwon` object property must be of type Map. " +
- "Groovy short-hand to define: `[:]`.")
- }
-
- final def version = markwon.version
- final def includeAll = markwon.computeIfAbsent('includeAll', { false })
- final def artifacts
- if (includeAll) {
-
- // cannot exclude core
- final def exclude = markwon.computeIfAbsent('exclude', { [] }) \
- .unique() \
- .findAll { 'core' != it }
-
- artifacts = [
- 'core',
- 'ext-latex',
- 'ext-strikethrough',
- 'ext-tables',
- 'ext-tasklist',
- 'html',
- 'image-gif',
- 'image-okhttp',
- 'image-svg',
- 'recycler',
- 'syntax-highlight'
- ].findAll { !exclude.contains(it) }
-
- } else {
- artifacts = (markwon.containsKey('artifacts') ? markwon.artifacts : ['core']).with {
- // add implicit core artifact
- if (!it.contains('core')) {
- it.add('core')
- }
- return it
- }
- }
-
- if (!version) {
- throw new RuntimeException("Please specify version of Markwon, for example: " +
- "`ext.markwon = [ 'version': '1.0.0']`")
- }
-
- artifacts.forEach {
- project.dependencies.add('implementation', "ru.noties.markwon:$it:$version")
- }
-}
\ No newline at end of file
diff --git a/markwon-core/src/main/java/ru/noties/markwon/Markwon.java b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java
index 6e7e1ba3..0ac110ff 100644
--- a/markwon-core/src/main/java/ru/noties/markwon/Markwon.java
+++ b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java
@@ -2,6 +2,7 @@ package ru.noties.markwon;
import android.content.Context;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.text.Spanned;
import android.widget.TextView;
@@ -101,6 +102,9 @@ public abstract class Markwon {
*/
public abstract boolean hasPlugin(@NonNull Class extends MarkwonPlugin> plugin);
+ @Nullable
+ public abstract
P getPlugin(@NonNull Class
type);
+
/**
* Builder for {@link Markwon}.
*
diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java
index a51bd62a..e90b7898 100644
--- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java
+++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java
@@ -1,6 +1,7 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.text.Spanned;
import android.widget.TextView;
@@ -91,13 +92,19 @@ class MarkwonImpl extends Markwon {
@Override
public boolean hasPlugin(@NonNull Class extends MarkwonPlugin> type) {
- boolean result = false;
+ return getPlugin(type) != null;
+ }
+
+ @Nullable
+ @Override
+ public
P getPlugin(@NonNull Class
type) {
+ MarkwonPlugin out = null;
for (MarkwonPlugin plugin : plugins) {
if (type.isAssignableFrom(plugin.getClass())) {
- result = true;
- break;
+ out = plugin;
}
}
- return result;
+ //noinspection unchecked
+ return (P) out;
}
}
diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java
index 251cf1fe..a139e3f1 100644
--- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java
+++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java
@@ -13,6 +13,10 @@ import java.util.List;
*/
public abstract class MarkwonReducer {
+ /**
+ * @return direct children of supplied Node. In the most usual case
+ * will return all BlockNodes of a Document
+ */
@NonNull
public static MarkwonReducer directChildren() {
return new DirectChildren();
diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java
index dbc6960d..8cf25a28 100644
--- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java
+++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java
@@ -34,6 +34,12 @@ public interface MarkwonSpansFactory {
@NonNull
Builder setFactory(@NonNull Class node, @Nullable SpanFactory factory);
+ /**
+ * Can be useful when enhancing an already defined SpanFactory with another one.
+ */
+ @Nullable
+ SpanFactory getFactory(@NonNull Class node);
+
@NonNull
MarkwonSpansFactory build();
}
diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java
index f3cd0dee..ef1906d8 100644
--- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java
+++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java
@@ -52,6 +52,12 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory {
return this;
}
+ @Nullable
+ @Override
+ public SpanFactory getFactory(@NonNull Class node) {
+ return factories.get(node);
+ }
+
@NonNull
@Override
public MarkwonSpansFactory build() {
diff --git a/markwon-core/src/main/java/ru/noties/markwon/utils/NoCopySpannableFactory.java b/markwon-core/src/main/java/ru/noties/markwon/utils/NoCopySpannableFactory.java
new file mode 100644
index 00000000..f5eedc2a
--- /dev/null
+++ b/markwon-core/src/main/java/ru/noties/markwon/utils/NoCopySpannableFactory.java
@@ -0,0 +1,30 @@
+package ru.noties.markwon.utils;
+
+import android.support.annotation.NonNull;
+import android.text.Spannable;
+import android.text.SpannableString;
+
+/**
+ * Utility SpannableFactory that re-uses Spannable instance between multiple
+ * `TextView#setText` calls.
+ *
+ * @since 3.0.0
+ */
+public class NoCopySpannableFactory extends Spannable.Factory {
+
+ @NonNull
+ public static NoCopySpannableFactory getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ @Override
+ public Spannable newSpannable(CharSequence source) {
+ return source instanceof Spannable
+ ? (Spannable) source
+ : new SpannableString(source);
+ }
+
+ static class Holder {
+ private static final NoCopySpannableFactory INSTANCE = new NoCopySpannableFactory();
+ }
+}
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 ece065bf..f05a84fc 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
@@ -54,13 +54,20 @@ public class TablePlugin extends AbstractMarkwonPlugin {
return new TablePlugin(builder.build());
}
+ private final TableTheme theme;
private final TableVisitor visitor;
@SuppressWarnings("WeakerAccess")
TablePlugin(@NonNull TableTheme tableTheme) {
+ this.theme = tableTheme;
this.visitor = new TableVisitor(tableTheme);
}
+ @NonNull
+ public TableTheme theme() {
+ return theme;
+ }
+
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.extensions(Collections.singleton(TablesExtension.create()));
diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java
index 6752e729..e9b1bd47 100644
--- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java
+++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java
@@ -13,15 +13,19 @@ public class TableTheme {
@NonNull
public static TableTheme create(@NonNull Context context) {
- final Dip dip = Dip.create(context);
- return builder()
- .tableCellPadding(dip.toPx(4))
- .tableBorderWidth(dip.toPx(1))
- .build();
+ return buildWithDefaults(context).build();
}
@NonNull
- public static Builder builder() {
+ public static Builder buildWithDefaults(@NonNull Context context) {
+ final Dip dip = Dip.create(context);
+ return emptyBuilder()
+ .tableCellPadding(dip.toPx(4))
+ .tableBorderWidth(dip.toPx(1));
+ }
+
+ @NonNull
+ public static Builder emptyBuilder() {
return new Builder();
}
@@ -58,6 +62,20 @@ public class TableTheme {
this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor;
}
+ /**
+ * @since 3.0.0
+ */
+ @NonNull
+ public Builder asBuilder() {
+ return new Builder()
+ .tableCellPadding(tableCellPadding)
+ .tableBorderColor(tableBorderColor)
+ .tableBorderWidth(tableBorderWidth)
+ .tableOddRowBackgroundColor(tableOddRowBackgroundColor)
+ .tableEvenRowBackgroundColor(tableEvenRowBackgroundColor)
+ .tableHeaderRowBackgroundColor(tableHeaderRowBackgroundColor);
+ }
+
public int tableCellPadding() {
return tableCellPadding;
}
diff --git a/markwon-recycler-table/build.gradle b/markwon-recycler-table/build.gradle
new file mode 100644
index 00000000..841c2b60
--- /dev/null
+++ b/markwon-recycler-table/build.gradle
@@ -0,0 +1,29 @@
+apply plugin: 'com.android.library'
+
+android {
+
+ compileSdkVersion config['compile-sdk']
+ buildToolsVersion config['build-tools']
+
+ defaultConfig {
+ minSdkVersion config['min-sdk']
+ targetSdkVersion config['target-sdk']
+ versionCode 1
+ versionName version
+ }
+}
+
+dependencies {
+
+ api project(':markwon-core')
+ api project(':markwon-recycler')
+ api project(':markwon-ext-tables')
+
+ deps['test'].with {
+ testImplementation it['junit']
+ testImplementation it['robolectric']
+ testImplementation it['mockito']
+ }
+}
+
+registerArtifact(this)
\ No newline at end of file
diff --git a/markwon-recycler-table/gradle.properties b/markwon-recycler-table/gradle.properties
new file mode 100644
index 00000000..8e87be51
--- /dev/null
+++ b/markwon-recycler-table/gradle.properties
@@ -0,0 +1,4 @@
+POM_NAME=Recycler Table
+POM_ARTIFACT_ID=recycler-table
+POM_DESCRIPTION=Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget
+POM_PACKAGING=aar
\ No newline at end of file
diff --git a/markwon-recycler-table/src/main/AndroidManifest.xml b/markwon-recycler-table/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..b215e1f6
--- /dev/null
+++ b/markwon-recycler-table/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableBorderDrawable.java b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableBorderDrawable.java
new file mode 100644
index 00000000..06a89f24
--- /dev/null
+++ b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableBorderDrawable.java
@@ -0,0 +1,48 @@
+package ru.noties.markwon.recycler.table;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.Px;
+
+class TableBorderDrawable extends Drawable {
+
+ private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ TableBorderDrawable() {
+ paint.setStyle(Paint.Style.STROKE);
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ if (paint.getStrokeWidth() > 0) {
+ canvas.drawRect(getBounds(), paint);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // no op
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ // no op
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+
+ void update(@Px int borderWidth, @ColorInt int color) {
+ paint.setStrokeWidth(borderWidth);
+ paint.setColor(color);
+ invalidateSelf();
+ }
+}
diff --git a/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntry.java b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntry.java
new file mode 100644
index 00000000..5ff8b25e
--- /dev/null
+++ b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntry.java
@@ -0,0 +1,530 @@
+package ru.noties.markwon.recycler.table;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IdRes;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Px;
+import android.support.annotation.VisibleForTesting;
+import android.view.Gravity;
+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 java.util.HashMap;
+import java.util.List;
+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.utils.NoCopySpannableFactory;
+
+/**
+ * @since 3.0.0
+ */
+public class TableEntry extends MarkwonAdapter.Entry {
+
+ public interface Builder {
+
+ /**
+ * @param tableLayoutResId layout with TableLayout
+ * @param tableIdRes id of the TableLayout inside specified layout
+ * @see #tableLayoutIsRoot(int)
+ */
+ @NonNull
+ Builder tableLayout(@LayoutRes int tableLayoutResId, @IdRes int tableIdRes);
+
+ /**
+ * @param tableLayoutResId layout with TableLayout as the root view
+ * @see #tableLayout(int, int)
+ */
+ @NonNull
+ Builder tableLayoutIsRoot(@LayoutRes int tableLayoutResId);
+
+ /**
+ * @param textLayoutResId layout with TextView
+ * @param textIdRes id of the TextView inside specified layout
+ * @see #textLayoutIsRoot(int)
+ */
+ @NonNull
+ Builder textLayout(@LayoutRes int textLayoutResId, @IdRes int textIdRes);
+
+ /**
+ * @param textLayoutResId layout with TextView as the root view
+ * @see #textLayout(int, int)
+ */
+ @NonNull
+ Builder textLayoutIsRoot(@LayoutRes int textLayoutResId);
+
+ /**
+ * @param cellTextCenterVertical if text inside a table cell should centered
+ * vertically (by default `true`)
+ */
+ @NonNull
+ Builder cellTextCenterVertical(boolean cellTextCenterVertical);
+
+ /**
+ * @param isRecyclable flag to set on RecyclerView.ViewHolder (by default `true`)
+ */
+ @NonNull
+ Builder isRecyclable(boolean isRecyclable);
+
+ @NonNull
+ TableEntry build();
+ }
+
+ public interface BuilderConfigure {
+ void configure(@NonNull Builder builder);
+ }
+
+ @NonNull
+ public static Builder builder() {
+ return new BuilderImpl();
+ }
+
+ @NonNull
+ public static TableEntry create(@NonNull BuilderConfigure configure) {
+ final Builder builder = builder();
+ configure.configure(builder);
+ return builder.build();
+ }
+
+ private final int tableLayoutResId;
+ private final int tableIdRes;
+
+ private final int textLayoutResId;
+ private final int textIdRes;
+
+ private final boolean isRecyclable;
+ private final boolean cellTextCenterVertical; // by default true
+
+ private LayoutInflater inflater;
+
+ private final Map map = new HashMap<>(3);
+
+ TableEntry(
+ @LayoutRes int tableLayoutResId,
+ @IdRes int tableIdRes,
+ @LayoutRes int textLayoutResId,
+ @IdRes int textIdRes,
+ boolean isRecyclable,
+ boolean cellTextCenterVertical) {
+ this.tableLayoutResId = tableLayoutResId;
+ this.tableIdRes = tableIdRes;
+ this.textLayoutResId = textLayoutResId;
+ this.textIdRes = textIdRes;
+ this.isRecyclable = isRecyclable;
+ this.cellTextCenterVertical = cellTextCenterVertical;
+ }
+
+ @NonNull
+ @Override
+ public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
+ return new Holder(
+ isRecyclable,
+ tableIdRes,
+ inflater.inflate(tableLayoutResId, parent, false));
+ }
+
+ @Override
+ public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull TableBlock node) {
+
+ Table table = map.get(node);
+ if (table == null) {
+ table = Table.parse(markwon, node);
+ map.put(node, table);
+ }
+
+ // check if this exact TableBlock was already applied
+ // set tag of tableLayoutResId as it's 100% to be present (we still allow 0 as
+ // tableIdRes if tableLayoutResId has TableLayout as root view)
+ final TableLayout layout = holder.tableLayout;
+ if (table == null
+ || table == layout.getTag(tableLayoutResId)) {
+ return;
+ }
+
+ // set this flag to indicate what table instance we current display
+ layout.setTag(tableLayoutResId, table);
+
+ final TableEntryPlugin plugin = markwon.getPlugin(TableEntryPlugin.class);
+ if (plugin == null) {
+ throw new IllegalStateException("No TableEntryPlugin is found. Make sure that it " +
+ "is _used_ whilst configuring Markwon instance");
+ }
+
+ // we must remove unwanted ones (rows and columns)
+
+ final TableEntryTheme theme = plugin.theme();
+ final int borderWidth;
+ final int borderColor;
+ final int cellPadding;
+ {
+ final TextView textView = ensureTextView(layout, 0, 0);
+ borderWidth = theme.tableBorderWidth(textView.getPaint());
+ borderColor = theme.tableBorderColor(textView.getPaint());
+ cellPadding = theme.tableCellPadding();
+ }
+
+ ensureTableBorderBackground(layout, borderWidth, borderColor);
+
+ //noinspection SuspiciousNameCombination
+// layout.setPadding(borderWidth, borderWidth, borderWidth, borderWidth);
+// layout.setClipToPadding(borderWidth == 0);
+
+ final List rows = table.rows();
+
+ final int rowsSize = rows.size();
+
+ // all rows should have equal number of columns
+ final int columnsSize = rowsSize > 0
+ ? rows.get(0).columns().size()
+ : 0;
+
+ Table.Row row;
+ Table.Column column;
+
+ TableRow tableRow;
+
+ for (int y = 0; y < rowsSize; y++) {
+
+ row = rows.get(y);
+ tableRow = ensureRow(layout, y);
+
+ for (int x = 0; x < columnsSize; x++) {
+
+ column = row.columns().get(x);
+
+ final TextView textView = ensureTextView(layout, y, x);
+ textView.setGravity(textGravity(column.alignment(), cellTextCenterVertical));
+ textView.getPaint().setFakeBoldText(row.header());
+
+ // apply padding only if not specified in theme (otherwise just use the value from layout)
+ if (cellPadding > 0) {
+ textView.setPadding(cellPadding, cellPadding, cellPadding, cellPadding);
+ }
+
+ ensureTableBorderBackground(textView, borderWidth, borderColor);
+ markwon.setParsedMarkdown(textView, column.content());
+ }
+
+ // row appearance
+ if (row.header()) {
+ tableRow.setBackgroundColor(theme.tableHeaderRowBackgroundColor());
+ } else {
+ // as we currently have no support for tables without head
+ // we shift even/odd calculation a bit (head should not be included in even/odd calculation)
+ final boolean isEven = (y % 2) == 1;
+ if (isEven) {
+ tableRow.setBackgroundColor(theme.tableEvenRowBackgroundColor());
+ } else {
+ // just take first
+ final TextView textView = ensureTextView(layout, y, 0);
+ tableRow.setBackgroundColor(
+ theme.tableOddRowBackgroundColor(textView.getPaint()));
+ }
+ }
+ }
+
+ // clean up here of un-used rows and columns
+ removeUnused(layout, rowsSize, columnsSize);
+ }
+
+ @NonNull
+ private TableRow ensureRow(@NonNull TableLayout layout, int row) {
+
+ final int count = layout.getChildCount();
+
+ // fill the requested views until we have added the `row` one
+ if (row >= count) {
+
+ final Context context = layout.getContext();
+
+ int diff = row - count + 1;
+ while (diff > 0) {
+ layout.addView(new TableRow(context));
+ diff -= 1;
+ }
+ }
+
+ // return requested child (here it always should be the last one)
+ return (TableRow) layout.getChildAt(row);
+ }
+
+ @NonNull
+ private TextView ensureTextView(@NonNull TableLayout layout, int row, int column) {
+
+ final TableRow tableRow = ensureRow(layout, row);
+ final int count = tableRow.getChildCount();
+
+ if (column >= count) {
+
+ final LayoutInflater inflater = ensureInflater(layout.getContext());
+
+ boolean textViewChecked = false;
+
+ View view;
+ TextView textView;
+ ViewGroup.LayoutParams layoutParams;
+
+ int diff = column - count + 1;
+
+ while (diff > 0) {
+
+ view = inflater.inflate(textLayoutResId, tableRow, false);
+
+ // we should have `match_parent` as height (important for borders and text-vertical-align)
+ layoutParams = view.getLayoutParams();
+ if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
+ layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ }
+
+ // it will be enough to check only once
+ if (!textViewChecked) {
+
+ if (textIdRes == 0) {
+ if (!(view instanceof TextView)) {
+ final String name = layout.getContext().getResources().getResourceName(textLayoutResId);
+ throw new IllegalStateException(String.format("textLayoutResId(R.layout.%s) " +
+ "has other than TextView root view. Specify TextView ID explicitly", name));
+ }
+ textView = (TextView) view;
+ } else {
+ textView = view.findViewById(textIdRes);
+ if (textView == null) {
+ final Resources r = layout.getContext().getResources();
+ final String layoutName = r.getResourceName(textLayoutResId);
+ final String idName = r.getResourceName(textIdRes);
+ throw new NullPointerException(String.format("textLayoutResId(R.layout.%s) " +
+ "has no TextView found by id(R.id.%s): %s", layoutName, idName, view));
+ }
+ }
+ // mark as checked
+ textViewChecked = true;
+ } else {
+ if (textIdRes == 0) {
+ textView = (TextView) view;
+ } else {
+ textView = view.findViewById(textIdRes);
+ }
+ }
+
+ // we should set SpannableFactory during creation (to avoid another setText method)
+ textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
+ tableRow.addView(textView);
+
+ diff -= 1;
+ }
+ }
+
+ // we can skip all the validation here as we have validated our views whilst inflating them
+ final View last = tableRow.getChildAt(column);
+ if (textIdRes == 0) {
+ return (TextView) last;
+ } else {
+ return last.findViewById(textIdRes);
+ }
+ }
+
+ private void ensureTableBorderBackground(@NonNull View view, @Px int borderWidth, @ColorInt int borderColor) {
+ if (borderWidth == 0) {
+ view.setBackground(null);
+ } else {
+ final Drawable drawable = view.getBackground();
+ if (!(drawable instanceof TableBorderDrawable)) {
+ final TableBorderDrawable borderDrawable = new TableBorderDrawable();
+ borderDrawable.update(borderWidth, borderColor);
+ view.setBackground(borderDrawable);
+ } else {
+ ((TableBorderDrawable) drawable).update(borderWidth, borderColor);
+ }
+ }
+ }
+
+ @NonNull
+ private LayoutInflater ensureInflater(@NonNull Context context) {
+ if (inflater == null) {
+ inflater = LayoutInflater.from(context);
+ }
+ return inflater;
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ static void removeUnused(@NonNull TableLayout layout, int usedRows, int usedColumns) {
+
+ // clean up rows
+ final int rowsCount = layout.getChildCount();
+ if (rowsCount > usedRows) {
+ layout.removeViews(usedRows, (rowsCount - usedRows));
+ }
+
+ // validate columns
+ // here we can use usedRows as children count
+
+ TableRow tableRow;
+ int columnCount;
+
+ for (int i = 0; i < usedRows; i++) {
+ tableRow = (TableRow) layout.getChildAt(i);
+ columnCount = tableRow.getChildCount();
+ if (columnCount > usedColumns) {
+ tableRow.removeViews(usedColumns, (columnCount - usedColumns));
+ }
+ }
+ }
+
+ @Override
+ public void clear() {
+ map.clear();
+ }
+
+ public static class Holder extends MarkwonAdapter.Holder {
+
+ final TableLayout tableLayout;
+
+ public Holder(boolean isRecyclable, @IdRes int tableLayoutIdRes, @NonNull View itemView) {
+ super(itemView);
+
+ // we must call this method only once (it's somehow _paired_ inside, so
+ // any call in `onCreateViewHolder` or `onBindViewHolder` will log an error
+ // `isRecyclable decremented below 0` which make little sense here)
+ setIsRecyclable(isRecyclable);
+
+ final TableLayout tableLayout;
+ if (tableLayoutIdRes == 0) {
+ // try to cast directly
+ if (!(itemView instanceof TableLayout)) {
+ throw new IllegalStateException("Root view is not TableLayout. Please provide " +
+ "TableLayout ID explicitly");
+ }
+ tableLayout = (TableLayout) itemView;
+ } else {
+ tableLayout = requireView(tableLayoutIdRes);
+ }
+ this.tableLayout = tableLayout;
+ }
+ }
+
+ // we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17)
+ @SuppressWarnings("WeakerAccess")
+ @SuppressLint("RtlHardcoded")
+ @VisibleForTesting
+ static int textGravity(@NonNull Table.Alignment alignment, boolean cellTextCenterVertical) {
+
+ final int gravity;
+
+ switch (alignment) {
+
+ case LEFT:
+ gravity = Gravity.LEFT;
+ break;
+
+ case CENTER:
+ gravity = Gravity.CENTER_HORIZONTAL;
+ break;
+
+ case RIGHT:
+ gravity = Gravity.RIGHT;
+ break;
+
+ default:
+ throw new IllegalStateException("Unknown table alignment: " + alignment);
+ }
+
+ if (cellTextCenterVertical) {
+ return gravity | Gravity.CENTER_VERTICAL;
+ }
+
+ // do not center vertically
+ return gravity;
+ }
+
+ static class BuilderImpl implements Builder {
+
+ private int tableLayoutResId;
+ private int tableIdRes;
+
+ private int textLayoutResId;
+ private int textIdRes;
+
+ private boolean cellTextCenterVertical = true;
+
+ private boolean isRecyclable = true;
+
+ @NonNull
+ @Override
+ public Builder tableLayout(int tableLayoutResId, int tableIdRes) {
+ this.tableLayoutResId = tableLayoutResId;
+ this.tableIdRes = tableIdRes;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Builder tableLayoutIsRoot(int tableLayoutResId) {
+ this.tableLayoutResId = tableLayoutResId;
+ this.tableIdRes = 0;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Builder textLayout(int textLayoutResId, int textIdRes) {
+ this.textLayoutResId = textLayoutResId;
+ this.textIdRes = textIdRes;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Builder textLayoutIsRoot(int textLayoutResId) {
+ this.textLayoutResId = textLayoutResId;
+ this.textIdRes = 0;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Builder cellTextCenterVertical(boolean cellTextCenterVertical) {
+ this.cellTextCenterVertical = cellTextCenterVertical;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Builder isRecyclable(boolean isRecyclable) {
+ this.isRecyclable = isRecyclable;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public TableEntry build() {
+
+ if (tableLayoutResId == 0) {
+ throw new IllegalStateException("`tableLayoutResId` argument is required");
+ }
+
+ if (textLayoutResId == 0) {
+ throw new IllegalStateException("`textLayoutResId` argument is required");
+ }
+
+ return new TableEntry(
+ tableLayoutResId, tableIdRes,
+ textLayoutResId, textIdRes,
+ isRecyclable, cellTextCenterVertical
+ );
+ }
+ }
+}
diff --git a/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryPlugin.java b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryPlugin.java
new file mode 100644
index 00000000..a92400ee
--- /dev/null
+++ b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryPlugin.java
@@ -0,0 +1,65 @@
+package ru.noties.markwon.recycler.table;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.commonmark.ext.gfm.tables.TablesExtension;
+import org.commonmark.parser.Parser;
+
+import java.util.Collections;
+
+import ru.noties.markwon.AbstractMarkwonPlugin;
+import ru.noties.markwon.ext.tables.TablePlugin;
+import ru.noties.markwon.ext.tables.TableTheme;
+
+/**
+ * This plugin must be used instead of {@link ru.noties.markwon.ext.tables.TablePlugin} when a markdown
+ * table is intended to be used in a RecyclerView via {@link TableEntry}. This is required
+ * because TablePlugin additionally processes markdown tables to be displayed in limited
+ * context of a TextView. If TablePlugin will be used, {@link TableEntry} will display table,
+ * but no content will be present
+ *
+ * @since 3.0.0
+ */
+public class TableEntryPlugin extends AbstractMarkwonPlugin {
+
+ @NonNull
+ public static TableEntryPlugin create(@NonNull Context context) {
+ final TableTheme tableTheme = TableTheme.create(context);
+ return create(tableTheme);
+ }
+
+ @NonNull
+ public static TableEntryPlugin create(@NonNull TableTheme tableTheme) {
+ return new TableEntryPlugin(TableEntryTheme.create(tableTheme));
+ }
+
+ @NonNull
+ public static TableEntryPlugin create(@NonNull TablePlugin.ThemeConfigure themeConfigure) {
+ final TableTheme.Builder builder = new TableTheme.Builder();
+ themeConfigure.configureTheme(builder);
+ return new TableEntryPlugin(new TableEntryTheme(builder));
+ }
+
+ @NonNull
+ public static TableEntryPlugin create(@NonNull TablePlugin plugin) {
+ return create(plugin.theme());
+ }
+
+ private final TableEntryTheme theme;
+
+ @SuppressWarnings("WeakerAccess")
+ TableEntryPlugin(@NonNull TableEntryTheme tableTheme) {
+ this.theme = tableTheme;
+ }
+
+ @NonNull
+ public TableEntryTheme theme() {
+ return theme;
+ }
+
+ @Override
+ public void configureParser(@NonNull Parser.Builder builder) {
+ builder.extensions(Collections.singleton(TablesExtension.create()));
+ }
+}
diff --git a/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryTheme.java b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryTheme.java
new file mode 100644
index 00000000..e9b9b7ca
--- /dev/null
+++ b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryTheme.java
@@ -0,0 +1,67 @@
+package ru.noties.markwon.recycler.table;
+
+import android.graphics.Paint;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Px;
+
+import ru.noties.markwon.ext.tables.TableTheme;
+import ru.noties.markwon.utils.ColorUtils;
+
+/**
+ * Mimics TableTheme to allow uniform table customization
+ *
+ * @see #create(TableTheme)
+ * @see TableEntryPlugin
+ * @since 3.0.0
+ */
+@SuppressWarnings("WeakerAccess")
+public class TableEntryTheme extends TableTheme {
+
+ @NonNull
+ public static TableEntryTheme create(@NonNull TableTheme tableTheme) {
+ return new TableEntryTheme(tableTheme.asBuilder());
+ }
+
+ protected TableEntryTheme(@NonNull Builder builder) {
+ super(builder);
+ }
+
+ @Px
+ @Override
+ public int tableCellPadding() {
+ return tableCellPadding;
+ }
+
+ @ColorInt
+ public int tableBorderColor(@NonNull Paint paint) {
+ return tableBorderColor == 0
+ ? ColorUtils.applyAlpha(paint.getColor(), TABLE_BORDER_DEF_ALPHA)
+ : tableBorderColor;
+ }
+
+ @Px
+ @Override
+ public int tableBorderWidth(@NonNull Paint paint) {
+ return tableBorderWidth < 0
+ ? (int) (paint.getStrokeWidth() + .5F)
+ : tableBorderWidth;
+ }
+
+ @ColorInt
+ public int tableOddRowBackgroundColor(@NonNull Paint paint) {
+ return tableOddRowBackgroundColor == 0
+ ? ColorUtils.applyAlpha(paint.getColor(), TABLE_ODD_ROW_DEF_ALPHA)
+ : tableOddRowBackgroundColor;
+ }
+
+ @ColorInt
+ public int tableEvenRowBackgroundColor() {
+ return tableEvenRowBackgroundColor;
+ }
+
+ @ColorInt
+ public int tableHeaderRowBackgroundColor() {
+ return tableHeaderRowBackgroundColor;
+ }
+}
diff --git a/markwon-recycler-table/src/test/java/ru/noties/markwon/recycler/table/TableEntryTest.java b/markwon-recycler-table/src/test/java/ru/noties/markwon/recycler/table/TableEntryTest.java
new file mode 100644
index 00000000..ae55f18e
--- /dev/null
+++ b/markwon-recycler-table/src/test/java/ru/noties/markwon/recycler/table/TableEntryTest.java
@@ -0,0 +1,103 @@
+package ru.noties.markwon.recycler.table;
+
+import android.content.res.Resources;
+import android.util.Pair;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.TableLayout;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+import java.util.List;
+
+import ru.noties.markwon.ext.tables.Table;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class TableEntryTest {
+
+ @Test
+ public void gravity() {
+ // test textGravity is calculated correctly
+
+ final List> noVerticalAlign = Arrays.asList(
+ new Pair(Table.Alignment.LEFT, Gravity.LEFT),
+ new Pair(Table.Alignment.CENTER, Gravity.CENTER_HORIZONTAL),
+ new Pair(Table.Alignment.RIGHT, Gravity.RIGHT)
+ );
+
+ final List> withVerticalAlign = Arrays.asList(
+ new Pair(Table.Alignment.LEFT, Gravity.LEFT | Gravity.CENTER_VERTICAL),
+ new Pair(Table.Alignment.CENTER, Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL),
+ new Pair(Table.Alignment.RIGHT, Gravity.RIGHT | Gravity.CENTER_VERTICAL)
+ );
+
+ for (Pair pair : noVerticalAlign) {
+ assertEquals(pair.first.name(), pair.second.intValue(), TableEntry.textGravity(pair.first, false));
+ }
+
+ for (Pair pair : withVerticalAlign) {
+ assertEquals(pair.first.name(), pair.second.intValue(), TableEntry.textGravity(pair.first, true));
+ }
+ }
+
+ @Test
+ public void holder_no_table_layout_id() {
+ // validate that holder correctly obtains TableLayout instance casting root view
+
+ // root is not TableLayout
+ try {
+ new TableEntry.Holder(false, 0, mock(View.class));
+ fail();
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage().contains("Root view is not TableLayout"));
+ }
+
+ // root is TableLayout
+ try {
+ final TableLayout tableLayout = mock(TableLayout.class);
+ final TableEntry.Holder h = new TableEntry.Holder(false, 0, tableLayout);
+ assertEquals(tableLayout, h.tableLayout);
+ } catch (IllegalStateException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ @Test
+ public void holder_with_table_layout_id() {
+
+ // not found
+ try {
+
+ final View view = mock(View.class);
+ // resources are used to obtain id name for proper error message
+ when(view.getResources()).thenReturn(mock(Resources.class));
+ new TableEntry.Holder(false, 1, view);
+ fail();
+ } catch (NullPointerException e) {
+ assertTrue(e.getMessage(), e.getMessage().contains("No view with id"));
+ }
+
+ // found
+ try {
+ final TableLayout tableLayout = mock(TableLayout.class);
+ final View view = mock(View.class);
+ when(view.findViewById(3)).thenReturn(tableLayout);
+ final TableEntry.Holder holder = new TableEntry.Holder(false, 3, view);
+ assertEquals(tableLayout, holder.tableLayout);
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+}
\ No newline at end of file
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 d288a44c..64241d55 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
@@ -18,17 +18,13 @@ import ru.noties.markwon.MarkwonReducer;
/**
* Adapter 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
+ * parsed markdown document (via {@link MarkwonReducer} 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 #builder(int, int)
+ * @see #builder(Entry)
+ * @see #create(int, int)
* @see #create(Entry)
* @see #setMarkdown(Markwon, String)
* @see #setParsedMarkdown(Markwon, Node)
@@ -37,14 +33,34 @@ import ru.noties.markwon.MarkwonReducer;
*/
public abstract class MarkwonAdapter extends RecyclerView.Adapter {
+ @NonNull
+ public static Builder builderTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
+ return builder(SimpleEntry.createTextViewIsRoot(defaultEntryLayoutResId));
+ }
+
/**
* Factory method to obtain {@link Builder} instance.
*
* @see Builder
*/
@NonNull
- public static Builder builder() {
- return new MarkwonAdapterImpl.BuilderImpl();
+ public static Builder builder(
+ @LayoutRes int defaultEntryLayoutResId,
+ @IdRes int defaultEntryTextViewResId
+ ) {
+ return builder(SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId));
+ }
+
+ @NonNull
+ public static Builder builder(@NonNull Entry extends Node, ? extends Holder> defaultEntry) {
+ //noinspection unchecked
+ return new MarkwonAdapterImpl.BuilderImpl((Entry) defaultEntry);
+ }
+
+ @NonNull
+ public static MarkwonAdapter createTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
+ return builderTextViewIsRoot(defaultEntryLayoutResId)
+ .build();
}
/**
@@ -52,12 +68,16 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter defaultEntry) {
- return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(defaultEntry).build();
- }
-
- /**
- * Factory method to create a {@link MarkwonAdapter} that will use supplied layoutResId view
- * to display all nodes.
- *
- * Please note that supplied layout must have a TextView inside
- * with {@code android:id="@+id/text"}
- *
- * @param layoutResId layout to be used to display all nodes
- * @see SimpleEntry
- */
- @NonNull
- public static MarkwonAdapter create(@LayoutRes int layoutResId) {
- return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(layoutResId).build();
+ return builder(defaultEntry).build();
}
/**
* Builder to create an instance of {@link MarkwonAdapter}
*
* @see #include(Class, Entry)
- * @see #defaultEntry(int)
- * @see #defaultEntry(Entry)
* @see #reducer(MarkwonReducer)
+ * @see #build()
*/
public interface Builder {
@@ -112,29 +116,6 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter node,
@NonNull Entry super N, ? extends Holder> entry);
- /**
- * Specify which {@link Entry} to use for all non-explicitly registered nodes
- *
- * @param defaultEntry {@link Entry}
- * @return self
- * @see SimpleEntry
- */
- @NonNull
- Builder defaultEntry(@NonNull Entry extends Node, ? extends Holder> defaultEntry);
-
- /**
- * Specify which layout {@link SimpleEntry} will use to render all non-explicitly
- * registered nodes.
- *
- * Please note that supplied layout must have a TextView inside
- * with {@code android:id="@+id/text"}
- *
- * @return self
- * @see SimpleEntry
- */
- @NonNull
- Builder defaultEntry(@LayoutRes int layoutResId);
-
/**
* Specify how root Node will be reduced to a list of nodes. There is a default
* {@link MarkwonReducer} that will be used if not provided explicitly (there is no need to
@@ -157,17 +138,27 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter {
+ public static abstract class Entry {
@NonNull
- H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);
+ public abstract H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);
- void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node);
+ public abstract void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node);
- long id(@NonNull N node);
+ /**
+ * Will be called when new content is available (clear internal cache if any)
+ */
+ public void clear() {
- // will be called when new content is available (clear internal cache if any)
- void clear();
+ }
+
+ public long id(@NonNull N node) {
+ return node.hashCode();
+ }
+
+ public void onViewRecycled(@NonNull H holder) {
+
+ }
}
public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown);
@@ -196,7 +187,15 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter V requireView(@IdRes int id) {
final V v = itemView.findViewById(id);
if (v == null) {
- throw new NullPointerException();
+ final String name;
+ if (id == 0
+ || id == View.NO_ID) {
+ name = String.valueOf(id);
+ } else {
+ name = "R.id." + itemView.getResources().getResourceName(id);
+ }
+ throw new NullPointerException(String.format("No view with id(R.id.%s) is found " +
+ "in layout: %s", name, itemView));
}
return v;
}
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 70406e78..30b093f5 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
@@ -32,6 +32,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
this.entries = entries;
this.defaultEntry = defaultEntry;
this.reducer = reducer;
+
setHasStableIds(true);
}
@@ -90,6 +91,14 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
: 0;
}
+ @Override
+ public void onViewRecycled(@NonNull Holder holder) {
+ super.onViewRecycled(holder);
+
+ final Entry entry = getEntry(holder.getItemViewType());
+ entry.onViewRecycled(holder);
+ }
+
@SuppressWarnings("unused")
@NonNull
public List getItems() {
@@ -132,9 +141,14 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
private final SparseArray> entries = new SparseArray<>(3);
- private Entry defaultEntry;
+ private final Entry defaultEntry;
+
private MarkwonReducer reducer;
+ BuilderImpl(@NonNull Entry defaultEntry) {
+ this.defaultEntry = defaultEntry;
+ }
+
@NonNull
@Override
public Builder include(
@@ -145,22 +159,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
return this;
}
- @NonNull
- @Override
- public Builder defaultEntry(@NonNull Entry extends Node, ? extends Holder> defaultEntry) {
- //noinspection unchecked
- this.defaultEntry = (Entry) defaultEntry;
- return this;
- }
-
- @NonNull
- @Override
- public Builder defaultEntry(int layoutResId) {
- //noinspection unchecked
- this.defaultEntry = (Entry) (Entry) new SimpleEntry(layoutResId);
- return this;
- }
-
@NonNull
@Override
public Builder reducer(@NonNull MarkwonReducer reducer) {
@@ -172,11 +170,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
@Override
public MarkwonAdapter build() {
- if (defaultEntry == null) {
- //noinspection unchecked
- defaultEntry = (Entry) (Entry) new SimpleEntry();
- }
-
if (reducer == null) {
reducer = MarkwonReducer.directChildren();
}
diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java
index 03ce5e56..93ef0d30 100644
--- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java
+++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java
@@ -1,9 +1,8 @@
package ru.noties.markwon.recycler;
+import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
-import android.text.Spannable;
-import android.text.SpannableString;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
@@ -16,32 +15,43 @@ import java.util.HashMap;
import java.util.Map;
import ru.noties.markwon.Markwon;
+import ru.noties.markwon.utils.NoCopySpannableFactory;
/**
* @since 3.0.0
*/
@SuppressWarnings("WeakerAccess")
-public class SimpleEntry implements MarkwonAdapter.Entry {
+public class SimpleEntry extends MarkwonAdapter.Entry {
- public static final Spannable.Factory NO_COPY_SPANNABLE_FACTORY = new NoCopySpannableFactory();
+ /**
+ * Create {@link SimpleEntry} that has TextView as the root view of
+ * specified layout.
+ */
+ @NonNull
+ public static SimpleEntry createTextViewIsRoot(@LayoutRes int layoutResId) {
+ return new SimpleEntry(layoutResId, 0);
+ }
+
+ @NonNull
+ public static SimpleEntry create(@LayoutRes int layoutResId, @IdRes int textViewIdRes) {
+ return new SimpleEntry(layoutResId, textViewIdRes);
+ }
// small cache for already rendered nodes
private final Map cache = new HashMap<>();
private final int layoutResId;
+ private final int textViewIdRes;
- public SimpleEntry() {
- this(R.layout.markwon_adapter_simple_entry);
- }
-
- public SimpleEntry(@LayoutRes int layoutResId) {
+ public SimpleEntry(@LayoutRes int layoutResId, @IdRes int textViewIdRes) {
this.layoutResId = layoutResId;
+ this.textViewIdRes = textViewIdRes;
}
@NonNull
@Override
public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
- return new Holder(inflater.inflate(layoutResId, parent, false));
+ return new Holder(textViewIdRes, inflater.inflate(layoutResId, parent, false));
}
@Override
@@ -54,11 +64,6 @@ public class SimpleEntry implements MarkwonAdapter.Entry
-
\ No newline at end of file
diff --git a/sample/build.gradle b/sample/build.gradle
index 35db7389..966d0747 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -29,14 +29,6 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
-
- sourceSets {
- main {
- // let's use different res directory so sample will have _isolated_ resources from others
- res.srcDirs += [ './src/main/recycler/res' ]
- assets.srcDirs += ['./src/main/recycler/assets']
- }
- }
}
dependencies {
@@ -51,6 +43,7 @@ dependencies {
implementation project(':markwon-image-svg')
implementation project(':markwon-syntax-highlight')
implementation project(':markwon-recycler')
+ implementation project(':markwon-recycler-table')
deps.with {
implementation it['support-recycler-view']
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index d1442bf0..376348ca 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -7,11 +7,9 @@
+ tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon">
diff --git a/sample/src/main/assets/README.md b/sample/src/main/assets/README.md
new file mode 120000
index 00000000..ff5c7960
--- /dev/null
+++ b/sample/src/main/assets/README.md
@@ -0,0 +1 @@
+../../../../README.md
\ No newline at end of file
diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java
index c230aa6c..adeb6c4b 100644
--- a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java
+++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java
@@ -11,15 +11,12 @@ import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import org.commonmark.ext.gfm.tables.TableBlock;
-import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.node.FencedCodeBlock;
-import org.commonmark.parser.Parser;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.util.Collections;
import ru.noties.debug.AndroidLogDebugOutput;
import ru.noties.debug.Debug;
@@ -33,6 +30,8 @@ import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.svg.SvgPlugin;
import ru.noties.markwon.recycler.MarkwonAdapter;
import ru.noties.markwon.recycler.SimpleEntry;
+import ru.noties.markwon.recycler.table.TableEntry;
+import ru.noties.markwon.recycler.table.TableEntryPlugin;
import ru.noties.markwon.sample.R;
import ru.noties.markwon.urlprocessor.UrlProcessor;
import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
@@ -51,14 +50,12 @@ public class RecyclerActivity extends Activity {
// create MarkwonAdapter and register two blocks that will be rendered differently
// * fenced code block (can also specify the same Entry for indended code block)
// * table block
- final MarkwonAdapter adapter = MarkwonAdapter.builder()
- // we can simply use bundled SimpleEntry, that will lookup a TextView
- // with `@+id/text` id
- .include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block))
- // create own implementation of entry for different rendering
- .include(TableBlock.class, new TableEntry2())
- // specify default entry (for all other blocks)
- .defaultEntry(new SimpleEntry(R.layout.adapter_default_entry))
+ final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text)
+ // we can simply use bundled SimpleEntry
+ .include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text))
+ .include(TableBlock.class, TableEntry.create(builder -> builder
+ .tableLayout(R.layout.adapter_table_block, R.id.table_layout)
+ .textLayoutIsRoot(R.layout.view_table_entry_cell)))
.build();
final RecyclerView recyclerView = findViewById(R.id.recycler_view);
@@ -71,10 +68,6 @@ public class RecyclerActivity extends Activity {
// please note that we should notify updates (adapter doesn't do it implicitly)
adapter.notifyDataSetChanged();
-
- // NB, there is no currently available widget to render tables gracefully
- // TableEntryView is here for demonstration purposes only (to show that rendering
- // tables
}
@NonNull
@@ -83,14 +76,8 @@ public class RecyclerActivity extends Activity {
.usePlugin(CorePlugin.create())
.usePlugin(ImagesPlugin.createWithAssets(context))
.usePlugin(SvgPlugin.create(context.getResources()))
- .usePlugin(new AbstractMarkwonPlugin() {
- @Override
- public void configureParser(@NonNull Parser.Builder builder) {
- // it's important NOT to use TablePlugin
- // the only thing we want from it is commonmark-java parser extension
- builder.extensions(Collections.singleton(TablesExtension.create()));
- }
- })
+ // important to use TableEntryPlugin instead of TablePlugin
+ .usePlugin(TableEntryPlugin.create(context))
.usePlugin(HtmlPlugin.create())
// .usePlugin(SyntaxHighlightPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java
deleted file mode 100644
index 900ce719..00000000
--- a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package ru.noties.markwon.sample.recycler;
-
-import android.support.annotation.NonNull;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.commonmark.ext.gfm.tables.TableBlock;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import ru.noties.debug.Debug;
-import ru.noties.markwon.Markwon;
-import ru.noties.markwon.ext.tables.Table;
-import ru.noties.markwon.recycler.MarkwonAdapter;
-import ru.noties.markwon.sample.R;
-
-// do not use in real applications, this is just a showcase
-public class TableEntry implements MarkwonAdapter.Entry {
-
- private final Map cache = new HashMap<>(2);
-
- @NonNull
- @Override
- public TableNodeHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
- return new TableNodeHolder(inflater.inflate(R.layout.adapter_table_block, parent, false));
- }
-
- @Override
- public void bindHolder(@NonNull Markwon markwon, @NonNull TableNodeHolder holder, @NonNull TableBlock node) {
-
- Table table = cache.get(node);
- if (table == null) {
- table = Table.parse(markwon, node);
- cache.put(node, table);
- }
-
- Debug.i(table);
-
- if (table != null) {
- holder.tableEntryView.setTable(table);
- // render table
- } // we need to do something with null table...
- }
-
- @Override
- public long id(@NonNull TableBlock node) {
- return node.hashCode();
- }
-
- @Override
- public void clear() {
- cache.clear();
- }
-
- static class TableNodeHolder extends MarkwonAdapter.Holder {
-
- final TableEntryView tableEntryView;
-
- TableNodeHolder(@NonNull View itemView) {
- super(itemView);
-
- this.tableEntryView = requireView(R.id.table_entry);
- }
- }
-}
diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry2.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry2.java
deleted file mode 100644
index dcf8d061..00000000
--- a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry2.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package ru.noties.markwon.sample.recycler;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.view.Gravity;
-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 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.R;
-
-public class TableEntry2 implements MarkwonAdapter.Entry {
-
- private final Map map = new HashMap<>(3);
-
- @NonNull
- @Override
- public TableHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
- return new TableHolder(inflater.inflate(R.layout.adapter_table_block_2, parent, false));
- }
-
- @Override
- public void bindHolder(@NonNull Markwon markwon, @NonNull TableHolder holder, @NonNull TableBlock node) {
-
- Table table = map.get(node);
- if (table == null) {
- table = Table.parse(markwon, node);
- map.put(node, table);
- }
-
- // check if this exact TableBlock was already
- final TableLayout layout = holder.layout;
- if (table == null
- || table == layout.getTag(R.id.table_layout)) {
- return;
- }
-
- layout.setTag(R.id.table_layout, table);
- layout.removeAllViews();
- layout.setBackgroundResource(R.drawable.bg_table_cell);
-
- final Context context = layout.getContext();
- final LayoutInflater inflater = LayoutInflater.from(context);
-
- TableRow tableRow;
- TextView textView;
-
- for (Table.Row row : table.rows()) {
- tableRow = new TableRow(context);
- for (Table.Column column : row.columns()) {
- textView = (TextView) inflater.inflate(R.layout.view_table_entry_cell, tableRow, false);
- textView.setGravity(textGravity(column.alignment()));
- markwon.setParsedMarkdown(textView, column.content());
- textView.getPaint().setFakeBoldText(row.header());
- textView.setBackgroundResource(R.drawable.bg_table_cell);
- tableRow.addView(textView);
- }
- layout.addView(tableRow);
- }
- }
-
- @Override
- public long id(@NonNull TableBlock node) {
- return node.hashCode();
- }
-
- @Override
- public void clear() {
- map.clear();
- }
-
- static class TableHolder extends MarkwonAdapter.Holder {
-
- final TableLayout layout;
-
- TableHolder(@NonNull View itemView) {
- super(itemView);
-
- this.layout = requireView(R.id.table_layout);
- }
- }
-
- // we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17)
- @SuppressLint("RtlHardcoded")
- private static int textGravity(@NonNull Table.Alignment alignment) {
-
- final int gravity;
-
- switch (alignment) {
-
- case LEFT:
- gravity = Gravity.LEFT;
- break;
-
- case CENTER:
- gravity = Gravity.CENTER_HORIZONTAL;
- break;
-
- case RIGHT:
- gravity = Gravity.RIGHT;
- break;
-
- default:
- throw new IllegalStateException("Unknown table alignment: " + alignment);
- }
-
- return gravity | Gravity.CENTER_VERTICAL;
- }
-}
diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java
deleted file mode 100644
index df789728..00000000
--- a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java
+++ /dev/null
@@ -1,219 +0,0 @@
-package ru.noties.markwon.sample.recycler;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.text.SpannedString;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import ru.noties.markwon.ext.tables.Table;
-import ru.noties.markwon.sample.R;
-
-public class TableEntryView extends LinearLayout {
-
- // paint and rect to draw borders
- private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private final Rect rect = new Rect();
-
- private LayoutInflater inflater;
-
- private int rowEvenBackgroundColor;
-
- public TableEntryView(Context context) {
- super(context);
- init(context, null);
- }
-
- public TableEntryView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context, attrs);
- }
-
- private void init(Context context, @Nullable AttributeSet attrs) {
- inflater = LayoutInflater.from(context);
- setOrientation(VERTICAL);
-
- if (attrs != null) {
- final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TableEntryView);
- try {
-
- rowEvenBackgroundColor = array.getColor(R.styleable.TableEntryView_tev_rowEvenBackgroundColor, 0);
-
- final int stroke = array.getDimensionPixelSize(R.styleable.TableEntryView_tev_borderWidth, 0);
-
- // half of requested
- final float strokeWidth = stroke > 0
- ? stroke / 2.F
- : context.getResources().getDisplayMetrics().density / 2.F;
-
- paint.setStyle(Paint.Style.STROKE);
- paint.setStrokeWidth(strokeWidth);
- paint.setColor(array.getColor(R.styleable.TableEntryView_tev_borderColor, Color.BLACK));
-
- if (isInEditMode()) {
- final String data = array.getString(R.styleable.TableEntryView_tev_debugData);
- if (data != null) {
-
- boolean first = true;
-
- final List rows = new ArrayList<>();
- for (String row : data.split("\\|")) {
- final List columns = new ArrayList<>();
- for (String column : row.split(",")) {
- columns.add(new Table.Column(Table.Alignment.LEFT, new SpannedString(column)));
- }
- final boolean header = first;
- first = false;
- rows.add(new Table.Row(header, columns));
- }
- final Table table = new Table(rows);
- setTable(table);
- }
- }
- } finally {
- array.recycle();
- }
- }
-
- setWillNotDraw(false);
- }
-
- public void setTable(@NonNull Table table) {
- final List rows = table.rows();
- for (int i = 0, size = rows.size(); i < size; i++) {
- addRow(i, rows.get(i));
- }
- requestLayout();
- }
-
- private void addRow(int index, @NonNull Table.Row row) {
-
- final ViewGroup group = ensureRow(index);
-
- final int backgroundColor = !row.header() && (index % 2) == 0
- ? rowEvenBackgroundColor
- : 0;
-
- group.setBackgroundColor(backgroundColor);
-
- final List columns = row.columns();
-
- TextView textView;
- Table.Column column;
-
- for (int i = 0, size = columns.size(); i < size; i++) {
- textView = ensureCell(group, i);
- column = columns.get(i);
- textView.setGravity(textGravity(column.alignment()));
- textView.setText(column.content());
- textView.getPaint().setFakeBoldText(row.header());
- }
-
- group.requestLayout();
- }
-
- @NonNull
- private ViewGroup ensureRow(int index) {
-
- final int count = getChildCount();
- if (index >= count) {
-
- // count=0,index=1, diff=2
- // count=0,index=5, diff=6
- // count=1,index=2, diff=2
- int diff = index - count + 1;
- while (diff > 0) {
- addView(inflater.inflate(R.layout.view_table_entry_row, this, false));
- diff -= 1;
- }
- }
-
- return (ViewGroup) getChildAt(index);
- }
-
- @NonNull
- private TextView ensureCell(@NonNull ViewGroup group, int index) {
-
- final int count = group.getChildCount();
- if (index >= count) {
- int diff = index - count + 1;
- while (diff > 0) {
- group.addView(inflater.inflate(R.layout.view_table_entry_cell, group, false));
- diff -= 1;
- }
- }
-
- return (TextView) group.getChildAt(index);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- final int rows = getChildCount();
- if (rows == 0) {
- return;
- }
-
- // first draw the whole border
- rect.set(0, 0, getWidth(), getHeight());
- canvas.drawRect(rect, paint);
-
- ViewGroup group;
- View view;
-
- int top;
-
- for (int row = 0; row < rows; row++) {
- group = (ViewGroup) getChildAt(row);
- top = group.getTop();
- for (int col = 0, cols = group.getChildCount(); col < cols; col++) {
- view = group.getChildAt(col);
- rect.set(view.getLeft(), top + view.getTop(), view.getRight(), top + view.getBottom());
- canvas.drawRect(rect, paint);
- }
- }
- }
-
- // we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17)
- @SuppressLint("RtlHardcoded")
- private static int textGravity(@NonNull Table.Alignment alignment) {
-
- final int gravity;
-
- switch (alignment) {
-
- case LEFT:
- gravity = Gravity.LEFT;
- break;
-
- case CENTER:
- gravity = Gravity.CENTER_HORIZONTAL;
- break;
-
- case RIGHT:
- gravity = Gravity.RIGHT;
- break;
-
- default:
- throw new IllegalStateException("Unknown table alignment: " + alignment);
- }
-
- return gravity;
- }
-}
diff --git a/sample/src/main/recycler/assets/README.md b/sample/src/main/recycler/assets/README.md
deleted file mode 120000
index 1dfab242..00000000
--- a/sample/src/main/recycler/assets/README.md
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../README.md
\ No newline at end of file
diff --git a/sample/src/main/recycler/res/layout/adapter_table_block.xml b/sample/src/main/recycler/res/layout/adapter_table_block.xml
deleted file mode 100644
index 3358ca5a..00000000
--- a/sample/src/main/recycler/res/layout/adapter_table_block.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/sample/src/main/recycler/res/values/attrs.xml b/sample/src/main/recycler/res/values/attrs.xml
deleted file mode 100644
index 1827819d..00000000
--- a/sample/src/main/recycler/res/values/attrs.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/sample/src/main/res/drawable-v26/ic_launcher_background.xml b/sample/src/main/res/drawable-v26/ic_launcher_background.xml
deleted file mode 100644
index a197b896..00000000
--- a/sample/src/main/res/drawable-v26/ic_launcher_background.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
diff --git a/sample/src/main/recycler/res/layout/activity_recycler.xml b/sample/src/main/res/layout/activity_recycler.xml
similarity index 100%
rename from sample/src/main/recycler/res/layout/activity_recycler.xml
rename to sample/src/main/res/layout/activity_recycler.xml
diff --git a/sample/src/main/recycler/res/layout/adapter_default_entry.xml b/sample/src/main/res/layout/adapter_default_entry.xml
similarity index 100%
rename from sample/src/main/recycler/res/layout/adapter_default_entry.xml
rename to sample/src/main/res/layout/adapter_default_entry.xml
diff --git a/sample/src/main/recycler/res/layout/adapter_fenced_code_block.xml b/sample/src/main/res/layout/adapter_fenced_code_block.xml
similarity index 100%
rename from sample/src/main/recycler/res/layout/adapter_fenced_code_block.xml
rename to sample/src/main/res/layout/adapter_fenced_code_block.xml
diff --git a/sample/src/main/recycler/res/layout/adapter_table_block_2.xml b/sample/src/main/res/layout/adapter_table_block.xml
similarity index 83%
rename from sample/src/main/recycler/res/layout/adapter_table_block_2.xml
rename to sample/src/main/res/layout/adapter_table_block.xml
index 6cdb3be6..aaaaa369 100644
--- a/sample/src/main/recycler/res/layout/adapter_table_block_2.xml
+++ b/sample/src/main/res/layout/adapter_table_block.xml
@@ -13,6 +13,7 @@
+ android:layout_height="wrap_content"
+ android:stretchColumns="*" />
\ No newline at end of file
diff --git a/sample/src/main/recycler/res/layout/view_table_entry_cell.xml b/sample/src/main/res/layout/view_table_entry_cell.xml
similarity index 69%
rename from sample/src/main/recycler/res/layout/view_table_entry_cell.xml
rename to sample/src/main/res/layout/view_table_entry_cell.xml
index 6261bacb..6d544918 100644
--- a/sample/src/main/recycler/res/layout/view_table_entry_cell.xml
+++ b/sample/src/main/res/layout/view_table_entry_cell.xml
@@ -2,9 +2,7 @@
-
-
-
-
\ No newline at end of file
diff --git a/sample/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png b/sample/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png
deleted file mode 100644
index 01bae147..00000000
Binary files a/sample/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png and /dev/null differ
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 1ba57872..00000000
Binary files a/sample/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/sample/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png b/sample/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png
deleted file mode 100644
index 7361012f..00000000
Binary files a/sample/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png and /dev/null differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 8025e6dc..00000000
Binary files a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/sample/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png b/sample/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png
deleted file mode 100644
index 24f9ddbc..00000000
Binary files a/sample/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png and /dev/null differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 11dcdbc9..00000000
Binary files a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png b/sample/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png
deleted file mode 100644
index b64bf44f..00000000
Binary files a/sample/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png and /dev/null differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 2439acf0..00000000
Binary files a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/settings.gradle b/settings.gradle
index e3177a74..faf09343 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,5 +10,6 @@ include ':app', ':sample',
':markwon-image-okhttp',
':markwon-image-svg',
':markwon-recycler',
+ ':markwon-recycler-table',
':markwon-syntax-highlight',
':markwon-test-span'