Initial commit (migrating from testing project to library)
This commit is contained in:
		
						commit
						3e9ff80da1
					
				
							
								
								
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| *.iml | ||||
| .gradle | ||||
| /local.properties | ||||
| /.idea | ||||
| .DS_Store | ||||
| /build | ||||
| /captures | ||||
| .externalNativeBuild | ||||
| gradlew | ||||
| gradlew.bat | ||||
| **/build | ||||
							
								
								
									
										32
									
								
								app/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -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' | ||||
| } | ||||
| @ -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 <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", appContext.getPackageName()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										20
									
								
								app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <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"> | ||||
|         <activity android:name=".MainActivity"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN"/> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.LAUNCHER"/> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|     </application> | ||||
| 
 | ||||
| </manifest> | ||||
							
								
								
									
										63
									
								
								app/src/main/assets/test.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								app/src/main/assets/test.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| # Hello! | ||||
| 
 | ||||
| **bold *italic*** _just italic_ | ||||
| 
 | ||||
| [](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 | ||||
| 
 | ||||
| 
 | ||||
| <b>j<i><del>o</del></i></b> | ||||
| 
 | ||||
| 
 | ||||
| #### Code block | ||||
| 
 | ||||
| ```java | ||||
| final String s = "this id code block"; | ||||
| s.length() > 0; | ||||
| ``` | ||||
| --- | ||||
| okay, have a good day! | ||||
| 
 | ||||
| Yo<sup>**2**<sup>22</sup><sub>42</sub></sup> | ||||
| 
 | ||||
| To compare<sub>~~13~~</sub> | ||||
| 
 | ||||
| ~~Just strike it~~ | ||||
| 
 | ||||
| <br /><br /><br /><br /> | ||||
| 
 | ||||
| <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! | ||||
| 
 | ||||
							
								
								
									
										88
									
								
								app/src/main/java/ru/noties/markwon/MainActivity.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								app/src/main/java/ru/noties/markwon/MainActivity.java
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										7
									
								
								app/src/main/java/ru/noties/markwon/Markwon.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/src/main/java/ru/noties/markwon/Markwon.java
									
									
									
									
									
										Normal file
									
								
							| @ -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! | ||||
| } | ||||
							
								
								
									
										442
									
								
								app/src/main/java/ru/noties/markwon/SpannableRenderer.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										442
									
								
								app/src/main/java/ru/noties/markwon/SpannableRenderer.java
									
									
									
									
									
										Normal file
									
								
							| @ -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<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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										199
									
								
								app/src/main/java/ru/noties/markwon/spans/CodeSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								app/src/main/java/ru/noties/markwon/spans/CodeSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
| //    } | ||||
| } | ||||
							
								
								
									
										97
									
								
								app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										116
									
								
								app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java
									
									
									
									
									
										Normal file
									
								
							| @ -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<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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								app/src/main/java/ru/noties/markwon/spans/SubSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/java/ru/noties/markwon/spans/SubSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								app/src/main/java/ru/noties/markwon/spans/SupSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/java/ru/noties/markwon/spans/SupSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								app/src/main/res/layout/activity_main.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/main/res/layout/activity_main.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| <?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"> | ||||
| 
 | ||||
|     <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"/> | ||||
| 
 | ||||
| </ScrollView> | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 7.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										6
									
								
								app/src/main/res/values-w820dp/dimens.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app/src/main/res/values-w820dp/dimens.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <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> | ||||
							
								
								
									
										6
									
								
								app/src/main/res/values/colors.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app/src/main/res/values/colors.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <color name="colorPrimary">#3F51B5</color> | ||||
|     <color name="colorPrimaryDark">#303F9F</color> | ||||
|     <color name="colorAccent">#FF4081</color> | ||||
| </resources> | ||||
							
								
								
									
										5
									
								
								app/src/main/res/values/dimens.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/values/dimens.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| <resources> | ||||
|     <!-- Default screen margins, per the Android Design guidelines. --> | ||||
|     <dimen name="activity_horizontal_margin">16dp</dimen> | ||||
|     <dimen name="activity_vertical_margin">16dp</dimen> | ||||
| </resources> | ||||
							
								
								
									
										3
									
								
								app/src/main/res/values/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/src/main/res/values/strings.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| <resources> | ||||
|     <string name="app_name">Markwon</string> | ||||
| </resources> | ||||
							
								
								
									
										11
									
								
								app/src/main/res/values/styles.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/src/main/res/values/styles.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| <resources> | ||||
| 
 | ||||
|     <!-- 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> | ||||
| 
 | ||||
| </resources> | ||||
							
								
								
									
										17
									
								
								app/src/test/java/ru/noties/markwon/ExampleUnitTest.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/test/java/ru/noties/markwon/ExampleUnitTest.java
									
									
									
									
									
										Normal file
									
								
							| @ -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 <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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										36
									
								
								build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								build.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||
| } | ||||
							
								
								
									
										9
									
								
								gradle.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								gradle.properties
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										6
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										18
									
								
								library-spans/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								library-spans/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
| @ -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 <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()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								library-spans/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								library-spans/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <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> | ||||
							
								
								
									
										3
									
								
								library-spans/src/main/res/values/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								library-spans/src/main/res/values/strings.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| <resources> | ||||
|     <string name="app_name">Spans</string> | ||||
| </resources> | ||||
| @ -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 <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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1
									
								
								settings.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								settings.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| include ':app', ':library-spans' | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov