Working with tables

This commit is contained in:
Dimitry Ivanov 2017-05-25 13:22:11 +03:00
parent 37bf9f79db
commit 392f53c133
9 changed files with 408 additions and 21 deletions

View File

@ -110,24 +110,19 @@ Lorem ipsum dolor sit amet
### H.T.M.L. ### H.T.M.L.
<b>O</b><i>K<s>A</s><sup>42<sup>43<sub><b>42</b></sub></sup></sup><u>Y</u></i> <b>O</b><i>K<s>A</s><sup>42<sup>43<sub><b>42</b></sub></sup></sup><u>Y</u></i>
<img src="h" /> <img src="h"> ### Tables
<img src="h" alt="alt text"> Header #1 | Header #2 | Header #3
---: | :---: | :---
content | content | content
long long long skjfs fgjsdfhj sf `dfk df` | sdsd,fklsdfklsdfklsdfkl sdfkl dsfjksdf sjkf jksdfjksdf sjkdf sdfkjsdjkf sdkjfs fkjsf sdkjfs fkjsd fkjsdf skjdf sdkjf skjfs fkjs fkjsdf jskdf sdjkf sjdkf sdkjf skjf sdkjf sdkjf sdfkjsd fkjsd fkjsdf sdkjfsjk dfkjsdf sdkjfs | yeah
<hr>
<hr /> |head #1| head #2|
|---|---|
<h1>Hello</h1> | content | content |
| content | content |
<h2>Hello</h2> | content | content |
| content | content |
<h3>Hello</h3>
<h4>Hello</h4>
<h5>Hello</h5>
<h6>Hello</h6>
[1]: https://github.com [1]: https://github.com

View File

@ -60,7 +60,7 @@ public class MainActivity extends Activity {
markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
@Override @Override
public void apply(String text) { public void apply(final String text) {
markdownRenderer.render(MainActivity.this, uri(), text, new MarkdownRenderer.MarkdownReadyListener() { markdownRenderer.render(MainActivity.this, uri(), text, new MarkdownRenderer.MarkdownReadyListener() {
@Override @Override
public void onMarkdownReady(CharSequence markdown) { public void onMarkdownReady(CharSequence markdown) {

View File

@ -32,6 +32,7 @@ ext {
final def commonMarkVersion = '0.9.0' final def commonMarkVersion = '0.9.0'
COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion" COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion"
COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion" COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion"
COMMON_MARK_TABLE = "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion"
ANDROID_SVG = 'com.caverock:androidsvg:1.2.1' ANDROID_SVG = 'com.caverock:androidsvg:1.2.1'
ANDROID_GIF = 'pl.droidsonroids.gif:android-gif-drawable:1.2.7' ANDROID_GIF = 'pl.droidsonroids.gif:android-gif-drawable:1.2.7'

View File

@ -17,4 +17,7 @@ dependencies {
compile SUPPORT_ANNOTATIONS compile SUPPORT_ANNOTATIONS
compile COMMON_MARK compile COMMON_MARK
compile COMMON_MARK_STRIKETHROUGHT compile COMMON_MARK_STRIKETHROUGHT
compile COMMON_MARK_TABLE
compile 'ru.noties:debug:3.0.0@jar'
} }

View File

@ -8,10 +8,11 @@ import android.text.method.LinkMovementMethod;
import android.widget.TextView; import android.widget.TextView;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import java.util.Collections; import java.util.Arrays;
import ru.noties.markwon.renderer.SpannableRenderer; import ru.noties.markwon.renderer.SpannableRenderer;
@ -20,7 +21,7 @@ public abstract class Markwon {
public static Parser createParser() { public static Parser createParser() {
return new Parser.Builder() return new Parser.Builder()
.extensions(Collections.singleton(StrikethroughExtension.create())) .extensions(Arrays.asList(StrikethroughExtension.create(), TablesExtension.create()))
.build(); .build();
} }
@ -40,6 +41,7 @@ public abstract class Markwon {
public static void setText(@NonNull TextView view, CharSequence text) { public static void setText(@NonNull TextView view, CharSequence text) {
unscheduleDrawables(view); unscheduleDrawables(view);
unscheduleTableRows(view);
// update movement method (for links to be clickable) // update movement method (for links to be clickable)
view.setMovementMethod(LinkMovementMethod.getInstance()); view.setMovementMethod(LinkMovementMethod.getInstance());
@ -47,6 +49,7 @@ public abstract class Markwon {
// schedule drawables (dynamic drawables that can change bounds/animate will be correctly updated) // schedule drawables (dynamic drawables that can change bounds/animate will be correctly updated)
scheduleDrawables(view); scheduleDrawables(view);
scheduleTableRows(view);
} }
// with default configuration // with default configuration
@ -82,6 +85,14 @@ public abstract class Markwon {
DrawablesScheduler.unschedule(view); DrawablesScheduler.unschedule(view);
} }
public static void scheduleTableRows(@NonNull TextView view) {
TableRowsScheduler.schedule(view);
}
public static void unscheduleTableRows(@NonNull TextView view) {
TableRowsScheduler.unschedule(view);
}
private Markwon() { private Markwon() {
} }
} }

View File

@ -0,0 +1,64 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import ru.noties.markwon.spans.TableRowSpan;
abstract class TableRowsScheduler {
static void schedule(@NonNull final TextView view) {
final Object[] spans = extract(view);
if (spans != null
&& spans.length > 0) {
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
}
@Override
public void onViewDetachedFromWindow(View v) {
unschedule(view);
view.removeOnAttachStateChangeListener(this);
}
});
final TableRowSpan.Invalidator invalidator = new TableRowSpan.Invalidator() {
@Override
public void invalidate() {
view.setText(view.getText());
}
};
for (Object span : spans) {
((TableRowSpan) span).invalidator(invalidator);
}
}
}
static void unschedule(@NonNull TextView view) {
final Object[] spans = extract(view);
if (spans != null
&& spans.length > 0) {
for (Object span : spans) {
((TableRowSpan) span).invalidator(null);
}
}
}
private static Object[] extract(@NonNull TextView view) {
final Object[] out;
final CharSequence text = view.getText();
if (!TextUtils.isEmpty(text) && text instanceof Spanned) {
out = ((Spanned) text).getSpans(0, text.length(), TableRowSpan.class);
} else {
out = null;
}
return out;
}
private TableRowsScheduler() {
}
}

View File

@ -7,6 +7,9 @@ import android.text.TextUtils;
import android.text.style.StrikethroughSpan; import android.text.style.StrikethroughSpan;
import org.commonmark.ext.gfm.strikethrough.Strikethrough; import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.ext.gfm.tables.TableBody;
import org.commonmark.ext.gfm.tables.TableCell;
import org.commonmark.ext.gfm.tables.TableRow;
import org.commonmark.node.AbstractVisitor; import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.BlockQuote; import org.commonmark.node.BlockQuote;
import org.commonmark.node.BulletList; import org.commonmark.node.BulletList;
@ -31,8 +34,11 @@ import org.commonmark.node.Text;
import org.commonmark.node.ThematicBreak; import org.commonmark.node.ThematicBreak;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
import java.util.List;
import ru.noties.debug.Debug;
import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.renderer.html.SpannableHtmlParser; import ru.noties.markwon.renderer.html.SpannableHtmlParser;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawable;
@ -45,6 +51,7 @@ import ru.noties.markwon.spans.HeadingSpan;
import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.OrderedListItemSpan; import ru.noties.markwon.spans.OrderedListItemSpan;
import ru.noties.markwon.spans.StrongEmphasisSpan; import ru.noties.markwon.spans.StrongEmphasisSpan;
import ru.noties.markwon.spans.TableRowSpan;
import ru.noties.markwon.spans.ThematicBreakSpan; import ru.noties.markwon.spans.ThematicBreakSpan;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@ -259,17 +266,85 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
newLine(); newLine();
} }
private List<TableRowSpan.Cell> pendingTableRow;
private boolean tableRowIsHeader;
private int tableRows;
@Override @Override
public void visit(CustomNode customNode) { public void visit(CustomNode customNode) {
// Log.e(null, String.valueOf(customNode));
if (customNode instanceof Strikethrough) { if (customNode instanceof Strikethrough) {
final int length = builder.length(); final int length = builder.length();
visitChildren(customNode); visitChildren(customNode);
setSpan(length, new StrikethroughSpan()); setSpan(length, new StrikethroughSpan());
} else {
} else if (!handleTableNodes(customNode)) {
super.visit(customNode); super.visit(customNode);
} }
} }
private boolean handleTableNodes(CustomNode node) {
final boolean handled;
Debug.i(node);
if (node instanceof TableBody) {
visitChildren(node);
tableRows = 0;
handled = true;
newLine();
builder.append('\n');
} else if (node instanceof TableRow) {
final int length = builder.length();
visitChildren(node);
if (pendingTableRow != null) {
builder.append(' ');
final TableRowSpan span = new TableRowSpan(
configuration.theme(),
pendingTableRow,
tableRowIsHeader,
tableRows % 2 == 1
);
setSpan(length, span);
newLine();
pendingTableRow = null;
}
handled = true;
} else if (node instanceof TableCell) {
final TableCell cell = (TableCell) node;
final int length = builder.length();
visitChildren(cell);
if (pendingTableRow == null) {
pendingTableRow = new ArrayList<>(2);
}
pendingTableRow.add(new TableRowSpan.Cell(
tableCellAlignment(cell.getAlignment()),
builder.subSequence(length, builder.length())
));
builder.replace(length, builder.length(), "");
tableRowIsHeader = cell.isHeader();
tableRows = tableRowIsHeader
? 0
: tableRows + 1;
handled = true;
} else {
handled = false;
}
return handled;
}
@Override @Override
public void visit(Paragraph paragraph) { public void visit(Paragraph paragraph) {
@ -402,6 +477,27 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
return false; return false;
} }
@TableRowSpan.Alignment
private static int tableCellAlignment(TableCell.Alignment alignment) {
final int out;
if (alignment != null) {
switch (alignment) {
case CENTER:
out = TableRowSpan.ALIGN_CENTER;
break;
case RIGHT:
out = TableRowSpan.ALIGN_RIGHT;
break;
default:
out = TableRowSpan.ALIGN_LEFT;
break;
}
} else {
out = TableRowSpan.ALIGN_LEFT;
}
return out;
}
private static class HtmlInlineItem { private static class HtmlInlineItem {
final SpannableHtmlParser.Tag tag; final SpannableHtmlParser.Tag tag;

View File

@ -16,7 +16,7 @@ public class SpannableTheme {
// this method should be used if TextView is known beforehand // this method should be used if TextView is known beforehand
// it will correctly measure the `space` char and set it as `codeMultilineMargin` // it will correctly measure the `space` char and set it as `codeMultilineMargin`
// otherwise this value must be set explicitly ( // otherwise this value must be set explicitly
public static SpannableTheme create(@NonNull TextView textView) { public static SpannableTheme create(@NonNull TextView textView) {
return builderWithDefaults(textView.getContext()) return builderWithDefaults(textView.getContext())
.codeMultilineMargin((int) (textView.getPaint().measureText("\u00a0") + .5F)) .codeMultilineMargin((int) (textView.getPaint().measureText("\u00a0") + .5F))

View File

@ -0,0 +1,217 @@
package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.IntDef;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.style.ReplacementSpan;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
public class TableRowSpan extends ReplacementSpan {
public static final int ALIGN_LEFT = 0;
public static final int ALIGN_CENTER = 1;
public static final int ALIGN_RIGHT = 2;
@IntDef(value = {ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT})
@Retention(RetentionPolicy.SOURCE)
public @interface Alignment {
}
public interface Invalidator {
void invalidate();
}
public static class Cell {
final int alignment;
final CharSequence text;
public Cell(@Alignment int alignment, CharSequence text) {
this.alignment = alignment;
this.text = text;
}
@Alignment
public int alignment() {
return alignment;
}
public CharSequence text() {
return text;
}
@Override
public String toString() {
return "Cell{" +
"alignment=" + alignment +
", text=" + text +
'}';
}
}
private final SpannableTheme theme;
private final List<Cell> cells;
private final List<StaticLayout> layouts;
private final TextPaint textPaint;
private final boolean header;
private final boolean odd;
private int width;
private int height;
private Invalidator invalidator;
public TableRowSpan(
@NonNull SpannableTheme theme,
@NonNull List<Cell> cells,
boolean header,
boolean odd) {
this.theme = theme;
this.cells = cells;
this.layouts = new ArrayList<>(cells.size());
this.textPaint = new TextPaint();
this.header = header;
this.odd = odd;
}
@Override
public int getSize(
@NonNull Paint paint,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@Nullable Paint.FontMetricsInt fm) {
// it's our absolute requirement to have width of the canvas here... because, well, it changes
// the way we draw text. So, if we do not know the width of canvas we cannot correctly measure our text
if (layouts.size() > 0) {
if (fm != null) {
int max = 0;
for (StaticLayout layout : layouts) {
final int height = layout.getHeight();
if (height > max) {
max = height;
}
}
height = max;
fm.ascent = -max;
fm.descent = 0;
fm.top = fm.ascent;
fm.bottom = 0;
}
}
return width;
}
@Override
public void draw(
@NonNull Canvas canvas,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
float x,
int top,
int y,
int bottom,
@NonNull Paint paint) {
if (recreateLayouts(canvas.getWidth())) {
width = canvas.getWidth();
textPaint.set(paint);
makeNewLayouts();
}
int maxHeight = 0;
StaticLayout layout;
for (int i = 0, size = layouts.size(); i < size; i++) {
layout = layouts.get(i);
final int save = canvas.save();
try {
canvas.translate(x + (i * layout.getWidth()), top);
layout.draw(canvas);
if (layout.getHeight() > maxHeight) {
maxHeight = layout.getHeight();
}
} finally {
canvas.restoreToCount(save);
}
}
if (height != maxHeight) {
if (invalidator != null) {
invalidator.invalidate();
}
}
}
private boolean recreateLayouts(int newWidth) {
return width != newWidth;
}
private void makeNewLayouts() {
if (header) {
textPaint.setFakeBoldText(true);
}
final int w = width / cells.size();
this.layouts.clear();
Cell cell;
StaticLayout layout;
for (int i = 0, size = cells.size(); i < size; i++) {
cell = cells.get(i);
layout = new StaticLayout(
cell.text,
textPaint,
w,
alignment(cell.alignment),
1.F,
.0F,
false
);
layouts.add(layout);
}
}
private static Layout.Alignment alignment(@Alignment int alignment) {
final Layout.Alignment out;
switch (alignment) {
case ALIGN_CENTER:
out = Layout.Alignment.ALIGN_CENTER;
break;
case ALIGN_RIGHT:
out = Layout.Alignment.ALIGN_OPPOSITE;
break;
default:
out = Layout.Alignment.ALIGN_NORMAL;
break;
}
return out;
}
public TableRowSpan invalidator(Invalidator invalidator) {
this.invalidator = invalidator;
return this;
}
}