Task lists implementation

This commit is contained in:
Dimitry Ivanov 2017-10-23 15:35:05 +03:00
parent f95918104b
commit bf5a8142f5
15 changed files with 112 additions and 89 deletions

View File

@ -6,9 +6,6 @@
[![markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon-image-loader.svg?label=markwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon-image-loader%22) [![markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon-image-loader.svg?label=markwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon-image-loader%22)
[![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon-view%22) [![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon-view%22)
- [ ] one **one** _one_
- [X] ~~two~~
**Markwon** is a library for Android that renders markdown as system-native Spannables. It gives ability to display markdown in all TextView widgets (**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Notifications**, **Toasts**, etc. <u>**No WebView is required**</u>. Library provides reasonable defaults for display style of markdown but also gives all the means to tweak the appearance if desired. All markdown features are supported (including limited support for inlined HTML code, markdown tables and images). **Markwon** is a library for Android that renders markdown as system-native Spannables. It gives ability to display markdown in all TextView widgets (**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Notifications**, **Toasts**, etc. <u>**No WebView is required**</u>. Library provides reasonable defaults for display style of markdown but also gives all the means to tweak the appearance if desired. All markdown features are supported (including limited support for inlined HTML code, markdown tables and images).
<sup>*</sup>*This file is displayed by default in the [sample-apk] application. Which is a generic markdown viewer with support to display markdown via `http`, `https` & `file` schemes and 2 themes included: Light & Dark* <sup>*</sup>*This file is displayed by default in the [sample-apk] application. Which is a generic markdown viewer with support to display markdown via `http`, `https` & `file` schemes and 2 themes included: Light & Dark*
@ -41,6 +38,11 @@ compile 'ru.noties:markwon-view:1.0.0' // optional
* * Underline (`<u>`) * * Underline (`<u>`)
* * Strike-through (`<s>`, `<strike>`, `<del>`) * * Strike-through (`<s>`, `<strike>`, `<del>`)
* other inline html is rendered via (`Html.fromHtml(...)`) * other inline html is rendered via (`Html.fromHtml(...)`)
* Task lists:
- [ ] Not _done_
- [X] **Done** with `X`
- [x] ~~and~~ **or** small `x`
--- ---

View File

@ -105,6 +105,16 @@ public Builder tableBorderWidth(@Dimension int tableBorderWidth);
public Builder tableOddRowBackgroundColor(@ColorInt int tableOddRowBackgroundColor); public Builder tableOddRowBackgroundColor(@ColorInt int tableOddRowBackgroundColor);
``` ```
#### Task lists
Task lists are supported but with some limitations. First of all, task list cannot be nested
(in a list, quote, etc). By default (if used factory method `builderWithDefaults`) TaskListDrawable
will be used with `linkColor` as the primary color and `windowBackground` as the checkMarkColor.
```java
public Builder taskListDrawable(@NonNull Drawable taskListDrawable);
```
### Contents ### Contents

View File

@ -1,10 +0,0 @@
apply plugin: 'java'
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
dependencies {
compile SUPPORT_ANNOTATIONS
compile COMMON_MARK
compile 'ru.noties:debug:3.0.0@jar'
}

View File

@ -18,8 +18,6 @@ dependencies {
compile COMMON_MARK compile COMMON_MARK
compile COMMON_MARK_STRIKETHROUGHT compile COMMON_MARK_STRIKETHROUGHT
compile COMMON_MARK_TABLE compile COMMON_MARK_TABLE
compile project(':library-task-list')
} }
if (project.hasProperty('release')) { if (project.hasProperty('release')) {

View File

@ -17,7 +17,7 @@ import java.util.Arrays;
import ru.noties.markwon.renderer.SpannableRenderer; import ru.noties.markwon.renderer.SpannableRenderer;
import ru.noties.markwon.tasklist.TaskListExtension; import ru.noties.markwon.tasklist.TaskListExtension;
@SuppressWarnings("WeakerAccess") @SuppressWarnings({"WeakerAccess", "unused"})
public abstract class Markwon { public abstract class Markwon {
/** /**

View File

@ -299,11 +299,25 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
if (customNode instanceof TaskListItem) { if (customNode instanceof TaskListItem) {
final TaskListItem listItem = (TaskListItem) customNode;
final int length = builder.length(); final int length = builder.length();
blockQuoteIndent += listItem.indent();
visitChildren(customNode); visitChildren(customNode);
setSpan(length, new TaskListSpan(configuration.theme(), blockQuoteIndent, length, ((TaskListItem) customNode).done()));
setSpan(length, new TaskListSpan(
configuration.theme(),
blockQuoteIndent,
length,
listItem.done()
));
newLine(); newLine();
blockQuoteIndent -= listItem.indent();
} else { } else {
super.visit(customNode); super.visit(customNode);
} }

View File

@ -8,16 +8,9 @@ import org.commonmark.node.Node;
import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.SpannableConfiguration;
// please note that this class does not implement Renderer in order to return CharSequence (instead of String)
public class SpannableRenderer { public class SpannableRenderer {
// todo @Nullable
// * LinkDrawableSpan, that draws link whilst image is still loading (it must be clickable...)
// * Common interface for images (in markdown & inline-html)
// * util method to properly copy markdown (with lists/links, etc)
// * util to apply empty line height
// * transform relative urls to absolute ones...
public CharSequence render(@NonNull SpannableConfiguration configuration, @Nullable Node node) { public CharSequence render(@NonNull SpannableConfiguration configuration, @Nullable Node node) {
final CharSequence out; final CharSequence out;
if (node == null) { if (node == null) {

View File

@ -11,22 +11,13 @@ import android.support.annotation.Dimension;
import android.support.annotation.FloatRange; import android.support.annotation.FloatRange;
import android.support.annotation.IntRange; import android.support.annotation.IntRange;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextPaint; import android.text.TextPaint;
import android.util.TypedValue; import android.util.TypedValue;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class SpannableTheme { public class SpannableTheme {
//
// // this method should be used if TextView is known beforehand
// // it will correctly measure the `space` char and set it as `codeMultilineMargin`
// // otherwise this value must be set explicitly
// public static SpannableTheme create(@NonNull TextView textView) {
// return builderWithDefaults(textView.getContext())
// .codeMultilineMargin((int) (textView.getPaint().measureText("\u00a0") + .5F))
// .build();
// }
// this create default theme (except for `codeMultilineMargin` property)
public static SpannableTheme create(@NonNull Context context) { public static SpannableTheme create(@NonNull Context context) {
return builderWithDefaults(context).build(); return builderWithDefaults(context).build();
} }
@ -41,6 +32,8 @@ public class SpannableTheme {
public static Builder builderWithDefaults(@NonNull Context context) { public static Builder builderWithDefaults(@NonNull Context context) {
// by default we will be using link color for the checkbox color
// & window background as a checkMark color
final int linkColor = resolve(context, android.R.attr.textColorLink); final int linkColor = resolve(context, android.R.attr.textColorLink);
final int backgroundColor = resolve(context, android.R.attr.colorBackground); final int backgroundColor = resolve(context, android.R.attr.colorBackground);
@ -153,6 +146,8 @@ public class SpannableTheme {
// by default paint.color * TABLE_ODD_ROW_DEF_ALPHA // by default paint.color * TABLE_ODD_ROW_DEF_ALPHA
protected final int tableOddRowBackgroundColor; protected final int tableOddRowBackgroundColor;
// drawable that will be used to render checkbox (should be stateful)
// TaskListDrawable can be used
protected final Drawable taskListDrawable; protected final Drawable taskListDrawable;
protected SpannableTheme(@NonNull Builder builder) { protected SpannableTheme(@NonNull Builder builder) {
@ -252,10 +247,16 @@ public class SpannableTheme {
// custom typeface was set // custom typeface was set
if (codeTypeface != null) { if (codeTypeface != null) {
paint.setTypeface(codeTypeface); paint.setTypeface(codeTypeface);
// please note that we won't be calculating textSize
// (like we do when no Typeface is provided), if it's some specific typeface
// we would confuse users about textSize
if (codeTextSize != 0) { if (codeTextSize != 0) {
paint.setTextSize(codeTextSize); paint.setTextSize(codeTextSize);
} }
} else { } else {
paint.setTypeface(Typeface.MONOSPACE); paint.setTypeface(Typeface.MONOSPACE);
final float textSize; final float textSize;
@ -372,7 +373,7 @@ public class SpannableTheme {
paint.setStyle(Paint.Style.FILL); paint.setStyle(Paint.Style.FILL);
} }
@NonNull @Nullable
public Drawable getTaskListDrawable() { public Drawable getTaskListDrawable() {
return taskListDrawable; return taskListDrawable;
} }
@ -546,6 +547,7 @@ public class SpannableTheme {
} }
private static class Dip { private static class Dip {
private final float density; private final float density;
Dip(@NonNull Context context) { Dip(@NonNull Context context) {

View File

@ -46,7 +46,7 @@ public class TaskListDrawable extends Drawable {
protected void onBoundsChange(Rect bounds) { protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds); super.onBoundsChange(bounds);
// we should exclude stroke with from final bounds (half of the strokeWidth from both sides) // we should exclude stroke with from final bounds (half of the strokeWidth from all sides)
// we should have square shape // we should have square shape
final float min = Math.min(bounds.width(), bounds.height()); final float min = Math.min(bounds.width(), bounds.height());

View File

@ -23,7 +23,7 @@ public class TaskListSpan implements LeadingMarginSpan {
@Override @Override
public int getLeadingMargin(boolean first) { public int getLeadingMargin(boolean first) {
return theme.getBlockMargin(); return theme.getBlockMargin() * blockIndent;
} }
@Override @Override
@ -33,14 +33,17 @@ public class TaskListSpan implements LeadingMarginSpan {
return; return;
} }
final Drawable drawable = theme.getTaskListDrawable();
if (drawable == null) {
return;
}
final int save = c.save(); final int save = c.save();
try { try {
final int width = theme.getBlockMargin(); final int width = theme.getBlockMargin();
final int height = bottom - top; final int height = bottom - top;
final Drawable drawable = theme.getTaskListDrawable();
final int w = (int) (width * .75F + .5F); final int w = (int) (width * .75F + .5F);
final int h = (int) (height * .75F + .5F); final int h = (int) (height * .75F + .5F);

View File

@ -17,20 +17,20 @@ import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import ru.noties.debug.Debug; @SuppressWarnings("WeakerAccess")
class TaskListBlockParser extends AbstractBlockParser { class TaskListBlockParser extends AbstractBlockParser {
private static final Pattern PATTERN = Pattern.compile("\\s*-\\s+\\[(x|X|\\s)\\]\\s+(.*)"); private static final Pattern PATTERN = Pattern.compile("\\s*-\\s+\\[(x|X|\\s)\\]\\s+(.*)");
// private static final Pattern PATTERN_2 = Pattern.compile("^\\s*-\\s+\\[(x|X|\\s)\\]\\s+(.*)");
private final TaskListBlock block = new TaskListBlock(); private final TaskListBlock block = new TaskListBlock();
private final List<String> lines; private final List<Item> items = new ArrayList<>(3);
TaskListBlockParser(@NonNull String startLine) { private int indent = 0;
this.lines = new ArrayList<>(3);
this.lines.add(startLine); TaskListBlockParser(@NonNull String startLine, int startIndent) {
items.add(new Item(startLine, startIndent));
indent = startIndent;
} }
@Override @Override
@ -45,16 +45,18 @@ class TaskListBlockParser extends AbstractBlockParser {
final String line = line(parserState); final String line = line(parserState);
// Debug.i("line: %s, find: %s", line, PATTERN.matcher(line).find()); final int currentIndent = parserState.getIndent();
Debug.i("isBlank: %s, line: `%s`", parserState.isBlank(), line); if (currentIndent > indent) {
indent += 2;
} else if (currentIndent < indent && indent > 1) {
indent -= 2;
}
if (line != null if (line != null
&& line.length() > 0 && line.length() > 0
&& PATTERN.matcher(line).matches()) { && PATTERN.matcher(line).matches()) {
Debug.e();
blockContinue = BlockContinue.atIndex(parserState.getIndex()); blockContinue = BlockContinue.atIndex(parserState.getIndex());
} else { } else {
Debug.e();
blockContinue = BlockContinue.finished(); blockContinue = BlockContinue.finished();
} }
@ -63,54 +65,29 @@ class TaskListBlockParser extends AbstractBlockParser {
@Override @Override
public void addLine(CharSequence line) { public void addLine(CharSequence line) {
Debug.i("line: %s", line); if (length(line) > 0) {
if (line != null items.add(new Item(line.toString(), indent));
&& line.length() > 0) {
lines.add(line.toString());
} }
} }
@Override @Override
public void parseInlines(InlineParser inlineParser) { public void parseInlines(InlineParser inlineParser) {
Debug.i(lines);
Matcher matcher; Matcher matcher;
TaskListItem item; TaskListItem listItem;
for (String line : lines) {
matcher = PATTERN.matcher(line);
for (Item item : items) {
matcher = PATTERN.matcher(item.line);
if (!matcher.matches()) { if (!matcher.matches()) {
continue; continue;
} }
listItem = new TaskListItem()
item = new TaskListItem().done(isDone(matcher.group(1))); .done(isDone(matcher.group(1)))
.indent(item.indent / 2);
inlineParser.parse(matcher.group(2), item); inlineParser.parse(matcher.group(2), listItem);
block.appendChild(listItem);
block.appendChild(item);
} }
}
@Override
public boolean isContainer() {
return false;
}
@Override
public boolean canContain(Block block) {
Debug.i("block: %s", block);
return false;
}
@Override
public void closeBlock() {
Debug.e(block);
Debug.trace();
} }
static class Factory extends AbstractBlockParserFactory { static class Factory extends AbstractBlockParserFactory {
@ -124,8 +101,14 @@ class TaskListBlockParser extends AbstractBlockParser {
&& line.length() > 0 && line.length() > 0
&& PATTERN.matcher(line).matches()) { && PATTERN.matcher(line).matches()) {
return BlockStart.of(new TaskListBlockParser(line)) final int length = line.length();
.atIndex(state.getIndex() + line.length()); final int index = state.getIndex();
final int atIndex = index != 0
? index + (length - index)
: length;
return BlockStart.of(new TaskListBlockParser(line, state.getIndent()))
.atIndex(atIndex);
} }
return BlockStart.none(); return BlockStart.none();
@ -140,8 +123,25 @@ class TaskListBlockParser extends AbstractBlockParser {
: null; : null;
} }
private static int length(@Nullable CharSequence text) {
return text != null
? text.length()
: 0;
}
private static boolean isDone(@NonNull String value) { private static boolean isDone(@NonNull String value) {
return "X".equals(value) return "X".equals(value)
|| "x".equals(value); || "x".equals(value);
} }
private static class Item {
final String line;
final int indent;
Item(@NonNull String line, int indent) {
this.line = line;
this.indent = indent;
}
}
} }

View File

@ -2,9 +2,11 @@ package ru.noties.markwon.tasklist;
import org.commonmark.node.CustomNode; import org.commonmark.node.CustomNode;
@SuppressWarnings("WeakerAccess")
public class TaskListItem extends CustomNode { public class TaskListItem extends CustomNode {
private boolean done; private boolean done;
private int indent;
public boolean done() { public boolean done() {
return done; return done;
@ -14,4 +16,13 @@ public class TaskListItem extends CustomNode {
this.done = done; this.done = done;
return this; return this;
} }
public int indent() {
return indent;
}
public TaskListItem indent(int indent) {
this.indent = indent;
return this;
}
} }

View File

@ -1 +1 @@
include ':app', ':library', ':library-image-loader', ':library-view', ':library-task-list' include ':app', ':library', ':library-image-loader', ':library-view'