diff --git a/app/build.gradle b/app/build.gradle index fbb16176..6003452c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 815ca533..8bb5b207 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,20 +1,22 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="ru.noties.markwon"> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="ru.noties.markwon"> <application - android:allowBackup="true" - android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" - android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + 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> \ No newline at end of file diff --git a/app/src/main/assets/test.md b/app/src/main/assets/test.md index 8adf2d45..55aaab47 100644 --- a/app/src/main/assets/test.md +++ b/app/src/main/assets/test.md @@ -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# \ No newline at end of file diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 473b8607..4397f6bf 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -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); } diff --git a/app/src/main/java/ru/noties/markwon/Markwon.java b/app/src/main/java/ru/noties/markwon/Markwon.java index 285a4714..2a76c84c 100644 --- a/app/src/main/java/ru/noties/markwon/Markwon.java +++ b/app/src/main/java/ru/noties/markwon/Markwon.java @@ -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! +//} diff --git a/app/src/main/java/ru/noties/markwon/SpannableRenderer.java b/app/src/main/java/ru/noties/markwon/SpannableRenderer.java index 5db373da..e75d7071 100644 --- a/app/src/main/java/ru/noties/markwon/SpannableRenderer.java +++ b/app/src/main/java/ru/noties/markwon/SpannableRenderer.java @@ -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; +// } +// } +//} diff --git a/app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java b/app/src/main/java/ru/noties/markwon/spans2/BlockQuoteSpan.java similarity index 97% rename from app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java rename to app/src/main/java/ru/noties/markwon/spans2/BlockQuoteSpan.java index bfe76eec..68937347 100644 --- a/app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java +++ b/app/src/main/java/ru/noties/markwon/spans2/BlockQuoteSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.spans2; import android.graphics.Canvas; import android.graphics.Paint; diff --git a/app/src/main/java/ru/noties/markwon/spans/CodeSpan.java b/app/src/main/java/ru/noties/markwon/spans2/CodeSpan.java similarity index 65% rename from app/src/main/java/ru/noties/markwon/spans/CodeSpan.java rename to app/src/main/java/ru/noties/markwon/spans2/CodeSpan.java index 2c22af23..485592b3 100644 --- a/app/src/main/java/ru/noties/markwon/spans/CodeSpan.java +++ b/app/src/main/java/ru/noties/markwon/spans2/CodeSpan.java @@ -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); + } } diff --git a/app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java b/app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java new file mode 100644 index 00000000..5da2fbce --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java @@ -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; + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java b/app/src/main/java/ru/noties/markwon/spans2/DrawableSpanUtils.java similarity index 99% rename from app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java rename to app/src/main/java/ru/noties/markwon/spans2/DrawableSpanUtils.java index 7528c8ec..8b3fe20b 100644 --- a/app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java +++ b/app/src/main/java/ru/noties/markwon/spans2/DrawableSpanUtils.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.spans2; import android.graphics.Rect; import android.graphics.drawable.Drawable; diff --git a/app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java b/app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java new file mode 100644 index 00000000..bcd8804b --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java @@ -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); + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java b/app/src/main/java/ru/noties/markwon/spans2/ListItemSpan.java similarity index 97% rename from app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java rename to app/src/main/java/ru/noties/markwon/spans2/ListItemSpan.java index 065a1b2f..5b81952d 100644 --- a/app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java +++ b/app/src/main/java/ru/noties/markwon/spans2/ListItemSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.spans2; import android.graphics.Canvas; import android.graphics.Paint; diff --git a/app/src/main/java/ru/noties/markwon/spans2/StrongEmphasisSpan.java b/app/src/main/java/ru/noties/markwon/spans2/StrongEmphasisSpan.java new file mode 100644 index 00000000..0ec3e8b7 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans2/StrongEmphasisSpan.java @@ -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); + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans2/SubSpan.java b/app/src/main/java/ru/noties/markwon/spans2/SubSpan.java new file mode 100644 index 00000000..991219c6 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans2/SubSpan.java @@ -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); + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans2/SupSpan.java b/app/src/main/java/ru/noties/markwon/spans2/SupSpan.java new file mode 100644 index 00000000..044efc70 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans2/SupSpan.java @@ -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); + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans2/ThematicBreakSpan.java b/app/src/main/java/ru/noties/markwon/spans2/ThematicBreakSpan.java new file mode 100644 index 00000000..731c9628 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans2/ThematicBreakSpan.java @@ -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); + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5d4636b2..b80f26ff 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,17 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <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"> + 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"> - <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" - tools:context="ru.noties.markwon.MainActivity" - tools:text="yo\nman"/> + <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" /> </ScrollView> diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..4e1e75f3 --- /dev/null +++ b/app/src/main/res/values-v21/styles.xml @@ -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> \ No newline at end of file diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml deleted file mode 100644 index 63fc8164..00000000 --- a/app/src/main/res/values-w820dp/dimens.xml +++ /dev/null @@ -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> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5885930d..5b267e56 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -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> diff --git a/build.gradle b/build.gradle index a83e0d6a..43ca8a81 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac15c9df..38657fe7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/library-renderer/build.gradle b/library-renderer/build.gradle new file mode 100644 index 00000000..faf18fd8 --- /dev/null +++ b/library-renderer/build.gradle @@ -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' +} diff --git a/library-renderer/src/main/AndroidManifest.xml b/library-renderer/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c3f0b404 --- /dev/null +++ b/library-renderer/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="ru.noties.markwon.renderer" /> diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableConfiguration.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableConfiguration.java new file mode 100644 index 00000000..079c3d5c --- /dev/null +++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableConfiguration.java @@ -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); + } + } +} diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java new file mode 100644 index 00000000..afcb93b3 --- /dev/null +++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java @@ -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)); + } + } + } +} diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java new file mode 100644 index 00000000..a9a01bf3 --- /dev/null +++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -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; + } +} diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java new file mode 100644 index 00000000..accb401f --- /dev/null +++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java @@ -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; + } +} diff --git a/library-spans/src/androidTest/java/ru/noties/markwon/spans/ExampleInstrumentedTest.java b/library-spans/src/androidTest/java/ru/noties/markwon/spans/ExampleInstrumentedTest.java deleted file mode 100644 index 6386d053..00000000 --- a/library-spans/src/androidTest/java/ru/noties/markwon/spans/ExampleInstrumentedTest.java +++ /dev/null @@ -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()); - } -} diff --git a/library-spans/src/main/AndroidManifest.xml b/library-spans/src/main/AndroidManifest.xml index 6ba735e1..84de88b7 100644 --- a/library-spans/src/main/AndroidManifest.xml +++ b/library-spans/src/main/AndroidManifest.xml @@ -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" /> diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java new file mode 100644 index 00000000..35f73e96 --- /dev/null +++ b/library-spans/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java @@ -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); + } + } +} diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java new file mode 100644 index 00000000..ba92f59d --- /dev/null +++ b/library-spans/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java @@ -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); + } + } +} diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/CodeSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/CodeSpan.java new file mode 100644 index 00000000..b36d51b2 --- /dev/null +++ b/library-spans/src/main/java/ru/noties/markwon/spans/CodeSpan.java @@ -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); +// } +// } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpan.java similarity index 98% rename from app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java rename to library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpan.java index 79043eb9..8c065099 100644 --- a/app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java +++ b/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpan.java @@ -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 }) diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java b/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java new file mode 100644 index 00000000..d182ae6d --- /dev/null +++ b/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java @@ -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); + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java similarity index 100% rename from app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java rename to library-spans/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/HeadingSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/HeadingSpan.java new file mode 100644 index 00000000..1ac74fb6 --- /dev/null +++ b/library-spans/src/main/java/ru/noties/markwon/spans/HeadingSpan.java @@ -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); + } + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java similarity index 100% rename from app/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java rename to library-spans/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java diff --git a/app/src/main/java/ru/noties/markwon/spans/SubSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/SubSpan.java similarity index 100% rename from app/src/main/java/ru/noties/markwon/spans/SubSpan.java rename to library-spans/src/main/java/ru/noties/markwon/spans/SubSpan.java diff --git a/app/src/main/java/ru/noties/markwon/spans/SupSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/SupSpan.java similarity index 100% rename from app/src/main/java/ru/noties/markwon/spans/SupSpan.java rename to library-spans/src/main/java/ru/noties/markwon/spans/SupSpan.java diff --git a/app/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java similarity index 100% rename from app/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java rename to library-spans/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java diff --git a/library-spans/src/main/res/values/strings.xml b/library-spans/src/main/res/values/strings.xml deleted file mode 100644 index b54e5f5a..00000000 --- a/library-spans/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ -<resources> - <string name="app_name">Spans</string> -</resources> diff --git a/library-spans/src/test/java/ru/noties/markwon/spans/ExampleUnitTest.java b/library-spans/src/test/java/ru/noties/markwon/spans/ExampleUnitTest.java deleted file mode 100644 index 12a283a9..00000000 --- a/library-spans/src/test/java/ru/noties/markwon/spans/ExampleUnitTest.java +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/library-view/build.gradle b/library-view/build.gradle new file mode 100644 index 00000000..fa1bfd9d --- /dev/null +++ b/library-view/build.gradle @@ -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 +} \ No newline at end of file diff --git a/library-view/src/main/AndroidManifest.xml b/library-view/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f763152f --- /dev/null +++ b/library-view/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="ru.noties.markwon.view"/> diff --git a/library-view/src/main/java/ru/noties/markwon/view/MarkdownTextView.java b/library-view/src/main/java/ru/noties/markwon/view/MarkdownTextView.java new file mode 100644 index 00000000..df8f77b5 --- /dev/null +++ b/library-view/src/main/java/ru/noties/markwon/view/MarkdownTextView.java @@ -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); + } +} diff --git a/settings.gradle b/settings.gradle index 8d8cfdd7..dce89cfc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':library-spans' +include ':app', ':library-spans', ':library-renderer', ':library-view'