Extracting functionality into library

This commit is contained in:
Dimitry Ivanov 2017-05-11 17:15:39 +03:00
parent 3e9ff80da1
commit 6f5fd08de4
47 changed files with 2242 additions and 615 deletions

View File

@ -1,32 +1,20 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 25
buildToolsVersion "25.0.2" compileSdkVersion TARGET_SDK
buildToolsVersion BUILD_TOOLS
defaultConfig { defaultConfig {
applicationId "ru.noties.markwon" applicationId "ru.noties.markwon"
minSdkVersion 15 minSdkVersion MIN_SDK
targetSdkVersion 25 targetSdkVersion TARGET_SDK
versionCode 1 versionCode 1
versionName "1.0" versionName version
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
} }
} }
dependencies { dependencies {
compile fileTree(dir: 'libs', include: ['*.jar']) compile project(':library-renderer')
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 'ru.noties:debug:3.0.0@jar' compile 'ru.noties:debug:3.0.0@jar'
testCompile 'junit:junit:4.12'
} }

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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"> package="ru.noties.markwon">
<application <application
@ -8,13 +9,14 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity android:name=".MainActivity"> <activity android:name=".MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

View File

@ -8,7 +8,13 @@
>> Second Quote >> Second Quote
>>> Third one, yuhuu! >>> 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 ## Unordered list
@ -17,6 +23,9 @@
* second * second
* * second first * * second first
* * second __second__ jks8feif fdsuif yuweru sdfoisdfu wutwe iower wtew ruweir weoir wutywr wer woeirwr wieyriow eryowe rwyeor oweryower o * * 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` * third `and some code`
@ -59,5 +68,4 @@ To compare<sub>~~13~~</sub>
<font color="#FF0000">RED</font> <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#

View File

@ -1,26 +1,26 @@
package ru.noties.markwon; package ru.noties.markwon;
import android.graphics.drawable.Drawable; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.style.StrikethroughSpan;
import android.widget.TextView; import android.widget.TextView;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.parser.Parser; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.Scanner; 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 { static {
Debug.init(new AndroidLogDebugOutput(true)); Debug.init(new AndroidLogDebugOutput(true));
@ -62,21 +62,28 @@ public class MainActivity extends AppCompatActivity {
.extensions(Arrays.asList(StrikethroughExtension.create())) .extensions(Arrays.asList(StrikethroughExtension.create()))
.build(); .build();
final Node node = parser.parse(md); final Node node = parser.parse(md);
final CharSequence text = new SpannableRenderer()._render(node/*, new Runnable() {
@Override final CharSequence text = new ru.noties.markwon.renderer.SpannableRenderer().render(
public void run() { SpannableConfiguration.create(MainActivity.this),
textView.setText(textView.getText()); node
final Drawable drawable = null; );
drawable.setCallback(textView);
} // 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(); final long end = SystemClock.uptimeMillis();
Debug.i("Rendered: %d ms, length: %d", end - start, text.length()); Debug.i("Rendered: %d ms, length: %d", end - start, text.length());
// Debug.i(text);
textView.post(new Runnable() { textView.post(new Runnable() {
@Override @Override
public void run() { public void run() {
// NB! LinkMovementMethod forces frequent updates... // NB! LinkMovementMethod forces frequent updates...
// textView.setMovementMethod(LinkMovementMethod.getInstance()); textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setText(text); textView.setText(text);
DrawableSpanUtils.scheduleDrawables(textView); DrawableSpanUtils.scheduleDrawables(textView);
} }

View File

@ -1,7 +1,7 @@
package ru.noties.markwon; //package ru.noties.markwon;
//
public class Markwon { //public class Markwon {
//
// todo, annotation processor to PRE_COMPILE markdown!! no... multiple lnguages and you are out, forget about it // // 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! // // view for debugging (to view in preview) x3!
} //}

View File

@ -1,442 +1,444 @@
package ru.noties.markwon; //package ru.noties.markwon;
//
import android.graphics.Canvas; //import android.graphics.Canvas;
import android.graphics.ColorFilter; //import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable; //import android.graphics.drawable.Drawable;
import android.os.Handler; //import android.os.Handler;
import android.os.Looper; //import android.os.Looper;
import android.support.annotation.IntRange; //import android.support.annotation.IntRange;
import android.support.annotation.NonNull; //import android.support.annotation.NonNull;
import android.support.annotation.Nullable; //import android.support.annotation.Nullable;
import android.text.Html; //import android.text.Html;
import android.text.SpannableStringBuilder; //import android.text.SpannableStringBuilder;
import android.text.Spanned; //import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan; //import android.text.style.AbsoluteSizeSpan;
import android.text.style.StrikethroughSpan; //import android.text.style.StrikethroughSpan;
import android.text.style.URLSpan; //import android.text.style.URLSpan;
//
import org.commonmark.ext.gfm.strikethrough.Strikethrough; //import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.node.AbstractVisitor; //import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.BlockQuote; //import org.commonmark.node.BlockQuote;
import org.commonmark.node.BulletList; //import org.commonmark.node.BulletList;
import org.commonmark.node.Code; //import org.commonmark.node.Code;
import org.commonmark.node.CustomBlock; //import org.commonmark.node.CustomBlock;
import org.commonmark.node.CustomNode; //import org.commonmark.node.CustomNode;
import org.commonmark.node.Document; //import org.commonmark.node.Document;
import org.commonmark.node.Emphasis; //import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock; //import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak; //import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading; //import org.commonmark.node.Heading;
import org.commonmark.node.HtmlBlock; //import org.commonmark.node.HtmlBlock;
import org.commonmark.node.HtmlInline; //import org.commonmark.node.HtmlInline;
import org.commonmark.node.Image; //import org.commonmark.node.Image;
import org.commonmark.node.IndentedCodeBlock; //import org.commonmark.node.IndentedCodeBlock;
import org.commonmark.node.Link; //import org.commonmark.node.Link;
import org.commonmark.node.ListItem; //import org.commonmark.node.ListItem;
import org.commonmark.node.Node; //import org.commonmark.node.Node;
import org.commonmark.node.OrderedList; //import org.commonmark.node.OrderedList;
import org.commonmark.node.Paragraph; //import org.commonmark.node.Paragraph;
import org.commonmark.node.SoftLineBreak; //import org.commonmark.node.SoftLineBreak;
import org.commonmark.node.StrongEmphasis; //import org.commonmark.node.StrongEmphasis;
import org.commonmark.node.Text; //import org.commonmark.node.Text;
import org.commonmark.node.ThematicBreak; //import org.commonmark.node.ThematicBreak;
import org.commonmark.renderer.Renderer; //import org.commonmark.renderer.Renderer;
//
import java.util.ArrayDeque; //import java.util.ArrayDeque;
import java.util.Arrays; //import java.util.Arrays;
import java.util.Deque; //import java.util.Deque;
//
import ru.noties.debug.Debug; //import ru.noties.debug.Debug;
import ru.noties.markwon.spans.BlockQuoteSpan; //import ru.noties.markwon.spans.BlockQuoteSpan;
import ru.noties.markwon.spans.CodeSpan; //import ru.noties.markwon.spans.CodeSpan;
import ru.noties.markwon.spans.DrawableSpan; //import ru.noties.markwon.spans.DrawableSpan;
import ru.noties.markwon.spans.EmphasisSpan; //import ru.noties.markwon.spans.EmphasisSpan;
import ru.noties.markwon.spans.ListItemSpan; //import ru.noties.markwon.spans.BulletListItemSpan;
import ru.noties.markwon.spans.StrongEmphasisSpan; //import ru.noties.markwon.spans.StrongEmphasisSpan;
import ru.noties.markwon.spans.SubSpan; //import ru.noties.markwon.spans.SubSpan;
import ru.noties.markwon.spans.SupSpan; //import ru.noties.markwon.spans.SupSpan;
import ru.noties.markwon.spans.ThematicBreakSpan; //import ru.noties.markwon.spans.ThematicBreakSpan;
//
public class SpannableRenderer implements Renderer { //public class SpannableRenderer implements Renderer {
//
// todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc) // // todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc)
//
@Override // @Override
public void render(Node node, Appendable output) { // public void render(Node node, Appendable output) {
//
} // }
//
@Override // @Override
public String render(Node node) { // public String render(Node node) {
// hm.. doesn't make sense to render to string // // hm.. doesn't make sense to render to string
throw null; // throw null;
} // }
//
public CharSequence _render(Node node) { // public CharSequence _render(Node node) {
final SpannableStringBuilder builder = new SpannableStringBuilder(); // final SpannableStringBuilder builder = new SpannableStringBuilder();
node.accept(new SpannableNodeRenderer(builder)); // node.accept(new SpannableNodeRenderer(builder));
return builder; // return builder;
} // }
//
private static class SpannableNodeRenderer extends AbstractVisitor { // private static class SpannableNodeRenderer extends AbstractVisitor {
//
// private static final float[] HEADING_SIZES = { //// private static final float[] HEADING_SIZES = {
// 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, //// 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
// }; //// };
//
private final SpannableStringBuilder builder; // private final SpannableStringBuilder builder;
//
private int blockQuoteIndent; // private int blockQuoteIndent;
private int listLevel; // private int listLevel;
//
SpannableNodeRenderer(SpannableStringBuilder builder) { // SpannableNodeRenderer(SpannableStringBuilder builder) {
this.builder = builder; // this.builder = builder;
} // }
//
@Override // @Override
public void visit(HardLineBreak hardLineBreak) { // public void visit(HardLineBreak hardLineBreak) {
Debug.i(hardLineBreak); // // todo
} // Debug.i(hardLineBreak);
// }
@Override //
public void visit(Text text) { // @Override
Debug.i(text); // public void visit(Text text) {
builder.append(text.getLiteral()); // builder.append(text.getLiteral());
} // }
//
@Override // @Override
public void visit(StrongEmphasis strongEmphasis) { // public void visit(StrongEmphasis strongEmphasis) {
final int length = builder.length(); // final int length = builder.length();
visitChildren(strongEmphasis); // visitChildren(strongEmphasis);
builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} // }
//
@Override // @Override
public void visit(Emphasis emphasis) { // public void visit(Emphasis emphasis) {
final int length = builder.length(); // final int length = builder.length();
visitChildren(emphasis); // visitChildren(emphasis);
builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} // }
//
@Override // @Override
public void visit(IndentedCodeBlock indentedCodeBlock) { // public void visit(IndentedCodeBlock indentedCodeBlock) {
Debug.i(indentedCodeBlock); // // todo
} // Debug.i(indentedCodeBlock);
// }
@Override //
public void visit(BlockQuote blockQuote) { // @Override
builder.append('\n'); // public void visit(BlockQuote blockQuote) {
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'); // builder.append('\n');
if (builder.charAt(builder.length() - 1) != '\n') { // final int length = builder.length();
builder.append('\n'); // blockQuoteIndent += 1;
} // visitChildren(blockQuote);
final int length = builder.length(); // builder.setSpan(new BlockQuoteSpan(blockQuoteIndent), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
blockQuoteIndent += 1; // blockQuoteIndent -= 1;
listLevel += 1; // }
visitChildren(listItem); //
// builder.setSpan(new BulletSpan(4, 0xff0000ff), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // @Override
builder.setSpan(new ListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // public void visit(Code code) {
blockQuoteIndent -= 1; // final int length = builder.length();
listLevel -= 1; // 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(ThematicBreak thematicBreak) { //
final int length = builder.length(); // @Override
builder.append('\n') // public void visit(BulletList bulletList) {
.append(' '); // without space it won't render // Debug.i(bulletList, bulletList.getBulletMarker());
builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // visitChildren(bulletList);
builder.append('\n'); // }
} //
// @Override
@Override // public void visit(ListItem listItem) {
public void visit(OrderedList orderedList) { // Debug.i(listItem);
Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber()); //// builder.append('\n');
// todo, ordering numbers // if (builder.charAt(builder.length() - 1) != '\n') {
super.visit(orderedList); // builder.append('\n');
} // }
// final int length = builder.length();
@Override // blockQuoteIndent += 1;
public void visit(SoftLineBreak softLineBreak) { // listLevel += 1;
Debug.i(softLineBreak); // 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);
@Override // blockQuoteIndent -= 1;
public void visit(Heading heading) { // listLevel -= 1;
Debug.i(heading); // }
if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') { //
builder.append('\n'); // @Override
} // public void visit(ThematicBreak thematicBreak) {
final int length = builder.length(); // final int length = builder.length();
visitChildren(heading); // builder.append('\n')
final int max = 120; // .append(' '); // without space it won't render
final int one = 20; // total is 6 // builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
final int size = max - ((heading.getLevel() - 1) * one); // builder.append('\n');
builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // }
builder.append('\n'); //
} // @Override
// public void visit(OrderedList orderedList) {
@Override // Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber());
public void visit(FencedCodeBlock fencedCodeBlock) { // // todo, ordering numbers
builder.append('\n'); // super.visit(orderedList);
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(SoftLineBreak softLineBreak) {
// Debug.i(softLineBreak);
@Override // }
public void visit(Paragraph paragraph) { //
Debug.i(paragraph); // @Override
if (listLevel == 0 // public void visit(Heading heading) {
&& blockQuoteIndent == 0) { // Debug.i(heading);
builder.append('\n') // if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') {
.append('\n'); // builder.append('\n');
} // }
visitChildren(paragraph); // final int length = builder.length();
// visitChildren(heading);
if (listLevel == 0 // final int max = 120;
&& blockQuoteIndent == 0) { // final int one = 20; // total is 6
builder.append('\n') // final int size = max - ((heading.getLevel() - 1) * one);
.append('\n'); // builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} // builder.append('\n');
} // }
//
// private int htmlStart = -1; // @Override
private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>(); // public void visit(FencedCodeBlock fencedCodeBlock) {
// builder.append('\n');
private static class HtmlInlineItem { // final int length = builder.length();
// builder.append(fencedCodeBlock.getLiteral());
final int start; // builder.setSpan(new CodeSpan(true, length, builder.length() - 1), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
final String tag; // }
//
private HtmlInlineItem(int start, String tag) { // @Override
this.start = start; // public void visit(Paragraph paragraph) {
this.tag = tag; // Debug.i(paragraph);
} // if (listLevel == 0
} // && blockQuoteIndent == 0) {
// builder.append('\n')
@Override // .append('\n');
public void visit(HtmlInline htmlInline) { // }
// visitChildren(paragraph);
// Debug.i(htmlInline, htmlStart); //
// Debug.i(htmlInline.getLiteral(), htmlInline.toString()); // if (listLevel == 0
// && blockQuoteIndent == 0) {
// okay, it's seems that we desperately need to understand if it's opening tag or closing // builder.append('\n')
// .append('\n');
final HtmlTag tag = parseTag(htmlInline.getLiteral()); // }
// }
Debug.i(htmlInline.getLiteral(), tag); //
//// private int htmlStart = -1;
if (tag != null) { // private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>();
Debug.i("tag: %s, closing: %s", tag.tag, tag.closing); //
if (!tag.closing) { // private static class HtmlInlineItem {
htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag)); //
visitChildren(htmlInline); // final int start;
} else { // final String tag;
final HtmlInlineItem item = htmlStack.pop(); //
final int start = item.start; // private HtmlInlineItem(int start, String tag) {
final int end = builder.length(); // this.start = start;
// here, additionally, we can render some tags ourselves (sup/sub) // this.tag = tag;
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); // @Override
} else if("del".equals(item.tag)) { // public void visit(HtmlInline htmlInline) {
// weird, but `Html` class does not return a spannable for `<del>o</del>` //
// seems like a bug //// Debug.i(htmlInline, htmlStart);
builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //// Debug.i(htmlInline.getLiteral(), htmlInline.toString());
} else { //
final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">"; // // okay, it's seems that we desperately need to understand if it's opening tag or closing
final Spanned spanned = Html.fromHtml(html); //
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); // final HtmlTag tag = parseTag(htmlInline.getLiteral());
//
Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans)); // Debug.i(htmlInline.getLiteral(), tag);
//
if (spans != null // if (tag != null) {
&& spans.length > 0) { // Debug.i("tag: %s, closing: %s", tag.tag, tag.closing);
for (Object span: spans) { // if (!tag.closing) {
Debug.i(span); // htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag));
builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // visitChildren(htmlInline);
} // } else {
} // final HtmlInlineItem item = htmlStack.pop();
} // final int start = item.start;
} // final int end = builder.length();
} else { // // here, additionally, we can render some tags ourselves (sup/sub)
super.visit(htmlInline); // 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);
private static class HtmlTag { // } else if("del".equals(item.tag)) {
final String tag; // // weird, but `Html` class does not return a spannable for `<del>o</del>`
final boolean closing; // // seems like a bug
HtmlTag(String tag, boolean closing) { // builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
this.tag = tag; // } else {
this.closing = closing; // final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">";
} // final Spanned spanned = Html.fromHtml(html);
@Override // final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
public String toString() { //
return "HtmlTag{" + // Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans));
"tag='" + tag + '\'' + //
", closing=" + closing + // if (spans != null
'}'; // && spans.length > 0) {
} // for (Object span: spans) {
} // Debug.i(span);
// builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
private static HtmlTag parseTag(String in) { // }
// }
final HtmlTag out; // }
// }
final int length = in != null // } else {
? in.length() // super.visit(htmlInline);
: 0; // }
// }
Debug.i(in, length); //
// private static class HtmlTag {
if (length == 0 || length < 3) { // final String tag;
out = null; // final boolean closing;
} else { // HtmlTag(String tag, boolean closing) {
// this.tag = tag;
final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1); // this.closing = closing;
final String tag = closing // }
? in.substring(2, in.length() - 1) // @Override
: in.substring(1, in.length() - 1); // public String toString() {
out = new HtmlTag(tag, closing); // return "HtmlTag{" +
} // "tag='" + tag + '\'' +
// ", closing=" + closing +
return out; // '}';
} // }
// }
@Override //
public void visit(HtmlBlock htmlBlock) { // private static HtmlTag parseTag(String in) {
// interestring thing... what is it also? //
Debug.i(htmlBlock); // final HtmlTag out;
} //
// final int length = in != null
@Override // ? in.length()
public void visit(CustomBlock customBlock) { // : 0;
// not supported, what is it anyway? //
Debug.i(customBlock); // Debug.i(in, length);
} //
// if (length == 0 || length < 3) {
@Override // out = null;
public void visit(Document document) { // } else {
// the whole document, no need to do anything //
Debug.i(document); // final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1);
super.visit(document); // final String tag = closing
} // ? in.substring(2, in.length() - 1)
// : in.substring(1, in.length() - 1);
@Override // out = new HtmlTag(tag, closing);
public void visit(Link link) { // }
Debug.i(link); //
final int length = builder.length(); // return out;
visitChildren(link); // }
builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //
} // @Override
// public void visit(HtmlBlock htmlBlock) {
@Override // // interestring thing... what is it also?
public void visit(Image image) { // Debug.i(htmlBlock);
// not supported... maybe for now? // }
Debug.i(image); //
super.visit(image); // @Override
// public void visit(CustomBlock customBlock) {
final int length = builder.length(); // // not supported, what is it anyway?
final TestDrawable drawable = new TestDrawable(); // Debug.i(customBlock);
final DrawableSpan span = new DrawableSpan(drawable); // }
builder.append(" "); //
builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // @Override
} // public void visit(Document document) {
// // the whole document, no need to do anything
@Override // Debug.i(document);
public void visit(CustomNode customNode) { // super.visit(document);
// }
Debug.i(customNode); //
// @Override
if (customNode instanceof Strikethrough) { // public void visit(Link link) {
final int length = builder.length(); // Debug.i(link);
visitChildren(customNode); // final int length = builder.length();
builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // visitChildren(link);
} else { // builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
super.visit(customNode); // }
} //
} // @Override
} // public void visit(Image image) {
// // not supported... maybe for now?
// Debug.i(image);
private static class TestDrawable extends Drawable { // final int length = builder.length();
// super.visit(image);
private final Handler handler = new Handler(Looper.getMainLooper()); //
private boolean called; //// final int length = builder.length();
// final TestDrawable drawable = new TestDrawable();
TestDrawable() { // final DrawableSpan span = new DrawableSpan(drawable);
setBounds(0, 0, 50, 50); // builder.append(" ");
} // builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// }
@Override //
public void draw(@NonNull final Canvas canvas) { // @Override
canvas.clipRect(getBounds()); // public void visit(CustomNode customNode) {
if (!called) { //
canvas.drawColor(0xFF00ff00); // Debug.i(customNode);
handler.removeCallbacksAndMessages(null); //
handler.postDelayed(new Runnable() { // if (customNode instanceof Strikethrough) {
@Override // final int length = builder.length();
public void run() { // visitChildren(customNode);
called = true; // builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setBounds(0, 0, 400, 400); // } else {
invalidateSelf(); // super.visit(customNode);
} // }
}, 2000L); // }
} else { // }
canvas.drawColor(0xFFff0000); //
} //
} // private static class TestDrawable extends Drawable {
//
@Override // private final Handler handler = new Handler(Looper.getMainLooper());
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { // private boolean called;
//
} // TestDrawable() {
// setBounds(0, 0, 50, 50);
@Override // }
public void setColorFilter(@Nullable ColorFilter colorFilter) { //
// @Override
} // public void draw(@NonNull final Canvas canvas) {
// canvas.clipRect(getBounds());
@Override // if (!called) {
public int getOpacity() { // canvas.drawColor(0xFF00ff00);
return 0; // handler.removeCallbacksAndMessages(null);
} // handler.postDelayed(new Runnable() {
// @Override
@Override // public void run() {
public int getIntrinsicWidth() { // called = true;
return called ? 400 : 50; // setBounds(0, 0, 400, 400);
} // invalidateSelf();
// }
@Override // }, 2000L);
public int getIntrinsicHeight() { // } else {
return called ? 400 : 50; // 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;
// }
// }
//}

View File

@ -1,4 +1,4 @@
package ru.noties.markwon.spans; package ru.noties.markwon.spans2;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;

View File

@ -1,4 +1,4 @@
package ru.noties.markwon.spans; package ru.noties.markwon.spans2;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
@ -6,12 +6,6 @@ import android.graphics.Rect;
import android.support.annotation.IntRange; import android.support.annotation.IntRange;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.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 android.text.style.ReplacementSpan;
import ru.noties.debug.Debug; import ru.noties.debug.Debug;
@ -24,10 +18,16 @@ public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ {
private final int start; private final int start;
private final int end; 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) { public CodeSpan(boolean multiline, int start, int end) {
this.multiline = multiline; this.multiline = multiline;
this.start = start; this.start = start;
this.end = end; this.end = end;
paint.setStyle(Paint.Style.FILL);
} }
@Override @Override
@ -74,33 +74,104 @@ public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ {
@NonNull Paint paint @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 left = (int) (x + .5F);
final int right = multiline
? canvas.getWidth()
: left + width;
final Rect rect = new Rect( final int right;
left, if (multiline) {
top, right = canvas.getWidth();
right, } else {
bottom final int width = (16 * 2) + (int) (paint.measureText(text, start, end) + .5F);
); right = left + width;
}
final Paint p = new Paint(); rect.set(left, top, right, bottom);
p.setStyle(Paint.Style.FILL);
p.setColor(0x80ff0000);
canvas.drawRect(rect, p);
// okay, draw background first
drawBackground(canvas);
// then, if any, draw borders
drawBorders(canvas, this.start == start, this.end == end);
// draw text
// y center position // y center position
final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2); final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
p.setColor(0xFF000000); // if (config.textColor != 0) {
canvas.drawText(cs, 0, cs.length(), x + 16, b, paint); // // 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);
}
} }

View 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;
}
}

View File

@ -1,4 +1,4 @@
package ru.noties.markwon.spans; package ru.noties.markwon.spans2;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;

View 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);
}
}

View File

@ -1,4 +1,4 @@
package ru.noties.markwon.spans; package ru.noties.markwon.spans2;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;

View File

@ -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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -5,12 +5,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main" android:id="@+id/activity_main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dip" android:layout_margin="16dip"
android:textSize="18sp"
android:lineSpacingExtra="2dip"
android:textColor="@color/colorPrimaryDark"
tools:context="ru.noties.markwon.MainActivity" tools:context="ru.noties.markwon.MainActivity"
tools:text="yo\nman" /> tools:text="yo\nman" />

View 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>

View File

@ -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>

View File

@ -1,11 +1,8 @@
<resources> <resources>
<style name="AppThemeBase" parent="android:Theme.Holo.Light"/>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="AppTheme" parent="AppThemeBase"/>
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources> </resources>

View File

@ -1,14 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
repositories { repositories {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.3.1' 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 // Dependencies
final def supportVersion = '25.3.1' final def supportVersion = '25.3.1'
SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion" 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"
} }

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View 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'
}

View File

@ -0,0 +1 @@
<manifest package="ru.noties.markwon.renderer" />

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -1,10 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest package="ru.noties.markwon.spans" />
package="ru.noties.markwon.spans">
<application android:allowBackup="true" android:label="@string/app_name"
android:supportsRtl="true">
</application>
</manifest>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
// }
// }
}

View File

@ -10,6 +10,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.style.ReplacementSpan; import android.text.style.ReplacementSpan;
@SuppressWarnings("WeakerAccess")
public class DrawableSpan extends ReplacementSpan { public class DrawableSpan extends ReplacementSpan {
@IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER }) @IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER })

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -1,3 +0,0 @@
<resources>
<string name="app_name">Spans</string>
</resources>

View File

@ -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
View 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
}

View File

@ -0,0 +1 @@
<manifest package="ru.noties.markwon.view"/>

View File

@ -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);
}
}

View File

@ -1 +1 @@
include ':app', ':library-spans' include ':app', ':library-spans', ':library-renderer', ':library-view'