Extracting functionality into library
This commit is contained in:
parent
3e9ff80da1
commit
6f5fd08de4
@ -1,32 +1,20 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
compileSdkVersion TARGET_SDK
|
||||
buildToolsVersion BUILD_TOOLS
|
||||
|
||||
defaultConfig {
|
||||
applicationId "ru.noties.markwon"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 25
|
||||
minSdkVersion MIN_SDK
|
||||
targetSdkVersion TARGET_SDK
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
versionName version
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||
compile 'com.atlassian.commonmark:commonmark:0.9.0'
|
||||
compile 'com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.9.0'
|
||||
compile project(':library-renderer')
|
||||
compile 'ru.noties:debug:3.0.0@jar'
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="ru.noties.markwon">
|
||||
|
||||
<application
|
||||
@ -8,13 +9,14 @@
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity android:name=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -8,7 +8,13 @@
|
||||
>> Second Quote
|
||||
>>> Third one, yuhuu!
|
||||
|
||||
`can a code have **markdown?**` so go it doesn't
|
||||
`can a code have **markdown?**` so good it doesn't
|
||||
|
||||
<h1>Yo!
|
||||
Omg
|
||||
|
||||
ddffdg
|
||||
</h1>
|
||||
|
||||
|
||||
## Unordered list
|
||||
@ -17,6 +23,9 @@
|
||||
* second
|
||||
* * second first
|
||||
* * second __second__ jks8feif fdsuif yuweru sdfoisdfu wutwe iower wtew ruweir weoir wutywr wer woeirwr wieyriow eryowe rwyeor oweryower o
|
||||
* * * hm, is it actually a thing?
|
||||
* * * * and this?!
|
||||
* * * * * omg
|
||||
* third `and some code`
|
||||
|
||||
|
||||
@ -59,5 +68,4 @@ To compare<sub>~~13~~</sub>
|
||||
|
||||
<font color="#FF0000">RED</font>
|
||||
|
||||
**PS** additional text to check if this view scrolls gracefully, sofihweo fwfw fuwf weyf pwefiowef twe weuifphw efwepfuwoefh wfypiwe fuwoef wiefg wtefw uf ywfyw fweouf wpfyw fwfe# Hello!
|
||||
|
||||
**PS** additional text to check if this view scrolls gracefully, sofihweo fwfw fuwf weyf pwefiowef twe weuifphw efwepfuwoefh wfypiwe fuwoef wiefg wtefw uf ywfyw fweouf wpfyw fwfe#
|
@ -1,26 +1,26 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.Parser;
|
||||
import ru.noties.debug.AndroidLogDebugOutput;
|
||||
import ru.noties.debug.Debug;
|
||||
import ru.noties.markwon.spans.DrawableSpanUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
import ru.noties.debug.AndroidLogDebugOutput;
|
||||
import ru.noties.debug.Debug;
|
||||
import ru.noties.markwon.renderer.*;
|
||||
import ru.noties.markwon.spans.DrawableSpanUtils;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
static {
|
||||
Debug.init(new AndroidLogDebugOutput(true));
|
||||
@ -62,21 +62,28 @@ public class MainActivity extends AppCompatActivity {
|
||||
.extensions(Arrays.asList(StrikethroughExtension.create()))
|
||||
.build();
|
||||
final Node node = parser.parse(md);
|
||||
final CharSequence text = new SpannableRenderer()._render(node/*, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
textView.setText(textView.getText());
|
||||
final Drawable drawable = null;
|
||||
drawable.setCallback(textView);
|
||||
}
|
||||
}*/);
|
||||
|
||||
final CharSequence text = new ru.noties.markwon.renderer.SpannableRenderer().render(
|
||||
SpannableConfiguration.create(MainActivity.this),
|
||||
node
|
||||
);
|
||||
|
||||
// final CharSequence text = new SpannableRenderer()._render(node/*, new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// textView.setText(textView.getText());
|
||||
// final Drawable drawable = null;
|
||||
// drawable.setCallback(textView);
|
||||
// }
|
||||
// }*/);
|
||||
final long end = SystemClock.uptimeMillis();
|
||||
Debug.i("Rendered: %d ms, length: %d", end - start, text.length());
|
||||
// Debug.i(text);
|
||||
textView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// NB! LinkMovementMethod forces frequent updates...
|
||||
// textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
textView.setText(text);
|
||||
DrawableSpanUtils.scheduleDrawables(textView);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
public class Markwon {
|
||||
|
||||
// todo, annotation processor to PRE_COMPILE markdown!! no... multiple lnguages and you are out, forget about it
|
||||
// view for debugging (to view in preview) x3!
|
||||
}
|
||||
//package ru.noties.markwon;
|
||||
//
|
||||
//public class Markwon {
|
||||
//
|
||||
// // todo, annotation processor to PRE_COMPILE markdown!! no... multiple lnguages and you are out, forget about it
|
||||
// // view for debugging (to view in preview) x3!
|
||||
//}
|
||||
|
@ -1,442 +1,444 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.AbsoluteSizeSpan;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
import android.text.style.URLSpan;
|
||||
|
||||
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
|
||||
import org.commonmark.node.AbstractVisitor;
|
||||
import org.commonmark.node.BlockQuote;
|
||||
import org.commonmark.node.BulletList;
|
||||
import org.commonmark.node.Code;
|
||||
import org.commonmark.node.CustomBlock;
|
||||
import org.commonmark.node.CustomNode;
|
||||
import org.commonmark.node.Document;
|
||||
import org.commonmark.node.Emphasis;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.node.HardLineBreak;
|
||||
import org.commonmark.node.Heading;
|
||||
import org.commonmark.node.HtmlBlock;
|
||||
import org.commonmark.node.HtmlInline;
|
||||
import org.commonmark.node.Image;
|
||||
import org.commonmark.node.IndentedCodeBlock;
|
||||
import org.commonmark.node.Link;
|
||||
import org.commonmark.node.ListItem;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.OrderedList;
|
||||
import org.commonmark.node.Paragraph;
|
||||
import org.commonmark.node.SoftLineBreak;
|
||||
import org.commonmark.node.StrongEmphasis;
|
||||
import org.commonmark.node.Text;
|
||||
import org.commonmark.node.ThematicBreak;
|
||||
import org.commonmark.renderer.Renderer;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
|
||||
import ru.noties.debug.Debug;
|
||||
import ru.noties.markwon.spans.BlockQuoteSpan;
|
||||
import ru.noties.markwon.spans.CodeSpan;
|
||||
import ru.noties.markwon.spans.DrawableSpan;
|
||||
import ru.noties.markwon.spans.EmphasisSpan;
|
||||
import ru.noties.markwon.spans.ListItemSpan;
|
||||
import ru.noties.markwon.spans.StrongEmphasisSpan;
|
||||
import ru.noties.markwon.spans.SubSpan;
|
||||
import ru.noties.markwon.spans.SupSpan;
|
||||
import ru.noties.markwon.spans.ThematicBreakSpan;
|
||||
|
||||
public class SpannableRenderer implements Renderer {
|
||||
|
||||
// todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc)
|
||||
|
||||
@Override
|
||||
public void render(Node node, Appendable output) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String render(Node node) {
|
||||
// hm.. doesn't make sense to render to string
|
||||
throw null;
|
||||
}
|
||||
|
||||
public CharSequence _render(Node node) {
|
||||
final SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
node.accept(new SpannableNodeRenderer(builder));
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static class SpannableNodeRenderer extends AbstractVisitor {
|
||||
|
||||
// private static final float[] HEADING_SIZES = {
|
||||
// 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
|
||||
// };
|
||||
|
||||
private final SpannableStringBuilder builder;
|
||||
|
||||
private int blockQuoteIndent;
|
||||
private int listLevel;
|
||||
|
||||
SpannableNodeRenderer(SpannableStringBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HardLineBreak hardLineBreak) {
|
||||
Debug.i(hardLineBreak);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Text text) {
|
||||
Debug.i(text);
|
||||
builder.append(text.getLiteral());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(StrongEmphasis strongEmphasis) {
|
||||
final int length = builder.length();
|
||||
visitChildren(strongEmphasis);
|
||||
builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Emphasis emphasis) {
|
||||
final int length = builder.length();
|
||||
visitChildren(emphasis);
|
||||
builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(IndentedCodeBlock indentedCodeBlock) {
|
||||
Debug.i(indentedCodeBlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BlockQuote blockQuote) {
|
||||
builder.append('\n');
|
||||
final int length = builder.length();
|
||||
blockQuoteIndent += 1;
|
||||
visitChildren(blockQuote);
|
||||
builder.setSpan(new BlockQuoteSpan(blockQuoteIndent), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
blockQuoteIndent -= 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Code code) {
|
||||
final int length = builder.length();
|
||||
builder.append(code.getLiteral());
|
||||
// builder.setSpan(new ForegroundColorSpan(0xff00ff00), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
builder.setSpan(new CodeSpan(false, length, builder.length()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BulletList bulletList) {
|
||||
Debug.i(bulletList, bulletList.getBulletMarker());
|
||||
visitChildren(bulletList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ListItem listItem) {
|
||||
Debug.i(listItem);
|
||||
//package ru.noties.markwon;
|
||||
//
|
||||
//import android.graphics.Canvas;
|
||||
//import android.graphics.ColorFilter;
|
||||
//import android.graphics.drawable.Drawable;
|
||||
//import android.os.Handler;
|
||||
//import android.os.Looper;
|
||||
//import android.support.annotation.IntRange;
|
||||
//import android.support.annotation.NonNull;
|
||||
//import android.support.annotation.Nullable;
|
||||
//import android.text.Html;
|
||||
//import android.text.SpannableStringBuilder;
|
||||
//import android.text.Spanned;
|
||||
//import android.text.style.AbsoluteSizeSpan;
|
||||
//import android.text.style.StrikethroughSpan;
|
||||
//import android.text.style.URLSpan;
|
||||
//
|
||||
//import org.commonmark.ext.gfm.strikethrough.Strikethrough;
|
||||
//import org.commonmark.node.AbstractVisitor;
|
||||
//import org.commonmark.node.BlockQuote;
|
||||
//import org.commonmark.node.BulletList;
|
||||
//import org.commonmark.node.Code;
|
||||
//import org.commonmark.node.CustomBlock;
|
||||
//import org.commonmark.node.CustomNode;
|
||||
//import org.commonmark.node.Document;
|
||||
//import org.commonmark.node.Emphasis;
|
||||
//import org.commonmark.node.FencedCodeBlock;
|
||||
//import org.commonmark.node.HardLineBreak;
|
||||
//import org.commonmark.node.Heading;
|
||||
//import org.commonmark.node.HtmlBlock;
|
||||
//import org.commonmark.node.HtmlInline;
|
||||
//import org.commonmark.node.Image;
|
||||
//import org.commonmark.node.IndentedCodeBlock;
|
||||
//import org.commonmark.node.Link;
|
||||
//import org.commonmark.node.ListItem;
|
||||
//import org.commonmark.node.Node;
|
||||
//import org.commonmark.node.OrderedList;
|
||||
//import org.commonmark.node.Paragraph;
|
||||
//import org.commonmark.node.SoftLineBreak;
|
||||
//import org.commonmark.node.StrongEmphasis;
|
||||
//import org.commonmark.node.Text;
|
||||
//import org.commonmark.node.ThematicBreak;
|
||||
//import org.commonmark.renderer.Renderer;
|
||||
//
|
||||
//import java.util.ArrayDeque;
|
||||
//import java.util.Arrays;
|
||||
//import java.util.Deque;
|
||||
//
|
||||
//import ru.noties.debug.Debug;
|
||||
//import ru.noties.markwon.spans.BlockQuoteSpan;
|
||||
//import ru.noties.markwon.spans.CodeSpan;
|
||||
//import ru.noties.markwon.spans.DrawableSpan;
|
||||
//import ru.noties.markwon.spans.EmphasisSpan;
|
||||
//import ru.noties.markwon.spans.BulletListItemSpan;
|
||||
//import ru.noties.markwon.spans.StrongEmphasisSpan;
|
||||
//import ru.noties.markwon.spans.SubSpan;
|
||||
//import ru.noties.markwon.spans.SupSpan;
|
||||
//import ru.noties.markwon.spans.ThematicBreakSpan;
|
||||
//
|
||||
//public class SpannableRenderer implements Renderer {
|
||||
//
|
||||
// // todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc)
|
||||
//
|
||||
// @Override
|
||||
// public void render(Node node, Appendable output) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public String render(Node node) {
|
||||
// // hm.. doesn't make sense to render to string
|
||||
// throw null;
|
||||
// }
|
||||
//
|
||||
// public CharSequence _render(Node node) {
|
||||
// final SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
// node.accept(new SpannableNodeRenderer(builder));
|
||||
// return builder;
|
||||
// }
|
||||
//
|
||||
// private static class SpannableNodeRenderer extends AbstractVisitor {
|
||||
//
|
||||
//// private static final float[] HEADING_SIZES = {
|
||||
//// 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
|
||||
//// };
|
||||
//
|
||||
// private final SpannableStringBuilder builder;
|
||||
//
|
||||
// private int blockQuoteIndent;
|
||||
// private int listLevel;
|
||||
//
|
||||
// SpannableNodeRenderer(SpannableStringBuilder builder) {
|
||||
// this.builder = builder;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(HardLineBreak hardLineBreak) {
|
||||
// // todo
|
||||
// Debug.i(hardLineBreak);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(Text text) {
|
||||
// builder.append(text.getLiteral());
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(StrongEmphasis strongEmphasis) {
|
||||
// final int length = builder.length();
|
||||
// visitChildren(strongEmphasis);
|
||||
// builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(Emphasis emphasis) {
|
||||
// final int length = builder.length();
|
||||
// visitChildren(emphasis);
|
||||
// builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(IndentedCodeBlock indentedCodeBlock) {
|
||||
// // todo
|
||||
// Debug.i(indentedCodeBlock);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(BlockQuote blockQuote) {
|
||||
// builder.append('\n');
|
||||
if (builder.charAt(builder.length() - 1) != '\n') {
|
||||
builder.append('\n');
|
||||
}
|
||||
final int length = builder.length();
|
||||
blockQuoteIndent += 1;
|
||||
listLevel += 1;
|
||||
visitChildren(listItem);
|
||||
// builder.setSpan(new BulletSpan(4, 0xff0000ff), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
builder.setSpan(new ListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
blockQuoteIndent -= 1;
|
||||
listLevel -= 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ThematicBreak thematicBreak) {
|
||||
final int length = builder.length();
|
||||
builder.append('\n')
|
||||
.append(' '); // without space it won't render
|
||||
builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(OrderedList orderedList) {
|
||||
Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber());
|
||||
// todo, ordering numbers
|
||||
super.visit(orderedList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(SoftLineBreak softLineBreak) {
|
||||
Debug.i(softLineBreak);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Heading heading) {
|
||||
Debug.i(heading);
|
||||
if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') {
|
||||
builder.append('\n');
|
||||
}
|
||||
final int length = builder.length();
|
||||
visitChildren(heading);
|
||||
final int max = 120;
|
||||
final int one = 20; // total is 6
|
||||
final int size = max - ((heading.getLevel() - 1) * one);
|
||||
builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(FencedCodeBlock fencedCodeBlock) {
|
||||
builder.append('\n');
|
||||
final int length = builder.length();
|
||||
builder.append(fencedCodeBlock.getLiteral());
|
||||
builder.setSpan(new CodeSpan(true, length, builder.length() - 1), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Paragraph paragraph) {
|
||||
Debug.i(paragraph);
|
||||
if (listLevel == 0
|
||||
&& blockQuoteIndent == 0) {
|
||||
builder.append('\n')
|
||||
.append('\n');
|
||||
}
|
||||
visitChildren(paragraph);
|
||||
|
||||
if (listLevel == 0
|
||||
&& blockQuoteIndent == 0) {
|
||||
builder.append('\n')
|
||||
.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// private int htmlStart = -1;
|
||||
private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>();
|
||||
|
||||
private static class HtmlInlineItem {
|
||||
|
||||
final int start;
|
||||
final String tag;
|
||||
|
||||
private HtmlInlineItem(int start, String tag) {
|
||||
this.start = start;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HtmlInline htmlInline) {
|
||||
|
||||
// Debug.i(htmlInline, htmlStart);
|
||||
// Debug.i(htmlInline.getLiteral(), htmlInline.toString());
|
||||
|
||||
// okay, it's seems that we desperately need to understand if it's opening tag or closing
|
||||
|
||||
final HtmlTag tag = parseTag(htmlInline.getLiteral());
|
||||
|
||||
Debug.i(htmlInline.getLiteral(), tag);
|
||||
|
||||
if (tag != null) {
|
||||
Debug.i("tag: %s, closing: %s", tag.tag, tag.closing);
|
||||
if (!tag.closing) {
|
||||
htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag));
|
||||
visitChildren(htmlInline);
|
||||
} else {
|
||||
final HtmlInlineItem item = htmlStack.pop();
|
||||
final int start = item.start;
|
||||
final int end = builder.length();
|
||||
// here, additionally, we can render some tags ourselves (sup/sub)
|
||||
if ("sup".equals(item.tag)) {
|
||||
builder.setSpan(new SupSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
} else if("sub".equals(item.tag)) {
|
||||
builder.setSpan(new SubSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
} else if("del".equals(item.tag)) {
|
||||
// weird, but `Html` class does not return a spannable for `<del>o</del>`
|
||||
// seems like a bug
|
||||
builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
} else {
|
||||
final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">";
|
||||
final Spanned spanned = Html.fromHtml(html);
|
||||
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
|
||||
|
||||
Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans));
|
||||
|
||||
if (spans != null
|
||||
&& spans.length > 0) {
|
||||
for (Object span: spans) {
|
||||
Debug.i(span);
|
||||
builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.visit(htmlInline);
|
||||
}
|
||||
}
|
||||
|
||||
private static class HtmlTag {
|
||||
final String tag;
|
||||
final boolean closing;
|
||||
HtmlTag(String tag, boolean closing) {
|
||||
this.tag = tag;
|
||||
this.closing = closing;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HtmlTag{" +
|
||||
"tag='" + tag + '\'' +
|
||||
", closing=" + closing +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
private static HtmlTag parseTag(String in) {
|
||||
|
||||
final HtmlTag out;
|
||||
|
||||
final int length = in != null
|
||||
? in.length()
|
||||
: 0;
|
||||
|
||||
Debug.i(in, length);
|
||||
|
||||
if (length == 0 || length < 3) {
|
||||
out = null;
|
||||
} else {
|
||||
|
||||
final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1);
|
||||
final String tag = closing
|
||||
? in.substring(2, in.length() - 1)
|
||||
: in.substring(1, in.length() - 1);
|
||||
out = new HtmlTag(tag, closing);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HtmlBlock htmlBlock) {
|
||||
// interestring thing... what is it also?
|
||||
Debug.i(htmlBlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CustomBlock customBlock) {
|
||||
// not supported, what is it anyway?
|
||||
Debug.i(customBlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Document document) {
|
||||
// the whole document, no need to do anything
|
||||
Debug.i(document);
|
||||
super.visit(document);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Link link) {
|
||||
Debug.i(link);
|
||||
final int length = builder.length();
|
||||
visitChildren(link);
|
||||
builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Image image) {
|
||||
// not supported... maybe for now?
|
||||
Debug.i(image);
|
||||
super.visit(image);
|
||||
|
||||
final int length = builder.length();
|
||||
final TestDrawable drawable = new TestDrawable();
|
||||
final DrawableSpan span = new DrawableSpan(drawable);
|
||||
builder.append(" ");
|
||||
builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CustomNode customNode) {
|
||||
|
||||
Debug.i(customNode);
|
||||
|
||||
if (customNode instanceof Strikethrough) {
|
||||
final int length = builder.length();
|
||||
visitChildren(customNode);
|
||||
builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
} else {
|
||||
super.visit(customNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestDrawable extends Drawable {
|
||||
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private boolean called;
|
||||
|
||||
TestDrawable() {
|
||||
setBounds(0, 0, 50, 50);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull final Canvas canvas) {
|
||||
canvas.clipRect(getBounds());
|
||||
if (!called) {
|
||||
canvas.drawColor(0xFF00ff00);
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
called = true;
|
||||
setBounds(0, 0, 400, 400);
|
||||
invalidateSelf();
|
||||
}
|
||||
}, 2000L);
|
||||
} else {
|
||||
canvas.drawColor(0xFFff0000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return called ? 400 : 50;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return called ? 400 : 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
// final int length = builder.length();
|
||||
// blockQuoteIndent += 1;
|
||||
// visitChildren(blockQuote);
|
||||
// builder.setSpan(new BlockQuoteSpan(blockQuoteIndent), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// blockQuoteIndent -= 1;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(Code code) {
|
||||
// final int length = builder.length();
|
||||
// builder.append(code.getLiteral());
|
||||
//// builder.setSpan(new ForegroundColorSpan(0xff00ff00), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// builder.setSpan(new CodeSpan(false, length, builder.length()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(BulletList bulletList) {
|
||||
// Debug.i(bulletList, bulletList.getBulletMarker());
|
||||
// visitChildren(bulletList);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(ListItem listItem) {
|
||||
// Debug.i(listItem);
|
||||
//// builder.append('\n');
|
||||
// if (builder.charAt(builder.length() - 1) != '\n') {
|
||||
// builder.append('\n');
|
||||
// }
|
||||
// final int length = builder.length();
|
||||
// blockQuoteIndent += 1;
|
||||
// listLevel += 1;
|
||||
// visitChildren(listItem);
|
||||
//// builder.setSpan(new BulletSpan(4, 0xff0000ff), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// builder.setSpan(new BulletListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// blockQuoteIndent -= 1;
|
||||
// listLevel -= 1;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(ThematicBreak thematicBreak) {
|
||||
// final int length = builder.length();
|
||||
// builder.append('\n')
|
||||
// .append(' '); // without space it won't render
|
||||
// builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// builder.append('\n');
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(OrderedList orderedList) {
|
||||
// Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber());
|
||||
// // todo, ordering numbers
|
||||
// super.visit(orderedList);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(SoftLineBreak softLineBreak) {
|
||||
// Debug.i(softLineBreak);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(Heading heading) {
|
||||
// Debug.i(heading);
|
||||
// if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') {
|
||||
// builder.append('\n');
|
||||
// }
|
||||
// final int length = builder.length();
|
||||
// visitChildren(heading);
|
||||
// final int max = 120;
|
||||
// final int one = 20; // total is 6
|
||||
// final int size = max - ((heading.getLevel() - 1) * one);
|
||||
// builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// builder.append('\n');
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(FencedCodeBlock fencedCodeBlock) {
|
||||
// builder.append('\n');
|
||||
// final int length = builder.length();
|
||||
// builder.append(fencedCodeBlock.getLiteral());
|
||||
// builder.setSpan(new CodeSpan(true, length, builder.length() - 1), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(Paragraph paragraph) {
|
||||
// Debug.i(paragraph);
|
||||
// if (listLevel == 0
|
||||
// && blockQuoteIndent == 0) {
|
||||
// builder.append('\n')
|
||||
// .append('\n');
|
||||
// }
|
||||
// visitChildren(paragraph);
|
||||
//
|
||||
// if (listLevel == 0
|
||||
// && blockQuoteIndent == 0) {
|
||||
// builder.append('\n')
|
||||
// .append('\n');
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//// private int htmlStart = -1;
|
||||
// private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>();
|
||||
//
|
||||
// private static class HtmlInlineItem {
|
||||
//
|
||||
// final int start;
|
||||
// final String tag;
|
||||
//
|
||||
// private HtmlInlineItem(int start, String tag) {
|
||||
// this.start = start;
|
||||
// this.tag = tag;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(HtmlInline htmlInline) {
|
||||
//
|
||||
//// Debug.i(htmlInline, htmlStart);
|
||||
//// Debug.i(htmlInline.getLiteral(), htmlInline.toString());
|
||||
//
|
||||
// // okay, it's seems that we desperately need to understand if it's opening tag or closing
|
||||
//
|
||||
// final HtmlTag tag = parseTag(htmlInline.getLiteral());
|
||||
//
|
||||
// Debug.i(htmlInline.getLiteral(), tag);
|
||||
//
|
||||
// if (tag != null) {
|
||||
// Debug.i("tag: %s, closing: %s", tag.tag, tag.closing);
|
||||
// if (!tag.closing) {
|
||||
// htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag));
|
||||
// visitChildren(htmlInline);
|
||||
// } else {
|
||||
// final HtmlInlineItem item = htmlStack.pop();
|
||||
// final int start = item.start;
|
||||
// final int end = builder.length();
|
||||
// // here, additionally, we can render some tags ourselves (sup/sub)
|
||||
// if ("sup".equals(item.tag)) {
|
||||
// builder.setSpan(new SupSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// } else if("sub".equals(item.tag)) {
|
||||
// builder.setSpan(new SubSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// } else if("del".equals(item.tag)) {
|
||||
// // weird, but `Html` class does not return a spannable for `<del>o</del>`
|
||||
// // seems like a bug
|
||||
// builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// } else {
|
||||
// final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">";
|
||||
// final Spanned spanned = Html.fromHtml(html);
|
||||
// final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
|
||||
//
|
||||
// Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans));
|
||||
//
|
||||
// if (spans != null
|
||||
// && spans.length > 0) {
|
||||
// for (Object span: spans) {
|
||||
// Debug.i(span);
|
||||
// builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// super.visit(htmlInline);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private static class HtmlTag {
|
||||
// final String tag;
|
||||
// final boolean closing;
|
||||
// HtmlTag(String tag, boolean closing) {
|
||||
// this.tag = tag;
|
||||
// this.closing = closing;
|
||||
// }
|
||||
// @Override
|
||||
// public String toString() {
|
||||
// return "HtmlTag{" +
|
||||
// "tag='" + tag + '\'' +
|
||||
// ", closing=" + closing +
|
||||
// '}';
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private static HtmlTag parseTag(String in) {
|
||||
//
|
||||
// final HtmlTag out;
|
||||
//
|
||||
// final int length = in != null
|
||||
// ? in.length()
|
||||
// : 0;
|
||||
//
|
||||
// Debug.i(in, length);
|
||||
//
|
||||
// if (length == 0 || length < 3) {
|
||||
// out = null;
|
||||
// } else {
|
||||
//
|
||||
// final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1);
|
||||
// final String tag = closing
|
||||
// ? in.substring(2, in.length() - 1)
|
||||
// : in.substring(1, in.length() - 1);
|
||||
// out = new HtmlTag(tag, closing);
|
||||
// }
|
||||
//
|
||||
// return out;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(HtmlBlock htmlBlock) {
|
||||
// // interestring thing... what is it also?
|
||||
// Debug.i(htmlBlock);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(CustomBlock customBlock) {
|
||||
// // not supported, what is it anyway?
|
||||
// Debug.i(customBlock);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(Document document) {
|
||||
// // the whole document, no need to do anything
|
||||
// Debug.i(document);
|
||||
// super.visit(document);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(Link link) {
|
||||
// Debug.i(link);
|
||||
// final int length = builder.length();
|
||||
// visitChildren(link);
|
||||
// builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(Image image) {
|
||||
// // not supported... maybe for now?
|
||||
// Debug.i(image);
|
||||
// final int length = builder.length();
|
||||
// super.visit(image);
|
||||
//
|
||||
//// final int length = builder.length();
|
||||
// final TestDrawable drawable = new TestDrawable();
|
||||
// final DrawableSpan span = new DrawableSpan(drawable);
|
||||
// builder.append(" ");
|
||||
// builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void visit(CustomNode customNode) {
|
||||
//
|
||||
// Debug.i(customNode);
|
||||
//
|
||||
// if (customNode instanceof Strikethrough) {
|
||||
// final int length = builder.length();
|
||||
// visitChildren(customNode);
|
||||
// builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// } else {
|
||||
// super.visit(customNode);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private static class TestDrawable extends Drawable {
|
||||
//
|
||||
// private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
// private boolean called;
|
||||
//
|
||||
// TestDrawable() {
|
||||
// setBounds(0, 0, 50, 50);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void draw(@NonNull final Canvas canvas) {
|
||||
// canvas.clipRect(getBounds());
|
||||
// if (!called) {
|
||||
// canvas.drawColor(0xFF00ff00);
|
||||
// handler.removeCallbacksAndMessages(null);
|
||||
// handler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// called = true;
|
||||
// setBounds(0, 0, 400, 400);
|
||||
// invalidateSelf();
|
||||
// }
|
||||
// }, 2000L);
|
||||
// } else {
|
||||
// canvas.drawColor(0xFFff0000);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public int getOpacity() {
|
||||
// return 0;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public int getIntrinsicWidth() {
|
||||
// return called ? 400 : 50;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public int getIntrinsicHeight() {
|
||||
// return called ? 400 : 50;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon.spans;
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon.spans;
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
@ -6,12 +6,6 @@ import android.graphics.Rect;
|
||||
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.LeadingMarginSpan;
|
||||
import android.text.style.LineHeightSpan;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
import android.text.style.ReplacementSpan;
|
||||
|
||||
import ru.noties.debug.Debug;
|
||||
@ -24,10 +18,16 @@ public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ {
|
||||
private final int start;
|
||||
private final int end;
|
||||
|
||||
private final Rect rect = new Rect();
|
||||
private final Rect borderRect = new Rect();
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
public CodeSpan(boolean multiline, int start, int end) {
|
||||
this.multiline = multiline;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -74,33 +74,104 @@ public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ {
|
||||
@NonNull Paint paint
|
||||
) {
|
||||
|
||||
Debug.i("text: %s, x: %s, top: %s, y: %s, bottom: %s", text.subSequence(start, end), x, top, y, bottom);
|
||||
|
||||
final CharSequence cs = text.subSequence(start, end);
|
||||
|
||||
final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F);
|
||||
|
||||
final int left = (int) (x + .5F);
|
||||
final int right = multiline
|
||||
? canvas.getWidth()
|
||||
: left + width;
|
||||
|
||||
final Rect rect = new Rect(
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom
|
||||
);
|
||||
final int right;
|
||||
if (multiline) {
|
||||
right = canvas.getWidth();
|
||||
} else {
|
||||
final int width = (16 * 2) + (int) (paint.measureText(text, start, end) + .5F);
|
||||
right = left + width;
|
||||
}
|
||||
|
||||
final Paint p = new Paint();
|
||||
p.setStyle(Paint.Style.FILL);
|
||||
p.setColor(0x80ff0000);
|
||||
canvas.drawRect(rect, p);
|
||||
rect.set(left, top, right, bottom);
|
||||
|
||||
|
||||
// okay, draw background first
|
||||
drawBackground(canvas);
|
||||
|
||||
// then, if any, draw borders
|
||||
drawBorders(canvas, this.start == start, this.end == end);
|
||||
|
||||
// draw text
|
||||
// y center position
|
||||
final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
|
||||
p.setColor(0xFF000000);
|
||||
canvas.drawText(cs, 0, cs.length(), x + 16, b, paint);
|
||||
// if (config.textColor != 0) {
|
||||
// // we will use Paint object that is used to draw all the text (textSize, textColor, typeface, etc)
|
||||
// paint.setColor(config.textColor);
|
||||
// }
|
||||
canvas.drawText(text, start, end, x + 16, b, paint);
|
||||
|
||||
|
||||
// Debug.i("text: %s, x: %s, top: %s, y: %s, bottom: %s", text.subSequence(start, end), x, top, y, bottom);
|
||||
//
|
||||
// final CharSequence cs = text.subSequence(start, end);
|
||||
//
|
||||
// final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F);
|
||||
//
|
||||
// final int left = (int) (x + .5F);
|
||||
// final int right = multiline
|
||||
// ? canvas.getWidth()
|
||||
// : left + width;
|
||||
//
|
||||
// final Rect rect = new Rect(
|
||||
// left,
|
||||
// top,
|
||||
// right,
|
||||
// bottom
|
||||
// );
|
||||
//
|
||||
// final Paint p = new Paint();
|
||||
// p.setStyle(Paint.Style.FILL);
|
||||
// p.setColor(0x80ff0000);
|
||||
// canvas.drawRect(rect, p);
|
||||
//
|
||||
// // y center position
|
||||
// final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
|
||||
// p.setColor(0xFF000000);
|
||||
// canvas.drawText(cs, 0, cs.length(), x + 16, b, paint);
|
||||
}
|
||||
|
||||
private void drawBackground(Canvas canvas) {
|
||||
// final int color = config.backgroundColor;
|
||||
// if (color != 0) {
|
||||
paint.setColor(0x40ff0000);
|
||||
canvas.drawRect(rect, paint);
|
||||
// }
|
||||
}
|
||||
|
||||
private void drawBorders(Canvas canvas, boolean top, boolean bottom) {
|
||||
|
||||
final int color = 0xFFff0000;
|
||||
final int width = 4;
|
||||
// if (color == 0
|
||||
// || width == 0) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
paint.setColor(color);
|
||||
|
||||
// left and right are always drawn
|
||||
|
||||
// LEFT
|
||||
borderRect.set(rect.left, rect.top, rect.left + width, rect.bottom);
|
||||
canvas.drawRect(borderRect, paint);
|
||||
|
||||
// RIGHT
|
||||
borderRect.set(rect.right - width, rect.top, rect.right, rect.bottom);
|
||||
canvas.drawRect(borderRect, paint);
|
||||
|
||||
// TOP
|
||||
if (top) {
|
||||
borderRect.set(rect.left, rect.top, rect.right, rect.top + width);
|
||||
canvas.drawRect(borderRect, paint);
|
||||
}
|
||||
|
||||
// BOTTOM
|
||||
if (bottom) {
|
||||
borderRect.set(rect.left, rect.bottom - width, rect.right, rect.bottom);
|
||||
canvas.drawRect(borderRect, paint);
|
||||
}
|
||||
}
|
||||
|
||||
|
97
app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java
Normal file
97
app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java
Normal file
@ -0,0 +1,97 @@
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.style.ReplacementSpan;
|
||||
|
||||
public class DrawableSpan extends ReplacementSpan {
|
||||
|
||||
@IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER })
|
||||
@interface Alignment {}
|
||||
|
||||
public static final int ALIGN_BOTTOM = 0;
|
||||
public static final int ALIGN_BASELINE = 1;
|
||||
public static final int ALIGN_CENTER = 2;
|
||||
|
||||
private final Drawable drawable;
|
||||
private final int alignment;
|
||||
|
||||
public DrawableSpan(@NonNull Drawable drawable) {
|
||||
this(drawable, ALIGN_BOTTOM);
|
||||
}
|
||||
|
||||
public DrawableSpan(@NonNull Drawable drawable, @Alignment int alignment) {
|
||||
this.drawable = drawable;
|
||||
this.alignment = alignment;
|
||||
|
||||
// additionally set intrinsic bounds if empty
|
||||
final Rect rect = drawable.getBounds();
|
||||
if (rect.isEmpty()) {
|
||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(
|
||||
@NonNull Paint paint,
|
||||
CharSequence text,
|
||||
@IntRange(from = 0) int start,
|
||||
@IntRange(from = 0) int end,
|
||||
@Nullable Paint.FontMetricsInt fm) {
|
||||
|
||||
final Rect rect = drawable.getBounds();
|
||||
|
||||
if (fm != null) {
|
||||
fm.ascent = -rect.bottom;
|
||||
fm.descent = 0;
|
||||
|
||||
fm.top = fm.ascent;
|
||||
fm.bottom = 0;
|
||||
}
|
||||
|
||||
return rect.right;
|
||||
}
|
||||
|
||||
@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) {
|
||||
|
||||
final Drawable drawable = this.drawable;
|
||||
|
||||
final int b = bottom - drawable.getBounds().bottom;
|
||||
|
||||
final int save = canvas.save();
|
||||
try {
|
||||
final int translationY;
|
||||
if (ALIGN_CENTER == alignment) {
|
||||
translationY = (int) (b / 2.F + .5F);
|
||||
} else if (ALIGN_BASELINE == alignment) {
|
||||
translationY = b - paint.getFontMetricsInt().descent;
|
||||
} else {
|
||||
translationY = b;
|
||||
}
|
||||
canvas.translate(x, translationY);
|
||||
drawable.draw(canvas);
|
||||
} finally {
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable getDrawable() {
|
||||
return drawable;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon.spans;
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
17
app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java
Normal file
17
app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java
Normal file
@ -0,0 +1,17 @@
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
public class EmphasisSpan extends MetricAffectingSpan {
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(TextPaint p) {
|
||||
p.setTextSkewX(-0.25f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setTextSkewX(-0.25f);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon.spans;
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
@ -0,0 +1,17 @@
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
public class StrongEmphasisSpan extends MetricAffectingSpan {
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(TextPaint p) {
|
||||
p.setFakeBoldText(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setFakeBoldText(true);
|
||||
}
|
||||
}
|
19
app/src/main/java/ru/noties/markwon/spans2/SubSpan.java
Normal file
19
app/src/main/java/ru/noties/markwon/spans2/SubSpan.java
Normal file
@ -0,0 +1,19 @@
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
public class SubSpan extends MetricAffectingSpan {
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setTextSize(tp.getTextSize() * .75F);
|
||||
tp.baselineShift -= (int) (tp.ascent() / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(TextPaint tp) {
|
||||
tp.setTextSize(tp.getTextSize() * .75F);
|
||||
tp.baselineShift -= (int) (tp.ascent() / 2);
|
||||
}
|
||||
}
|
19
app/src/main/java/ru/noties/markwon/spans2/SupSpan.java
Normal file
19
app/src/main/java/ru/noties/markwon/spans2/SupSpan.java
Normal file
@ -0,0 +1,19 @@
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
public class SupSpan extends MetricAffectingSpan {
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setTextSize(tp.getTextSize() * .75F);
|
||||
tp.baselineShift += (int) (tp.ascent() / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(TextPaint tp) {
|
||||
tp.setTextSize(tp.getTextSize() * .75F);
|
||||
tp.baselineShift += (int) (tp.ascent() / 2);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package ru.noties.markwon.spans2;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.text.Layout;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
|
||||
public class ThematicBreakSpan implements LeadingMarginSpan {
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||
final int middle = (bottom - top) / 2;
|
||||
final Rect rect = new Rect(0, top + middle - 2, c.getWidth(), top + middle + 2);
|
||||
final Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(0x80000000);
|
||||
c.drawRect(rect, paint);
|
||||
}
|
||||
}
|
@ -2,16 +2,18 @@
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/activity_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dip"
|
||||
android:textSize="18sp"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:textColor="@color/colorPrimaryDark"
|
||||
tools:context="ru.noties.markwon.MainActivity"
|
||||
tools:text="yo\nman"/>
|
||||
tools:text="yo\nman" />
|
||||
|
||||
</ScrollView>
|
||||
|
10
app/src/main/res/values-v21/styles.xml
Normal file
10
app/src/main/res/values-v21/styles.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="AppThemeBase" parent="android:Theme.Material.Light">
|
||||
<item name="android:colorAccent">@color/colorAccent</item>
|
||||
<item name="android:colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -1,6 +0,0 @@
|
||||
<resources>
|
||||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||
(such as screen margins) for screens with more than 820dp of available width. This
|
||||
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
|
||||
<dimen name="activity_horizontal_margin">64dp</dimen>
|
||||
</resources>
|
@ -1,11 +1,8 @@
|
||||
<resources>
|
||||
|
||||
<style name="AppThemeBase" parent="android:Theme.Holo.Light"/>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
<style name="AppTheme" parent="AppThemeBase"/>
|
||||
|
||||
</resources>
|
||||
|
@ -1,14 +1,9 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,4 +28,8 @@ ext {
|
||||
// Dependencies
|
||||
final def supportVersion = '25.3.1'
|
||||
SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion"
|
||||
|
||||
final def commonMarkVersion = '0.9.0'
|
||||
COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion"
|
||||
COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion"
|
||||
}
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip
|
||||
|
26
library-renderer/build.gradle
Normal file
26
library-renderer/build.gradle
Normal file
@ -0,0 +1,26 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion TARGET_SDK
|
||||
buildToolsVersion BUILD_TOOLS
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion MIN_SDK
|
||||
targetSdkVersion TARGET_SDK
|
||||
versionCode 1
|
||||
versionName version
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
compile project(':library-spans')
|
||||
|
||||
compile SUPPORT_ANNOTATIONS
|
||||
compile COMMON_MARK
|
||||
compile COMMON_MARK_STRIKETHROUGHT
|
||||
|
||||
// todo, debugging only
|
||||
compile 'ru.noties:debug:3.0.0@jar'
|
||||
}
|
1
library-renderer/src/main/AndroidManifest.xml
Normal file
1
library-renderer/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="ru.noties.markwon.renderer" />
|
@ -0,0 +1,109 @@
|
||||
package ru.noties.markwon.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import ru.noties.markwon.spans.BlockQuoteSpan;
|
||||
import ru.noties.markwon.spans.BulletListItemSpan;
|
||||
import ru.noties.markwon.spans.CodeSpan;
|
||||
import ru.noties.markwon.spans.HeadingSpan;
|
||||
|
||||
public class SpannableConfiguration {
|
||||
|
||||
// creates default configuration
|
||||
public static SpannableConfiguration create(@NonNull Context context) {
|
||||
return new Builder(context).build();
|
||||
}
|
||||
|
||||
public static Builder builder(@NonNull Context context) {
|
||||
return new Builder(context);
|
||||
}
|
||||
|
||||
private final BlockQuoteSpan.Config blockQuoteConfig;
|
||||
private final CodeSpan.Config codeConfig;
|
||||
private final BulletListItemSpan.Config bulletListConfig;
|
||||
private final HeadingSpan.Config headingConfig;
|
||||
|
||||
private SpannableConfiguration(Builder builder) {
|
||||
this.blockQuoteConfig = builder.blockQuoteConfig;
|
||||
this.codeConfig = builder.codeConfig;
|
||||
this.bulletListConfig = builder.bulletListConfig;
|
||||
this.headingConfig = builder.headingConfig;
|
||||
}
|
||||
|
||||
public BlockQuoteSpan.Config getBlockQuoteConfig() {
|
||||
return blockQuoteConfig;
|
||||
}
|
||||
|
||||
public CodeSpan.Config getCodeConfig() {
|
||||
return codeConfig;
|
||||
}
|
||||
|
||||
public BulletListItemSpan.Config getBulletListConfig() {
|
||||
return bulletListConfig;
|
||||
}
|
||||
|
||||
public HeadingSpan.Config getHeadingConfig() {
|
||||
return headingConfig;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Context context;
|
||||
private BlockQuoteSpan.Config blockQuoteConfig;
|
||||
private CodeSpan.Config codeConfig;
|
||||
private BulletListItemSpan.Config bulletListConfig;
|
||||
private HeadingSpan.Config headingConfig;
|
||||
|
||||
public Builder(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Builder setBlockQuoteConfig(@NonNull BlockQuoteSpan.Config blockQuoteConfig) {
|
||||
this.blockQuoteConfig = blockQuoteConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCodeConfig(@NonNull CodeSpan.Config codeConfig) {
|
||||
this.codeConfig = codeConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBulletListConfig(@NonNull BulletListItemSpan.Config bulletListConfig) {
|
||||
this.bulletListConfig = bulletListConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHeadingConfig(@NonNull HeadingSpan.Config headingConfig) {
|
||||
this.headingConfig = headingConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
// todo, change to something more reliable
|
||||
public SpannableConfiguration build() {
|
||||
if (blockQuoteConfig == null) {
|
||||
blockQuoteConfig = new BlockQuoteSpan.Config(
|
||||
px(16),
|
||||
px(4),
|
||||
0xFFcccccc
|
||||
);
|
||||
}
|
||||
if (codeConfig == null) {
|
||||
codeConfig = CodeSpan.Config.builder()
|
||||
.setMultilineMargin(px(8))
|
||||
.build();
|
||||
}
|
||||
if (bulletListConfig == null) {
|
||||
bulletListConfig = new BulletListItemSpan.Config(0, px(16), px(1));
|
||||
}
|
||||
if (headingConfig == null) {
|
||||
headingConfig = new HeadingSpan.Config(px(1), 0);
|
||||
}
|
||||
return new SpannableConfiguration(this);
|
||||
}
|
||||
|
||||
private int px(int dp) {
|
||||
return (int) (context.getResources().getDisplayMetrics().density * dp + .5F);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package ru.noties.markwon.renderer;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class SpannableHtmlParser {
|
||||
|
||||
// we need to handle images independently (in order to parse alt, width, height, etc)
|
||||
|
||||
// for simple tags without arguments
|
||||
// <b>, <i>, etc
|
||||
public interface SpanProvider {
|
||||
Object provide();
|
||||
}
|
||||
|
||||
public interface HtmlParser {
|
||||
Object[] getSpans(@NonNull String html);
|
||||
}
|
||||
|
||||
// creates default parser
|
||||
public static SpannableHtmlParser create() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
private final Map<String, SpanProvider> customTags;
|
||||
private final HtmlParser parser;
|
||||
|
||||
private SpannableHtmlParser(Builder builder) {
|
||||
this.customTags = builder.customTags;
|
||||
this.parser = builder.parser;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Map<String, SpanProvider> customTags = new HashMap<>(3);
|
||||
private HtmlParser parser;
|
||||
|
||||
public Builder customTag(@NonNull String tag, @NonNull SpanProvider provider) {
|
||||
customTags.put(tag, provider);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setParser(@NonNull HtmlParser parser) {
|
||||
this.parser = parser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpannableHtmlParser build() {
|
||||
if (parser == null) {
|
||||
// todo, images....
|
||||
parser = DefaultHtmlParser.create(null, null);
|
||||
}
|
||||
return new SpannableHtmlParser(this);
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class DefaultHtmlParser implements HtmlParser {
|
||||
|
||||
public static DefaultHtmlParser create(@Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) {
|
||||
final DefaultHtmlParser parser;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
parser = new Parser24(imageGetter, tagHandler);
|
||||
} else {
|
||||
parser = new ParserPre24(imageGetter, tagHandler);
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
|
||||
final Html.ImageGetter imageGetter;
|
||||
final Html.TagHandler tagHandler;
|
||||
|
||||
DefaultHtmlParser(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
|
||||
this.imageGetter = imageGetter;
|
||||
this.tagHandler = tagHandler;
|
||||
}
|
||||
|
||||
Object[] getSpans(Spanned spanned) {
|
||||
final Object[] spans;
|
||||
final int length = spanned != null ? spanned.length() : 0;
|
||||
if (length == 0) {
|
||||
spans = null;
|
||||
} else {
|
||||
spans = spanned.getSpans(0, length, Object.class);
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class ParserPre24 extends DefaultHtmlParser {
|
||||
|
||||
ParserPre24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
|
||||
super(imageGetter, tagHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getSpans(@NonNull String html) {
|
||||
return getSpans(Html.fromHtml(html, imageGetter, tagHandler));
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
private static class Parser24 extends DefaultHtmlParser {
|
||||
|
||||
Parser24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
|
||||
super(imageGetter, tagHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getSpans(@NonNull String html) {
|
||||
return getSpans(Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, tagHandler));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,310 @@
|
||||
package ru.noties.markwon.renderer;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
|
||||
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
|
||||
import org.commonmark.node.AbstractVisitor;
|
||||
import org.commonmark.node.BlockQuote;
|
||||
import org.commonmark.node.BulletList;
|
||||
import org.commonmark.node.Code;
|
||||
import org.commonmark.node.CustomNode;
|
||||
import org.commonmark.node.Emphasis;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.node.HardLineBreak;
|
||||
import org.commonmark.node.Heading;
|
||||
import org.commonmark.node.HtmlBlock;
|
||||
import org.commonmark.node.Image;
|
||||
import org.commonmark.node.ListBlock;
|
||||
import org.commonmark.node.ListItem;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.OrderedList;
|
||||
import org.commonmark.node.Paragraph;
|
||||
import org.commonmark.node.SoftLineBreak;
|
||||
import org.commonmark.node.StrongEmphasis;
|
||||
import org.commonmark.node.Text;
|
||||
import org.commonmark.node.ThematicBreak;
|
||||
|
||||
import ru.noties.debug.Debug;
|
||||
import ru.noties.markwon.spans.BlockQuoteSpan;
|
||||
import ru.noties.markwon.spans.BulletListItemSpan;
|
||||
import ru.noties.markwon.spans.CodeSpan;
|
||||
import ru.noties.markwon.spans.EmphasisSpan;
|
||||
import ru.noties.markwon.spans.HeadingSpan;
|
||||
import ru.noties.markwon.spans.StrongEmphasisSpan;
|
||||
import ru.noties.markwon.spans.ThematicBreakSpan;
|
||||
|
||||
public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
|
||||
// http://spec.commonmark.org/0.18/#html-blocks
|
||||
|
||||
private final SpannableConfiguration configuration;
|
||||
private final SpannableStringBuilder builder;
|
||||
|
||||
private int blockQuoteIndent;
|
||||
private int listLevel;
|
||||
|
||||
public SpannableMarkdownVisitor(
|
||||
@NonNull SpannableConfiguration configuration,
|
||||
@NonNull SpannableStringBuilder builder
|
||||
) {
|
||||
this.configuration = configuration;
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Text text) {
|
||||
Debug.i(text);
|
||||
builder.append(text.getLiteral());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(StrongEmphasis strongEmphasis) {
|
||||
Debug.i(strongEmphasis);
|
||||
final int length = builder.length();
|
||||
visitChildren(strongEmphasis);
|
||||
setSpan(length, new StrongEmphasisSpan());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Emphasis emphasis) {
|
||||
Debug.i(emphasis);
|
||||
final int length = builder.length();
|
||||
visitChildren(emphasis);
|
||||
setSpan(length, new EmphasisSpan());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BlockQuote blockQuote) {
|
||||
|
||||
newLine();
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
blockQuoteIndent += 1;
|
||||
|
||||
visitChildren(blockQuote);
|
||||
|
||||
setSpan(length, new BlockQuoteSpan(
|
||||
configuration.getBlockQuoteConfig(),
|
||||
blockQuoteIndent
|
||||
));
|
||||
|
||||
blockQuoteIndent -= 1;
|
||||
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Code code) {
|
||||
|
||||
Debug.i(code);
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
// NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces
|
||||
// unfortunately we cannot use this for multiline code as we cannot control there a new line break will be inserted
|
||||
builder.append('\u00a0');
|
||||
builder.append(code.getLiteral());
|
||||
builder.append('\u00a0');
|
||||
|
||||
setSpan(length, new CodeSpan(
|
||||
configuration.getCodeConfig(),
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(FencedCodeBlock fencedCodeBlock) {
|
||||
|
||||
Debug.i(fencedCodeBlock);
|
||||
|
||||
newLine();
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
builder.append(fencedCodeBlock.getLiteral());
|
||||
setSpan(length, new CodeSpan(
|
||||
configuration.getCodeConfig(),
|
||||
true
|
||||
));
|
||||
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Image image) {
|
||||
|
||||
Debug.i(image);
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
visitChildren(image);
|
||||
|
||||
if (length == builder.length()) {
|
||||
// nothing is added, and we need at least one symbol
|
||||
builder.append(' ');
|
||||
}
|
||||
|
||||
|
||||
//// final int length = builder.length();
|
||||
// final TestDrawable drawable = new TestDrawable();
|
||||
// final DrawableSpan span = new DrawableSpan(drawable);
|
||||
// builder.append(" ");
|
||||
// builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BulletList bulletList) {
|
||||
Debug.i(bulletList);
|
||||
newLine();
|
||||
visitChildren(bulletList);
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ListItem listItem) {
|
||||
|
||||
Debug.i(listItem);
|
||||
|
||||
final int length = builder.length();
|
||||
blockQuoteIndent += 1;
|
||||
listLevel += 1;
|
||||
visitChildren(listItem);
|
||||
// todo, can be a bullet list & ordered list (with leading numbers... looks like we need to `draw` numbers...
|
||||
setSpan(length, new BulletListItemSpan(
|
||||
configuration.getBulletListConfig(),
|
||||
blockQuoteIndent,
|
||||
listLevel - 1,
|
||||
length
|
||||
));
|
||||
// builder.setSpan(new BulletListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
blockQuoteIndent -= 1;
|
||||
listLevel -= 1;
|
||||
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ThematicBreak thematicBreak) {
|
||||
|
||||
Debug.i(thematicBreak);
|
||||
|
||||
newLine();
|
||||
|
||||
// todo, new lines...
|
||||
final int length = builder.length();
|
||||
builder.append(' '); // without space it won't render
|
||||
builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(OrderedList orderedList) {
|
||||
|
||||
Debug.i(orderedList);
|
||||
|
||||
newLine();
|
||||
|
||||
// Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber());
|
||||
// todo, ordering numbers
|
||||
super.visit(orderedList);
|
||||
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Heading heading) {
|
||||
|
||||
Debug.i(heading);
|
||||
|
||||
newLine();
|
||||
|
||||
final int length = builder.length();
|
||||
visitChildren(heading);
|
||||
setSpan(length, new HeadingSpan(
|
||||
configuration.getHeadingConfig(),
|
||||
heading.getLevel(),
|
||||
builder.length())
|
||||
);
|
||||
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(SoftLineBreak softLineBreak) {
|
||||
Debug.i(softLineBreak);
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HardLineBreak hardLineBreak) {
|
||||
Debug.i(hardLineBreak);
|
||||
newLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CustomNode customNode) {
|
||||
|
||||
Debug.i(customNode);
|
||||
|
||||
if (customNode instanceof Strikethrough) {
|
||||
final int length = builder.length();
|
||||
visitChildren(customNode);
|
||||
setSpan(length, new StrikethroughSpan());
|
||||
} else {
|
||||
super.visit(customNode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Paragraph paragraph) {
|
||||
|
||||
final boolean inTightList = isInTightList(paragraph);
|
||||
|
||||
Debug.i(paragraph, inTightList);
|
||||
|
||||
if (!inTightList) {
|
||||
newLine();
|
||||
}
|
||||
|
||||
visitChildren(paragraph);
|
||||
|
||||
if (!inTightList) {
|
||||
newLine();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HtmlBlock htmlBlock) {
|
||||
// http://spec.commonmark.org/0.18/#html-blocks
|
||||
Debug.i(htmlBlock, htmlBlock.getLiteral());
|
||||
super.visit(htmlBlock);
|
||||
}
|
||||
|
||||
private void setSpan(int start, @NonNull Object span) {
|
||||
builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
private void newLine() {
|
||||
if (builder.length() > 0
|
||||
&& '\n' != builder.charAt(builder.length() - 1)) {
|
||||
builder.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInTightList(Paragraph paragraph) {
|
||||
final Node parent = paragraph.getParent();
|
||||
if (parent != null) {
|
||||
final Node gramps = parent.getParent();
|
||||
if (gramps != null && gramps instanceof ListBlock) {
|
||||
ListBlock list = (ListBlock) gramps;
|
||||
return list.isTight();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package ru.noties.markwon.renderer;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
// please note that this class does not implement Renderer in order to return CharSequence (instead of String)
|
||||
public class SpannableRenderer {
|
||||
|
||||
// todo
|
||||
// * 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)
|
||||
|
||||
public CharSequence render(@NonNull SpannableConfiguration configuration, @Nullable Node node) {
|
||||
final CharSequence out;
|
||||
if (node == null) {
|
||||
out = null;
|
||||
} else {
|
||||
final SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
node.accept(new SpannableMarkdownVisitor(configuration, builder));
|
||||
out = builder;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumentation test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertEquals("ru.noties.markwon.spans.test", appContext.getPackageName());
|
||||
}
|
||||
}
|
@ -1,10 +1 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
||||
package="ru.noties.markwon.spans">
|
||||
|
||||
<application android:allowBackup="true" android:label="@string/app_name"
|
||||
android:supportsRtl="true">
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
<manifest package="ru.noties.markwon.spans" />
|
||||
|
@ -0,0 +1,73 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Layout;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
|
||||
public class BlockQuoteSpan implements LeadingMarginSpan {
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static class Config {
|
||||
|
||||
final int totalWidth;
|
||||
final int quoteWidth;
|
||||
final int quoteColor; // by default textColor with 0.1 alpha
|
||||
|
||||
public Config(
|
||||
@IntRange(from = 0) int totalWidth,
|
||||
@IntRange(from = 0) int quoteWidth,
|
||||
@ColorInt int quoteColor) {
|
||||
this.totalWidth = totalWidth;
|
||||
this.quoteWidth = quoteWidth;
|
||||
this.quoteColor = quoteColor;
|
||||
}
|
||||
}
|
||||
|
||||
private final Config config;
|
||||
private final Rect rect = new Rect();
|
||||
private final Paint paint = new Paint();
|
||||
private final int indent;
|
||||
|
||||
public BlockQuoteSpan(@NonNull Config config, int indent) {
|
||||
this.config = config;
|
||||
this.indent = indent;
|
||||
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(config.quoteColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
return config.totalWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawLeadingMargin(
|
||||
Canvas c,
|
||||
Paint p,
|
||||
int x,
|
||||
int dir,
|
||||
int top,
|
||||
int baseline,
|
||||
int bottom,
|
||||
CharSequence text,
|
||||
int start,
|
||||
int end,
|
||||
boolean first,
|
||||
Layout layout) {
|
||||
|
||||
final int save = c.save();
|
||||
try {
|
||||
final int left = config.totalWidth * (indent - 1);
|
||||
rect.set(left, top, left + config.quoteWidth, bottom);
|
||||
c.drawRect(rect, paint);
|
||||
} finally {
|
||||
c.restoreToCount(save);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Layout;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
|
||||
public class BulletListItemSpan implements LeadingMarginSpan {
|
||||
|
||||
// todo, there are 3 types of bullets: filled circle, stroke circle & filled rectangle
|
||||
// also, there are ordered lists
|
||||
|
||||
public static class Config {
|
||||
|
||||
final int bulletColor; // by default uses text color
|
||||
final int marginWidth;
|
||||
final int bulletStrokeWidth;
|
||||
|
||||
// from 0 but it makes sense to provide something wider
|
||||
public Config(@ColorInt int bulletColor, @IntRange(from = 0) int marginWidth, int bulletStrokeWidth) {
|
||||
this.bulletColor = bulletColor;
|
||||
this.marginWidth = marginWidth;
|
||||
this.bulletStrokeWidth = bulletStrokeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
private final Config config;
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private RectF circle;
|
||||
private Rect rectangle;
|
||||
|
||||
private final int blockIndent;
|
||||
private final int level;
|
||||
private final int start;
|
||||
|
||||
public BulletListItemSpan(
|
||||
@NonNull Config config,
|
||||
@IntRange(from = 0) int blockIndent,
|
||||
@IntRange(from = 0) int level,
|
||||
@IntRange(from = 0) int start) {
|
||||
this.config = config;
|
||||
this.blockIndent = blockIndent;
|
||||
this.level = level;
|
||||
this.start = start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
return config.marginWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||
|
||||
// if there was a line break, we don't need to draw it
|
||||
if (this.start != start) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int color;
|
||||
final float stroke;
|
||||
|
||||
if (config.bulletColor == 0) {
|
||||
color = p.getColor();
|
||||
} else {
|
||||
color = config.bulletColor;
|
||||
}
|
||||
|
||||
if (config.bulletStrokeWidth == 0) {
|
||||
stroke = p.getStrokeWidth();
|
||||
} else {
|
||||
stroke = config.bulletStrokeWidth;
|
||||
}
|
||||
|
||||
paint.setColor(color);
|
||||
paint.setStrokeWidth(stroke);
|
||||
|
||||
final int save = c.save();
|
||||
try {
|
||||
|
||||
// by default we use half of margin width, but if height is less than width, we calculate from it
|
||||
final int width = config.marginWidth;
|
||||
final int height = bottom - top;
|
||||
|
||||
final int side = Math.min(config.marginWidth, height) / 2;
|
||||
|
||||
final int marginLeft = (width - side) / 2;
|
||||
final int marginTop = (height - side) / 2;
|
||||
|
||||
final int l = (width * (blockIndent - 1)) + marginLeft;
|
||||
final int t = top + marginTop;
|
||||
final int r = l + side;
|
||||
final int b = t + side;
|
||||
|
||||
if (level == 0
|
||||
|| level == 1) {
|
||||
|
||||
// ensure we have circle rectF
|
||||
if (circle == null) {
|
||||
circle = new RectF();
|
||||
}
|
||||
|
||||
circle.set(l, t, r, b);
|
||||
|
||||
final Paint.Style style = level == 0
|
||||
? Paint.Style.FILL
|
||||
: Paint.Style.STROKE;
|
||||
paint.setStyle(style);
|
||||
|
||||
c.drawOval(circle, paint);
|
||||
} else {
|
||||
|
||||
// ensure rectangle
|
||||
if (rectangle == null) {
|
||||
rectangle = new Rect();
|
||||
}
|
||||
|
||||
rectangle.set(l, t, r, b);
|
||||
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
|
||||
c.drawRect(rectangle, paint);
|
||||
}
|
||||
|
||||
} finally {
|
||||
c.restoreToCount(save);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,307 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Layout;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan {
|
||||
|
||||
// the thing is.. we cannot use replacementSpan, because it won't let us create multiline code..
|
||||
// and we want new lines when we do not fit the width
|
||||
// plus it complicates the copying
|
||||
|
||||
// replacement span is great because we can have additional paddings & can actually get a hold
|
||||
// of Canvas to draw background, but it implies a lot of manual text handling
|
||||
|
||||
// also, we can reuse Rect instance as long as we apply our dimensions in each draw call
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static class Config {
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
final int textColor; // by default the same as main text
|
||||
final int backgroundColor; // by default textColor with 0.1 alpha
|
||||
final int multilineMargin; // by default 0
|
||||
final int textSize; // by default the same as main text
|
||||
final Typeface typeface; // by default Typeface.MONOSPACE
|
||||
|
||||
private Config(Builder builder) {
|
||||
this.textColor = builder.textColor;
|
||||
this.backgroundColor = builder.backgroundColor;
|
||||
this.multilineMargin = builder.multilineMargin;
|
||||
this.textSize = builder.textSize;
|
||||
this.typeface = builder.typeface;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
int textColor;
|
||||
int backgroundColor;
|
||||
int multilineMargin;
|
||||
int textSize;
|
||||
Typeface typeface;
|
||||
|
||||
public Builder setTextColor(@ColorInt int textColor) {
|
||||
this.textColor = textColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBackgroundColor(@ColorInt int backgroundColor) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMultilineMargin(int multilineMargin) {
|
||||
this.multilineMargin = multilineMargin;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTextSize(@IntRange(from = 0) int textSize) {
|
||||
this.textSize = textSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTypeface(@NonNull Typeface typeface) {
|
||||
this.typeface = typeface;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Config build() {
|
||||
if (typeface == null) {
|
||||
typeface = Typeface.MONOSPACE;
|
||||
}
|
||||
return new Config(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Config config;
|
||||
private final Rect rect = new Rect();
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private final boolean multiline;
|
||||
|
||||
public CodeSpan(@NonNull Config config, boolean multiline) {
|
||||
this.config = config;
|
||||
this.multiline = multiline;
|
||||
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setTypeface(config.typeface);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(TextPaint p) {
|
||||
apply(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint ds) {
|
||||
apply(ds);
|
||||
if (!multiline) {
|
||||
final int color;
|
||||
if (config.backgroundColor == 0) {
|
||||
color = applyAlpha(ds.getColor(), 25);
|
||||
} else {
|
||||
color = config.backgroundColor;
|
||||
}
|
||||
ds.bgColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
private void apply(TextPaint p) {
|
||||
p.setTypeface(config.typeface);
|
||||
if (config.textSize > 0) {
|
||||
p.setTextSize(config.textSize);
|
||||
}
|
||||
if (config.textColor != 0) {
|
||||
p.setColor(config.textColor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
return multiline ? config.multilineMargin : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||
|
||||
if (multiline) {
|
||||
|
||||
final int color;
|
||||
if (config.backgroundColor == 0) {
|
||||
color = applyAlpha(p.getColor(), 25);
|
||||
} else {
|
||||
color = config.backgroundColor;
|
||||
}
|
||||
paint.setColor(color);
|
||||
|
||||
rect.set(x, top, c.getWidth(), bottom);
|
||||
|
||||
c.drawRect(rect, paint);
|
||||
}
|
||||
|
||||
// paint.setTextSize(p.getTextSize());
|
||||
//
|
||||
// final int left = (int) (x + .5F);
|
||||
//
|
||||
// final int right;
|
||||
// if (multiline) {
|
||||
// right = c.getWidth();
|
||||
// } else {
|
||||
// final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F);
|
||||
// right = left + width;
|
||||
// }
|
||||
//
|
||||
// rect.set(left, top, right, bottom);
|
||||
//
|
||||
// // okay, draw background first
|
||||
// drawBackground(c);
|
||||
|
||||
// then, if any, draw borders
|
||||
// drawBorders(c, this.start == start, this.end == end);
|
||||
|
||||
// final int color;
|
||||
// if (config.textColor == 0) {
|
||||
// color = p.getColor();
|
||||
// } else {
|
||||
// color = config.textColor;
|
||||
// }
|
||||
// paint.setColor(color);
|
||||
//
|
||||
// // draw text
|
||||
// // y center position
|
||||
// final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
|
||||
// canvas.drawText(text, start, end, x + config.paddingHorizontal, b, paint);
|
||||
}
|
||||
|
||||
private static int applyAlpha(int color, int alpha) {
|
||||
return (color & 0x00FFFFFF) | (alpha << 24);
|
||||
}
|
||||
|
||||
|
||||
// @Override
|
||||
// public int getSize(
|
||||
// @NonNull Paint p,
|
||||
// CharSequence text,
|
||||
// @IntRange(from = 0) int start,
|
||||
// @IntRange(from = 0) int end,
|
||||
// @Nullable Paint.FontMetricsInt fm
|
||||
// ) {
|
||||
//
|
||||
// paint.setTextSize(p.getTextSize());
|
||||
//
|
||||
// final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F);
|
||||
//
|
||||
// if (fm != null) {
|
||||
// // we add a padding top & bottom
|
||||
// final float ratio = .62F; // golden ratio, there is no much point of moving this to config... it seems a bit `specific`...
|
||||
// fm.ascent = fm.ascent - (config.paddingVertical);
|
||||
// fm.descent = (int) (-fm.ascent * ratio);
|
||||
// fm.top = fm.ascent;
|
||||
// fm.bottom = fm.descent;
|
||||
// }
|
||||
//
|
||||
// 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 p
|
||||
// ) {
|
||||
//
|
||||
// paint.setTextSize(p.getTextSize());
|
||||
//
|
||||
// final int left = (int) (x + .5F);
|
||||
//
|
||||
// final int right;
|
||||
// if (multiline) {
|
||||
// right = canvas.getWidth();
|
||||
// } else {
|
||||
// final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F);
|
||||
// right = left + width;
|
||||
// }
|
||||
//
|
||||
// rect.set(left, top, right, bottom);
|
||||
//
|
||||
// // okay, draw background first
|
||||
// drawBackground(canvas);
|
||||
//
|
||||
// // then, if any, draw borders
|
||||
// drawBorders(canvas, this.start == start, this.end == end);
|
||||
//
|
||||
// final int color;
|
||||
// if (config.textColor == 0) {
|
||||
// color = p.getColor();
|
||||
// } else {
|
||||
// color = config.textColor;
|
||||
// }
|
||||
// paint.setColor(color);
|
||||
//
|
||||
// // draw text
|
||||
// // y center position
|
||||
// final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
|
||||
// canvas.drawText(text, start, end, x + config.paddingHorizontal, b, paint);
|
||||
// }
|
||||
|
||||
// private void drawBackground(Canvas canvas) {
|
||||
// final int color = config.backgroundColor;
|
||||
// if (color != 0) {
|
||||
// paint.setColor(color);
|
||||
// canvas.drawRect(rect, paint);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void drawBorders(Canvas canvas, boolean top, boolean bottom) {
|
||||
//
|
||||
// final int color = config.borderColor;
|
||||
// final int width = config.borderWidth;
|
||||
// if (color == 0
|
||||
// || width == 0) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// paint.setColor(color);
|
||||
//
|
||||
// // left and right are always drawn
|
||||
//
|
||||
// // LEFT
|
||||
// borderRect.set(rect.left, rect.top, rect.left + width, rect.bottom);
|
||||
// canvas.drawRect(borderRect, paint);
|
||||
//
|
||||
// // RIGHT
|
||||
// borderRect.set(rect.right - width, rect.top, rect.right, rect.bottom);
|
||||
// canvas.drawRect(borderRect, paint);
|
||||
//
|
||||
// // TOP
|
||||
// if (top) {
|
||||
// borderRect.set(rect.left, rect.top, rect.right, rect.top + width);
|
||||
// canvas.drawRect(borderRect, paint);
|
||||
// }
|
||||
//
|
||||
// // BOTTOM
|
||||
// if (bottom) {
|
||||
// borderRect.set(rect.left, rect.bottom - width, rect.right, rect.bottom);
|
||||
// canvas.drawRect(borderRect, paint);
|
||||
// }
|
||||
// }
|
||||
}
|
@ -10,6 +10,7 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.style.ReplacementSpan;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class DrawableSpan extends ReplacementSpan {
|
||||
|
||||
@IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER })
|
@ -0,0 +1,117 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Spanned;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DrawableSpanUtils {
|
||||
|
||||
// todo, add `unschedule` method (to be used when new text is set, so
|
||||
// drawables are removed from callbacks)
|
||||
|
||||
// this method is not completely valid because DynamicDrawableSpan stores
|
||||
// a drawable in weakReference & it could easily be freed, thus we might need
|
||||
// to re-schedule a new one, but we have no means to do it
|
||||
public static void scheduleDrawables(@NonNull final TextView textView) {
|
||||
|
||||
final CharSequence cs = textView.getText();
|
||||
final int length = cs != null
|
||||
? cs.length()
|
||||
: 0;
|
||||
if (length == 0 || !(cs instanceof Spanned)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Object[] spans = ((Spanned) cs).getSpans(0, length, Object.class);
|
||||
if (spans != null
|
||||
&& spans.length > 0) {
|
||||
|
||||
final List<Drawable> list = new ArrayList<>(2);
|
||||
|
||||
for (Object span: spans) {
|
||||
if (span instanceof DrawableSpan) {
|
||||
list.add(((DrawableSpan) span).getDrawable());
|
||||
}
|
||||
}
|
||||
|
||||
if (list.size() > 0) {
|
||||
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
// can it happen that the same view first detached & them attached with all previous content? hm..
|
||||
// no op for now
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
// remove callbacks...
|
||||
textView.removeOnAttachStateChangeListener(this);
|
||||
for (Drawable drawable: list) {
|
||||
drawable.setCallback(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (Drawable drawable: list) {
|
||||
drawable.setCallback(new DrawableCallbackImpl(textView, drawable.getBounds()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DrawableSpanUtils() {}
|
||||
|
||||
private static class DrawableCallbackImpl implements Drawable.Callback {
|
||||
|
||||
private final TextView view;
|
||||
private Rect previousBounds;
|
||||
|
||||
DrawableCallbackImpl(TextView view, Rect initialBounds) {
|
||||
this.view = view;
|
||||
this.previousBounds = new Rect(initialBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateDrawable(@NonNull Drawable who) {
|
||||
|
||||
// okay... teh thing is IF we do not change bounds size, normal invalidate would do
|
||||
// but if the size has changed, then we need to update the whole layout...
|
||||
|
||||
final Rect rect = who.getBounds();
|
||||
|
||||
if (!previousBounds.equals(rect)) {
|
||||
// the only method that seems to work when bounds have changed
|
||||
view.setText(view.getText());
|
||||
previousBounds = new Rect(rect);
|
||||
} else {
|
||||
// if bounds are the same then simple invalidate would do
|
||||
final int scrollX = view.getScrollX();
|
||||
final int scrollY = view.getScrollY();
|
||||
view.postInvalidate(
|
||||
scrollX + rect.left,
|
||||
scrollY + rect.top,
|
||||
scrollX + rect.right,
|
||||
scrollY + rect.bottom
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
|
||||
final long delay = when - SystemClock.uptimeMillis();
|
||||
view.postDelayed(what, delay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
|
||||
view.removeCallbacks(what);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Layout;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
public class HeadingSpan extends MetricAffectingSpan implements LeadingMarginSpan {
|
||||
|
||||
// taken from html spec (most browsers render headings like that)
|
||||
private static final float[] HEADING_SIZES = {
|
||||
2.F, 1.5F, 1.17F, 1.F, .83F, .67F,
|
||||
};
|
||||
|
||||
public static class Config {
|
||||
|
||||
final int breakHeight; // by default stroke width
|
||||
final int breakColor; // by default -> textColor
|
||||
|
||||
public Config(@IntRange(from = 0) int breakHeight, @ColorInt int breakColor) {
|
||||
this.breakHeight = breakHeight;
|
||||
this.breakColor = breakColor;
|
||||
}
|
||||
}
|
||||
|
||||
private final Config config;
|
||||
private final Rect rect = new Rect();
|
||||
private final Paint paint = new Paint();
|
||||
private final int level;
|
||||
private final int end;
|
||||
|
||||
public HeadingSpan(@NonNull Config config, @IntRange(from = 1, to = 6) int level, @IntRange(from = 0) int end) {
|
||||
this.config = config;
|
||||
this.level = level - 1;
|
||||
this.end = end;
|
||||
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(TextPaint p) {
|
||||
apply(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
apply(tp);
|
||||
}
|
||||
|
||||
private void apply(TextPaint paint) {
|
||||
paint.setTextSize(paint.getTextSize() * HEADING_SIZES[level]);
|
||||
paint.setFakeBoldText(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
// no margin actually, but we need to access Canvas to draw break
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||
|
||||
// if we are configured to draw underlines, draw them here
|
||||
|
||||
if (level == 0
|
||||
|| level == 1) {
|
||||
|
||||
if (this.end == end) {
|
||||
|
||||
final int color;
|
||||
final int breakHeight;
|
||||
if (config.breakColor == 0) {
|
||||
color = p.getColor();
|
||||
} else {
|
||||
color = config.breakColor;
|
||||
}
|
||||
if (config.breakHeight == 0) {
|
||||
breakHeight = (int) (p.getStrokeWidth() + .5F);
|
||||
} else {
|
||||
breakHeight = config.breakHeight;
|
||||
}
|
||||
paint.setColor(color);
|
||||
|
||||
rect.set(x, bottom - breakHeight, c.getWidth(), bottom);
|
||||
c.drawRect(rect, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Spans</string>
|
||||
</resources>
|
@ -1,17 +0,0 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() throws Exception {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
19
library-view/build.gradle
Normal file
19
library-view/build.gradle
Normal file
@ -0,0 +1,19 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion TARGET_SDK
|
||||
buildToolsVersion BUILD_TOOLS
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion MIN_SDK
|
||||
targetSdkVersion TARGET_SDK
|
||||
versionCode 1
|
||||
versionName version
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library-renderer')
|
||||
compile SUPPORT_ANNOTATIONS
|
||||
}
|
1
library-view/src/main/AndroidManifest.xml
Normal file
1
library-view/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="ru.noties.markwon.view"/>
|
@ -0,0 +1,59 @@
|
||||
package ru.noties.markwon.view;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import ru.noties.markwon.renderer.SpannableConfiguration;
|
||||
import ru.noties.markwon.renderer.SpannableRenderer;
|
||||
|
||||
public class MarkdownTextView extends TextView {
|
||||
|
||||
private Parser parser;
|
||||
private SpannableRenderer renderer;
|
||||
private SpannableConfiguration configuration;
|
||||
|
||||
public MarkdownTextView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MarkdownTextView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MarkdownTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public MarkdownTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(CharSequence text, BufferType type) {
|
||||
if (parser == null) {
|
||||
parser = Parser.builder()
|
||||
.extensions(Collections.singletonList(StrikethroughExtension.create()))
|
||||
.build();
|
||||
}
|
||||
if (renderer == null) {
|
||||
renderer = new SpannableRenderer();
|
||||
}
|
||||
if (configuration == null) {
|
||||
configuration = SpannableConfiguration.create(getContext());
|
||||
}
|
||||
final Node node = parser.parse(text.toString());
|
||||
final CharSequence cs = renderer.render(configuration, node);
|
||||
super.setText(cs, type);
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
include ':app', ':library-spans'
|
||||
include ':app', ':library-spans', ':library-renderer', ':library-view'
|
||||
|
Loading…
x
Reference in New Issue
Block a user