commit 3e9ff80da1d95420db52adce82edaec593c295fc Author: Dimitry Ivanov Date: Wed May 10 12:18:30 2017 +0300 Initial commit (migrating from testing project to library) diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c534e76c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +gradlew +gradlew.bat +**/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..fbb16176 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "ru.noties.markwon" + minSdkVersion 15 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +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 'ru.noties:debug:3.0.0@jar' + testCompile 'junit:junit:4.12' +} diff --git a/app/src/androidTest/java/ru/noties/markwon/ExampleInstrumentedTest.java b/app/src/androidTest/java/ru/noties/markwon/ExampleInstrumentedTest.java new file mode 100644 index 00000000..fac6b350 --- /dev/null +++ b/app/src/androidTest/java/ru/noties/markwon/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package ru.noties.markwon; + +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 Testing documentation + */ +@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", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..815ca533 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/test.md b/app/src/main/assets/test.md new file mode 100644 index 00000000..8adf2d45 --- /dev/null +++ b/app/src/main/assets/test.md @@ -0,0 +1,63 @@ +# Hello! + +**bold *italic*** _just italic_ + +[![Maven Central](https://img.shields.io/maven-central/v/ru.noties/scrollable.svg)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22scrollable%22) + +> Quote +>> Second Quote +>>> Third one, yuhuu! + +`can a code have **markdown?**` so go it doesn't + + +## Unordered list + +* first +* second +* * second first +* * second __second__ jks8feif fdsuif yuweru sdfoisdfu wutwe iower wtew ruweir weoir wutywr wer woeirwr wieyriow eryowe rwyeor oweryower o +* third `and some code` + + +1. okay +2. okay 2 + 1. okay again + * it's also here + 2. and this + 3. and that + +### Quoted list + +> * first +> * second +> * third +> * * third first +>> * yo #1 +>> * yo #2 + + +jo + + +#### Code block + +```java +final String s = "this id code block"; +s.length() > 0; +``` +--- +okay, have a good day! + +Yo**2**2242 + +To compare~~13~~ + +~~Just strike it~~ + +



+ +RED + +**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! + diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java new file mode 100644 index 00000000..473b8607 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -0,0 +1,88 @@ +package ru.noties.markwon; + +import android.graphics.drawable.Drawable; +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 { + + static { + Debug.init(new AndroidLogDebugOutput(true)); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + final TextView textView = (TextView) findViewById(R.id.activity_main); + + new Thread(new Runnable() { + @Override + public void run() { + InputStream stream = null; + Scanner scanner = null; + String md = null; + try { + stream = getAssets().open("test.md"); + scanner = new Scanner(stream).useDelimiter("\\A"); + if (scanner.hasNext()) { + md = scanner.next(); + } + } catch (Throwable t) { + Debug.e(t); + } finally { + if (stream != null) { + try { stream.close(); } catch (IOException e) {} + } + if (scanner != null) { + scanner.close(); + } + } + + if (md != null) { + final long start = SystemClock.uptimeMillis(); + final Parser parser = new Parser.Builder() + .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 long end = SystemClock.uptimeMillis(); + Debug.i("Rendered: %d ms, length: %d", end - start, text.length()); + textView.post(new Runnable() { + @Override + public void run() { + // NB! LinkMovementMethod forces frequent updates... +// textView.setMovementMethod(LinkMovementMethod.getInstance()); + textView.setText(text); + DrawableSpanUtils.scheduleDrawables(textView); + } + }); + } + } + }).start(); + } +} diff --git a/app/src/main/java/ru/noties/markwon/Markwon.java b/app/src/main/java/ru/noties/markwon/Markwon.java new file mode 100644 index 00000000..285a4714 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/Markwon.java @@ -0,0 +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! +} diff --git a/app/src/main/java/ru/noties/markwon/SpannableRenderer.java b/app/src/main/java/ru/noties/markwon/SpannableRenderer.java new file mode 100644 index 00000000..5db373da --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/SpannableRenderer.java @@ -0,0 +1,442 @@ +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); +// 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 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 `o` + // 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()) + ""; + 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; + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java b/app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java new file mode 100644 index 00000000..bfe76eec --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java @@ -0,0 +1,45 @@ +package ru.noties.markwon.spans; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.text.Layout; +import android.text.style.LeadingMarginSpan; +import ru.noties.debug.Debug; + +public class BlockQuoteSpan implements LeadingMarginSpan { + + private final int indent; + + public BlockQuoteSpan(int indent) { + this.indent = indent; + } + + @Override + public int getLeadingMargin(boolean first) { + return 24; + } + + @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) { +// Debug.i("x: %d, dir: %d, top: %d, baseline: %d, bottom: %d, first: %s", +// x, dir, top, baseline, bottom, first +// ); + + final int save = c.save(); + try { + final int left = 24 * (indent - 1); +// final RectF rectF = new RectF(0, 0, 16, 16); + final Rect rect = new Rect(left, top, left + 8, bottom); + final Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(0xFFf0f0f0); + c.drawRect(rect, paint); +// c.translate(x, .0F); +// c.drawOval(rectF, paint); + } finally { + c.restoreToCount(save); + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/CodeSpan.java b/app/src/main/java/ru/noties/markwon/spans/CodeSpan.java new file mode 100644 index 00000000..2c22af23 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/CodeSpan.java @@ -0,0 +1,199 @@ +package ru.noties.markwon.spans; + +import android.graphics.Canvas; +import android.graphics.Paint; +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; + +// we will use Replacement span because code blocks cannot contain other markdown +// so we will render the string (not a charSequence with possible metric affecting spans) +public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ { + + private final boolean multiline; + private final int start; + private final int end; + + public CodeSpan(boolean multiline, int start, int end) { + this.multiline = multiline; + this.start = start; + this.end = end; + } + + @Override + public int getSize( + @NonNull Paint paint, + CharSequence text, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @Nullable Paint.FontMetricsInt fm + ) { + + final CharSequence cs = text.subSequence(start, end); + final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F); + +// final StaticLayout layout = new StaticLayout(cs, new TextPaint(paint), 10000, Layout.Alignment.ALIGN_NORMAL, 1.F, .0F, false); +// final float width = layout.getLineWidth(0); +// final int out = 32 + (int) (width + .5F); + +// Debug.i("text: %s, width: %s", cs, width); + + if (fm != null) { + // we add a padding top & bottom + Debug.i("a: %s, d: %s, t: %s, b: %s", fm.ascent, fm.descent, fm.top, fm.bottom); + final float ratio = .62F; // golden ratio + fm.ascent = fm.ascent - 8; + 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 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); + } + + +// @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) { +//// Debug.i("x: %d, top: %d, bottom: %d", x, top, bottom); +// +//// Debug.i("this: [%d, %d], came: [%d, %d]", this.start, this.end, start, end); +// Debug.i("x: %d, canvas: [%d-%d], text: %s", x, c.getWidth(), c.getHeight(), (text.subSequence(start, end))); +// +// // the thing is... if we do not draw, then text won't be drawn also +// final Rect rect = new Rect(); +// +// final Paint paint = new Paint(); +// paint.setStyle(Paint.Style.FILL); +// paint.setColor(0xffcccccc); +// +// rect.set(x, top, c.getWidth(), bottom); +// c.drawRect(rect, paint); +// +// if (this.start == start) { +// this.top = top; +// +//// final int save = c.save(); +//// try { +//// c.drawColor(0x00ffffff); +//// } finally { +//// c.restoreToCount(save); +//// } +// +//// c.drawColor(0x00ffffff); +// } +// +// if (this.end == end) { +// // draw borders +// final Rect r = new Rect(x + 1, this.top, c.getWidth() - x, bottom); +// final Paint pa = new Paint(); +// pa.setStyle(Paint.Style.STROKE); +// pa.setColor(0xff999999); +// c.drawRect(r, pa); +// } +//// rect.inset((int) paint.getStrokeWidth(), (int) paint.getStrokeWidth()); +//// paint.setStyle(Paint.Style.STROKE); +//// paint.setColor(0xff333333); +//// c.drawRect(rect, paint); +// } + +// @Override +// public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) { +//// int ht = mDrawable.getIntrinsicHeight(); +//// +//// int need = ht - (v + fm.descent - fm.ascent - istartv); +//// if (need > 0) +//// fm.descent += need; +//// +//// need = ht - (v + fm.bottom - fm.top - istartv); +//// if (need > 0) +//// fm.bottom += need; +//// +// +//// final int lineOffset = v - spanstartv; +//// final int desired = 128; +//// final int currentLineHeight = -fm.ascent + fm.descent; +//// final float ratio = (float) desired / currentLineHeight; +//// +//// Debug.i("fm, came: %s", fm); +//// Debug.i("lineOffset: %d, current: %d, ratio: %s", lineOffset, currentLineHeight, ratio); +//// +//// fm.ascent = (int) (ratio * fm.ascent + .5F); +//// fm.descent = (int) (ratio * fm.descent + .5F); +//// +//// Debug.i("fm, out: %s", fm); +// +//// Debug.i("top: %d, bottom: %d, ascent: %d, descent: %d", fm.top, fm.bottom, fm.ascent, fm.descent); +//// Debug.i("lineHeight: %d, v: %d, spanstartv: %d", lineOffset, v, spanstartv); +//// +//// final int h = 128; +//// final int descentNeed = h - (v + fm.descent - fm.ascent - spanstartv); +//// if (descentNeed > 0) { +//// fm.ascent -= descentNeed / 2; +//// fm.descent += descentNeed / 2; +//// } +//// final int bottomNeed = h - (v + fm.bottom - fm.top - spanstartv); +//// if (bottomNeed > 0) { +//// fm.top -= bottomNeed; +//// fm.bottom += bottomNeed; +//// } +//// +//// Debug.i("out, ascent: %d, descent: %d, bottom: %d", fm.ascent, fm.descent, fm.bottom); +// } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java b/app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java new file mode 100644 index 00000000..79043eb9 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java @@ -0,0 +1,97 @@ +package ru.noties.markwon.spans; + +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/spans/DrawableSpanUtils.java new file mode 100644 index 00000000..7528c8ec --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java @@ -0,0 +1,116 @@ +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; + +import ru.noties.debug.Debug; + +public class DrawableSpanUtils { + + // 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 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/app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java new file mode 100644 index 00000000..7c3a8916 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.spans; + +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/spans/ListItemSpan.java new file mode 100644 index 00000000..065a1b2f --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java @@ -0,0 +1,50 @@ +package ru.noties.markwon.spans; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.text.Layout; +import android.text.style.LeadingMarginSpan; +import ru.noties.debug.Debug; + +public class ListItemSpan implements LeadingMarginSpan { + + private final int blockIndent; + private final boolean nested; + private final int start; + + public ListItemSpan(int blockIndent, boolean nested, int start) { + this.blockIndent = blockIndent; + this.nested = nested; + this.start = start; + } + + @Override + public int getLeadingMargin(boolean first) { + return 36; + } + + @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) { +// Debug.i("x: %d, dir: %d, top: %d, baseline: %d, bottom: %d, first: %s", +// x, dir, top, baseline, bottom, first +// ); + + // if there was a line break, we don't need to draw it + if (this.start != start) { + return; + } + + final int save = c.save(); + try { + final int left = 24 * (blockIndent - 1) + (first ? 12 : 0); + final RectF rectF = new RectF(left, top, left + 16, bottom); + final Paint paint = new Paint(); + paint.setStyle(nested ? Paint.Style.STROKE : Paint.Style.FILL); + paint.setColor(0xFFff0000); + c.drawOval(rectF, paint); + } finally { + c.restoreToCount(save); + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java b/app/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java new file mode 100644 index 00000000..32b5e51b --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.spans; + +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/spans/SubSpan.java b/app/src/main/java/ru/noties/markwon/spans/SubSpan.java new file mode 100644 index 00000000..d38f75da --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/SubSpan.java @@ -0,0 +1,19 @@ +package ru.noties.markwon.spans; + +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/spans/SupSpan.java b/app/src/main/java/ru/noties/markwon/spans/SupSpan.java new file mode 100644 index 00000000..4fddd44d --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/SupSpan.java @@ -0,0 +1,19 @@ +package ru.noties.markwon.spans; + +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/spans/ThematicBreakSpan.java b/app/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java new file mode 100644 index 00000000..a110078f --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java @@ -0,0 +1,25 @@ +package ru.noties.markwon.spans; + +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 new file mode 100644 index 00000000..5d4636b2 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..cde69bcc Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..c133a0cb Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..bfa42f0e Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..324e72cd Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..aee44e13 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 00000000..63fc8164 --- /dev/null +++ b/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..3ab3e9cb --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..47c82246 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..35f36bb5 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Markwon + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..5885930d --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/test/java/ru/noties/markwon/ExampleUnitTest.java b/app/src/test/java/ru/noties/markwon/ExampleUnitTest.java new file mode 100644 index 00000000..10bd839a --- /dev/null +++ b/app/src/test/java/ru/noties/markwon/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package ru.noties.markwon; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..a83e0d6a --- /dev/null +++ b/build.gradle @@ -0,0 +1,36 @@ +// 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 + } +} + +allprojects { + repositories { + jcenter() + } + version = VERSION_NAME +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext { + + // Config + BUILD_TOOLS = '25.0.2' + TARGET_SDK = 25 + MIN_SDK = 16 + + // Dependencies + final def supportVersion = '25.3.1' + SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion" +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..c932768c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx5g -Dfile.encoding=UTF-8 +#org.gradle.parallel=true +org.gradle.configureondemand=true + +android.enableBuildCache=true +android.buildCacheDir=build/pre-dex-cache + +VERSION_NAME=1.0.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13372aef Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ac15c9df --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 09 19:02:54 MSK 2017 +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 diff --git a/library-spans/build.gradle b/library-spans/build.gradle new file mode 100644 index 00000000..c07ebc94 --- /dev/null +++ b/library-spans/build.gradle @@ -0,0 +1,18 @@ +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 SUPPORT_ANNOTATIONS +} 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 new file mode 100644 index 00000000..6386d053 --- /dev/null +++ b/library-spans/src/androidTest/java/ru/noties/markwon/spans/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +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 Testing documentation + */ +@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 new file mode 100644 index 00000000..6ba735e1 --- /dev/null +++ b/library-spans/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/library-spans/src/main/res/values/strings.xml b/library-spans/src/main/res/values/strings.xml new file mode 100644 index 00000000..b54e5f5a --- /dev/null +++ b/library-spans/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Spans + 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 new file mode 100644 index 00000000..12a283a9 --- /dev/null +++ b/library-spans/src/test/java/ru/noties/markwon/spans/ExampleUnitTest.java @@ -0,0 +1,17 @@ +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 Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..8d8cfdd7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':library-spans'