Custom extension sample (#27)
* Initialized custom extension module * Upgraded android gradle plugin to 3.0.1 * Shaping up the custom extension module * Added README to module * Small improvement for IconVisitor (added color null check)
| @ -27,13 +27,13 @@ android { | ||||
| 
 | ||||
| dependencies { | ||||
| 
 | ||||
|     compile project(':library') | ||||
|     compile project(':library-image-loader') | ||||
|     implementation project(':library') | ||||
|     implementation project(':library-image-loader') | ||||
| 
 | ||||
|     compile 'ru.noties:debug:3.0.0@jar' | ||||
|     implementation 'ru.noties:debug:3.0.0@jar' | ||||
| 
 | ||||
|     compile OK_HTTP | ||||
|     implementation OK_HTTP | ||||
| 
 | ||||
|     compile 'com.google.dagger:dagger:2.10' | ||||
|     implementation 'com.google.dagger:dagger:2.10' | ||||
|     annotationProcessor 'com.google.dagger:dagger-compiler:2.10' | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,7 @@ buildscript { | ||||
|         google() | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:2.3.3' | ||||
|         classpath 'com.android.tools.build:gradle:3.0.1' | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -22,7 +22,7 @@ task clean(type: Delete) { | ||||
| } | ||||
| 
 | ||||
| task wrapper(type: Wrapper) { | ||||
|     gradleVersion '4.3' | ||||
|     gradleVersion '4.5' | ||||
|     distributionType 'all' | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-all.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip | ||||
|  | ||||
| @ -19,10 +19,10 @@ android { | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     compile project(':library') | ||||
|     compile ANDROID_SVG | ||||
|     compile ANDROID_GIF | ||||
|     compile OK_HTTP | ||||
|     api project(':library') | ||||
|     api ANDROID_SVG | ||||
|     api ANDROID_GIF | ||||
|     api OK_HTTP | ||||
| } | ||||
| 
 | ||||
| if (project.hasProperty('release')) { | ||||
|  | ||||
| @ -14,8 +14,8 @@ android { | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     compile project(':library') | ||||
|     provided SUPPORT_APP_COMPAT | ||||
|     api project(':library') | ||||
|     compileOnly SUPPORT_APP_COMPAT | ||||
| } | ||||
| 
 | ||||
| if (project.hasProperty('release')) { | ||||
|  | ||||
| @ -14,10 +14,10 @@ android { | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     compile SUPPORT_ANNOTATIONS | ||||
|     compile COMMON_MARK | ||||
|     compile COMMON_MARK_STRIKETHROUGHT | ||||
|     compile COMMON_MARK_TABLE | ||||
|     api SUPPORT_ANNOTATIONS | ||||
|     api COMMON_MARK | ||||
|     api COMMON_MARK_STRIKETHROUGHT | ||||
|     api COMMON_MARK_TABLE | ||||
| } | ||||
| 
 | ||||
| if (project.hasProperty('release')) { | ||||
|  | ||||
							
								
								
									
										26
									
								
								sample-custom-extension/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,26 @@ | ||||
| # Custom extension | ||||
| 
 | ||||
| This module provides a simple implementation for icons that are bundled in your application resources using custom `DelimiterProcessor`. It can be used as a reference when dealing with new functionality based on _delimiters_.  | ||||
| 
 | ||||
| ```markdown | ||||
| # Hello @ic-android-black-24 | ||||
| 
 | ||||
| **Please** click @ic-home-green-24 (home icon) if you want to go home. | ||||
| ``` | ||||
| 
 | ||||
| Here we will substitute elements starting with `@ic-` for icons that we have in our resources: | ||||
| 
 | ||||
| * `@ic-android-black-24` -> `R.drawable.ic_android_black_24dp` | ||||
| * `@ic-home-green-24` -> `R.drawable.ic_home_green_24dp` | ||||
| 
 | ||||
| In order to provide reliable parsing we need to have delimiters _around_ desired content. So, `@ic-home-green-24` would become `@ic-home-green-24@`. This is current limitation of [commonmark-java](https://github.com/atlassian/commonmark-java) library that Markwon uses underneath. There is an ongoing [issue](https://github.com/atlassian/commonmark-java/issues/113) that might change this in future thought. | ||||
| 
 | ||||
| But as we known the pattern beforehand it's pretty easy to pre-process raw markdown and make it the way we want it. Please refer to `IconProcessor#process` method for the reference. | ||||
| 
 | ||||
| So, the our steps would be: | ||||
| 
 | ||||
| * prepare raw markdown (wrap icons with `@` if it's not already) | ||||
| * construct a Parser with our registered delimiter processor | ||||
| * parse markdown and obtain a `Node` | ||||
| * create a node visitor that will additionally visit custom node (`IconNode`) | ||||
| * use markdown | ||||
							
								
								
									
										22
									
								
								sample-custom-extension/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,22 @@ | ||||
| apply plugin: 'com.android.application' | ||||
| 
 | ||||
| android { | ||||
| 
 | ||||
|     compileSdkVersion TARGET_SDK | ||||
|     buildToolsVersion BUILD_TOOLS | ||||
| 
 | ||||
|     defaultConfig { | ||||
| 
 | ||||
|         applicationId "ru.noties.markwon.sample.extension" | ||||
| 
 | ||||
|         // using 21 as minimum only to be able to vector assets | ||||
|         minSdkVersion 21 | ||||
|         targetSdkVersion TARGET_SDK | ||||
|         versionCode 1 | ||||
|         versionName version | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     implementation project(':library') | ||||
| } | ||||
							
								
								
									
										21
									
								
								sample-custom-extension/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,21 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="ru.noties.markwon.sample.extension"> | ||||
| 
 | ||||
|     <application | ||||
|         android:allowBackup="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:roundIcon="@mipmap/ic_launcher_round" | ||||
|         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> | ||||
| @ -0,0 +1,8 @@ | ||||
| package ru.noties.markwon.sample.extension; | ||||
| 
 | ||||
| import org.commonmark.node.CustomNode; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public class IconGroupNode extends CustomNode { | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,61 @@ | ||||
| package ru.noties.markwon.sample.extension; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import org.commonmark.node.CustomNode; | ||||
| import org.commonmark.node.Delimited; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public class IconNode extends CustomNode implements Delimited { | ||||
| 
 | ||||
|     public static final char DELIMITER = '@'; | ||||
| 
 | ||||
|     public static final String DELIMITER_STRING = "" + DELIMITER; | ||||
| 
 | ||||
| 
 | ||||
|     private final String name; | ||||
| 
 | ||||
|     private final String color; | ||||
| 
 | ||||
|     private final String size; | ||||
| 
 | ||||
|     public IconNode(@NonNull String name, @NonNull String color, @NonNull String size) { | ||||
|         this.name = name; | ||||
|         this.color = color; | ||||
|         this.size = size; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public String name() { | ||||
|         return name; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public String color() { | ||||
|         return color; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public String size() { | ||||
|         return size; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getOpeningDelimiter() { | ||||
|         return DELIMITER_STRING; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getClosingDelimiter() { | ||||
|         return DELIMITER_STRING; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return "IconNode{" + | ||||
|                 "name='" + name + '\'' + | ||||
|                 ", color='" + color + '\'' + | ||||
|                 ", size='" + size + '\'' + | ||||
|                 '}'; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,160 @@ | ||||
| package ru.noties.markwon.sample.extension; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.TextUtils; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.node.Text; | ||||
| import org.commonmark.parser.delimiter.DelimiterProcessor; | ||||
| import org.commonmark.parser.delimiter.DelimiterRun; | ||||
| 
 | ||||
| import java.util.regex.Matcher; | ||||
| import java.util.regex.Pattern; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public class IconProcessor implements DelimiterProcessor { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static IconProcessor create() { | ||||
|         return new IconProcessor(); | ||||
|     } | ||||
| 
 | ||||
|     // ic-home-black-24 | ||||
|     private static final Pattern PATTERN = Pattern.compile("ic-(\\w+)-(\\w+)-(\\d+)"); | ||||
| 
 | ||||
|     private static final String TO_FIND = IconNode.DELIMITER_STRING + "ic-"; | ||||
| 
 | ||||
|     /** | ||||
|      * Should be used when input string does not wrap icon definition with `@` from both ends. | ||||
|      * So, `@ic-home-white-24` would become `@ic-home-white-24@`. This way parsing is easier | ||||
|      * and more predictable (cannot specify multiple ending delimiters, as we would require them: | ||||
|      * space, newline, end of a document, and a lot of more) | ||||
|      * | ||||
|      * @param input to process | ||||
|      * @return processed string | ||||
|      * @see #prepare(StringBuilder) | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static String prepare(@NonNull String input) { | ||||
|         final StringBuilder builder = new StringBuilder(input); | ||||
|         prepare(builder); | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     public static void prepare(@NonNull StringBuilder builder) { | ||||
| 
 | ||||
|         int start = builder.indexOf(TO_FIND); | ||||
|         int end; | ||||
| 
 | ||||
|         while (start > -1) { | ||||
| 
 | ||||
|             end = iconDefinitionEnd(start + TO_FIND.length(), builder); | ||||
| 
 | ||||
|             // if we match our pattern, append `@` else ignore | ||||
|             if (iconDefinitionValid(builder.subSequence(start + 1, end))) { | ||||
|                 builder.insert(end, '@'); | ||||
|             } | ||||
| 
 | ||||
|             // move to next | ||||
|             start = builder.indexOf(TO_FIND, end); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public char getOpeningCharacter() { | ||||
|         return IconNode.DELIMITER; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public char getClosingCharacter() { | ||||
|         return IconNode.DELIMITER; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getMinLength() { | ||||
|         return 1; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) { | ||||
|         return opener.length() >= 1 && closer.length() >= 1 ? 1 : 0; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void process(Text opener, Text closer, int delimiterUse) { | ||||
| 
 | ||||
|         final IconGroupNode iconGroupNode = new IconGroupNode(); | ||||
| 
 | ||||
|         final Node next = opener.getNext(); | ||||
| 
 | ||||
|         boolean handled = false; | ||||
| 
 | ||||
|         // process only if we have exactly one Text node | ||||
|         if (next instanceof Text && next.getNext() == closer) { | ||||
| 
 | ||||
|             final String text = ((Text) next).getLiteral(); | ||||
| 
 | ||||
|             if (!TextUtils.isEmpty(text)) { | ||||
| 
 | ||||
|                 // attempt to match | ||||
|                 final Matcher matcher = PATTERN.matcher(text); | ||||
|                 if (matcher.matches()) { | ||||
|                     final IconNode iconNode = new IconNode( | ||||
|                             matcher.group(1), | ||||
|                             matcher.group(2), | ||||
|                             matcher.group(3) | ||||
|                     ); | ||||
|                     iconGroupNode.appendChild(iconNode); | ||||
|                     next.unlink(); | ||||
|                     handled = true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (!handled) { | ||||
| 
 | ||||
|             // restore delimiters if we didn't match | ||||
| 
 | ||||
|             iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING)); | ||||
| 
 | ||||
|             Node node; | ||||
|             for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) { | ||||
|                 node = tmp.getNext(); | ||||
|                 // append a child anyway | ||||
|                 iconGroupNode.appendChild(tmp); | ||||
|             } | ||||
| 
 | ||||
|             iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING)); | ||||
|         } | ||||
| 
 | ||||
|         opener.insertBefore(iconGroupNode); | ||||
|     } | ||||
| 
 | ||||
|     private static int iconDefinitionEnd(int index, @NonNull StringBuilder builder) { | ||||
| 
 | ||||
|         // all spaces, new lines, non-words or digits, | ||||
| 
 | ||||
|         char c; | ||||
| 
 | ||||
|         int end = -1; | ||||
|         for (int i = index; i < builder.length(); i++) { | ||||
|             c = builder.charAt(i); | ||||
|             if (Character.isWhitespace(c) | ||||
|                     || !(Character.isLetterOrDigit(c) || c == '-' || c == '_')) { | ||||
|                 end = i; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (end == -1) { | ||||
|             end = builder.length(); | ||||
|         } | ||||
| 
 | ||||
|         return end; | ||||
|     } | ||||
| 
 | ||||
|     private static boolean iconDefinitionValid(@NonNull CharSequence cs) { | ||||
|         final Matcher matcher = PATTERN.matcher(cs); | ||||
|         return matcher.matches(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,77 @@ | ||||
| package ru.noties.markwon.sample.extension; | ||||
| 
 | ||||
| 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.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.style.ReplacementSpan; | ||||
| 
 | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public class IconSpan extends ReplacementSpan { | ||||
| 
 | ||||
|     @IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER}) | ||||
|     @Retention(RetentionPolicy.CLASS) | ||||
|     @interface Alignment { | ||||
|     } | ||||
| 
 | ||||
|     public static final int ALIGN_BOTTOM = 0; | ||||
|     public static final int ALIGN_BASELINE = 1; | ||||
|     public static final int ALIGN_CENTER = 2; // will only center if drawable height is less than text line height | ||||
| 
 | ||||
| 
 | ||||
|     private final Drawable drawable; | ||||
| 
 | ||||
|     private final int alignment; | ||||
| 
 | ||||
|     public IconSpan(@NonNull Drawable drawable, @Alignment int alignment) { | ||||
|         this.drawable = drawable; | ||||
|         this.alignment = alignment; | ||||
|         if (drawable.getBounds().isEmpty()) { | ||||
|             drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getSize(@NonNull Paint paint, CharSequence text, int start, 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, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { | ||||
| 
 | ||||
|         final int b = bottom - drawable.getBounds().bottom; | ||||
| 
 | ||||
|         final int save = canvas.save(); | ||||
|         try { | ||||
|             final int translationY; | ||||
|             if (ALIGN_CENTER == alignment) { | ||||
|                 translationY = b - ((bottom - top - drawable.getBounds().height()) / 2); | ||||
|             } else if (ALIGN_BASELINE == alignment) { | ||||
|                 translationY = b - paint.getFontMetricsInt().descent; | ||||
|             } else { | ||||
|                 translationY = b; | ||||
|             } | ||||
|             canvas.translate(x, translationY); | ||||
|             drawable.draw(canvas); | ||||
|         } finally { | ||||
|             canvas.restoreToCount(save); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,67 @@ | ||||
| package ru.noties.markwon.sample.extension; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.res.Resources; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.os.Build; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public abstract class IconSpanProvider { | ||||
| 
 | ||||
|     @SuppressWarnings("SameParameterValue") | ||||
|     @NonNull | ||||
|     public static IconSpanProvider create(@NonNull Context context, @DrawableRes int fallBack) { | ||||
|         return new Impl(context, fallBack); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @NonNull | ||||
|     public abstract IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size); | ||||
| 
 | ||||
| 
 | ||||
|     private static class Impl extends IconSpanProvider { | ||||
| 
 | ||||
|         private static final boolean IS_L = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; | ||||
| 
 | ||||
|         private final Context context; | ||||
|         private final Resources resources; | ||||
|         private final int fallBack; | ||||
| 
 | ||||
|         Impl(@NonNull Context context, @DrawableRes int fallBack) { | ||||
|             this.context = context; | ||||
|             this.resources = context.getResources(); | ||||
|             this.fallBack = fallBack; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size) { | ||||
|             final String resName = iconName(name, color, size); | ||||
|             int resId = resources.getIdentifier(resName, "drawable", context.getPackageName()); | ||||
|             if (resId == 0) { | ||||
|                 resId = fallBack; | ||||
|             } | ||||
|             return new IconSpan(getDrawable(resId), IconSpan.ALIGN_CENTER); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         @NonNull | ||||
|         private static String iconName(@NonNull String name, @NonNull String color, @NonNull String size) { | ||||
|             return "ic_" + name + "_" + color + "_" + size + "dp"; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         private Drawable getDrawable(int resId) { | ||||
|             final Drawable drawable; | ||||
|             if (IS_L) { | ||||
|                 drawable = context.getDrawable(resId); | ||||
|             } else { | ||||
|                 drawable = resources.getDrawable(resId); | ||||
|             } | ||||
|             //noinspection ConstantConditions | ||||
|             return drawable; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,63 @@ | ||||
| package ru.noties.markwon.sample.extension; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.TextUtils; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.CustomNode; | ||||
| 
 | ||||
| import ru.noties.markwon.SpannableBuilder; | ||||
| import ru.noties.markwon.SpannableConfiguration; | ||||
| import ru.noties.markwon.renderer.SpannableMarkdownVisitor; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public class IconVisitor extends SpannableMarkdownVisitor { | ||||
| 
 | ||||
|     private final SpannableBuilder builder; | ||||
| 
 | ||||
|     private final IconSpanProvider iconSpanProvider; | ||||
| 
 | ||||
|     public IconVisitor( | ||||
|             @NonNull SpannableConfiguration configuration, | ||||
|             @NonNull SpannableBuilder builder, | ||||
|             @NonNull IconSpanProvider iconSpanProvider | ||||
|     ) { | ||||
|         super(configuration, builder); | ||||
|         this.builder = builder; | ||||
|         this.iconSpanProvider = iconSpanProvider; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(CustomNode customNode) { | ||||
|         if (!visitIconNode(customNode)) { | ||||
|             super.visit(customNode); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private boolean visitIconNode(@NonNull CustomNode customNode) { | ||||
| 
 | ||||
|         if (customNode instanceof IconNode) { | ||||
| 
 | ||||
|             final IconNode node = (IconNode) customNode; | ||||
| 
 | ||||
|             final String name = node.name(); | ||||
|             final String color = node.color(); | ||||
|             final String size = node.size(); | ||||
| 
 | ||||
|             if (!TextUtils.isEmpty(name) | ||||
|                     && !TextUtils.isEmpty(color) | ||||
|                     && !TextUtils.isEmpty(size)) { | ||||
| 
 | ||||
|                 final int length = builder.length(); | ||||
| 
 | ||||
|                 builder.append(name); | ||||
|                 builder.setSpan(iconSpanProvider.provide(name, color, size), length); | ||||
|                 builder.append(' '); | ||||
|                  | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,66 @@ | ||||
| package ru.noties.markwon.sample.extension; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.os.Bundle; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; | ||||
| import org.commonmark.ext.gfm.tables.TablesExtension; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| 
 | ||||
| import ru.noties.markwon.SpannableBuilder; | ||||
| import ru.noties.markwon.SpannableConfiguration; | ||||
| import ru.noties.markwon.tasklist.TaskListExtension; | ||||
| 
 | ||||
| public class MainActivity extends Activity { | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
| 
 | ||||
|         setContentView(R.layout.activity_main); | ||||
| 
 | ||||
|         final TextView textView = findViewById(R.id.text_view); | ||||
| 
 | ||||
|         // obtain an instance of parser | ||||
|         final Parser parser = new Parser.Builder() | ||||
|                 // we will register all known to Markwon extensions | ||||
|                 .extensions(Arrays.asList( | ||||
|                         StrikethroughExtension.create(), | ||||
|                         TablesExtension.create(), | ||||
|                         TaskListExtension.create() | ||||
|                 )) | ||||
|                 // this is the handler for custom icons | ||||
|                 .customDelimiterProcessor(IconProcessor.create()) | ||||
|                 .build(); | ||||
| 
 | ||||
|         // we process input to wrap icon definitions with `@` on both ends | ||||
|         // if your input already does it, there is not need for `IconProcessor.prepare()` call. | ||||
|         final String markdown = IconProcessor.prepare(getString(R.string.input)); | ||||
| 
 | ||||
|         final Node node = parser.parse(markdown); | ||||
| 
 | ||||
|         final SpannableBuilder builder = new SpannableBuilder(); | ||||
| 
 | ||||
|         // please note that here I am passing `0` as fallback it means that if markdown references | ||||
|         // unknown icon, it will try to load fallback one and will fail with ResourceNotFound. It's | ||||
|         // better to provide a valid fallback option | ||||
|         final IconSpanProvider spanProvider = IconSpanProvider.create(this, 0); | ||||
| 
 | ||||
|         // create an instance of visitor to process parsed markdown | ||||
|         final IconVisitor visitor = new IconVisitor( | ||||
|                 SpannableConfiguration.create(this), | ||||
|                 builder, | ||||
|                 spanProvider | ||||
|         ); | ||||
| 
 | ||||
|         // trigger visit | ||||
|         node.accept(visitor); | ||||
| 
 | ||||
|         // apply | ||||
|         textView.setText(builder.text()); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,34 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:aapt="http://schemas.android.com/aapt" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportHeight="108" | ||||
|     android:viewportWidth="108"> | ||||
|     <path | ||||
|         android:fillType="evenOdd" | ||||
|         android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" | ||||
|         android:strokeColor="#00000000" | ||||
|         android:strokeWidth="1"> | ||||
|         <aapt:attr name="android:fillColor"> | ||||
|             <gradient | ||||
|                 android:endX="78.5885" | ||||
|                 android:endY="90.9159" | ||||
|                 android:startX="48.7653" | ||||
|                 android:startY="61.0927" | ||||
|                 android:type="linear"> | ||||
|                 <item | ||||
|                     android:color="#44000000" | ||||
|                     android:offset="0.0" /> | ||||
|                 <item | ||||
|                     android:color="#00000000" | ||||
|                     android:offset="1.0" /> | ||||
|             </gradient> | ||||
|         </aapt:attr> | ||||
|     </path> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFF" | ||||
|         android:fillType="nonZero" | ||||
|         android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" | ||||
|         android:strokeColor="#00000000" | ||||
|         android:strokeWidth="1" /> | ||||
| </vector> | ||||
| @ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"/> | ||||
| </vector> | ||||
| @ -0,0 +1,4 @@ | ||||
| <vector android:height="36dp" android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0" android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="#FF000000" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/> | ||||
| </vector> | ||||
| @ -0,0 +1,170 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportHeight="108" | ||||
|     android:viewportWidth="108"> | ||||
|     <path | ||||
|         android:fillColor="#26A69A" | ||||
|         android:pathData="M0,0h108v108h-108z" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M9,0L9,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,0L19,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,0L29,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,0L39,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,0L49,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,0L59,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,0L69,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,0L79,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M89,0L89,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M99,0L99,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,9L108,9" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,19L108,19" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,29L108,29" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,39L108,39" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,49L108,49" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,59L108,59" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,69L108,69" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,79L108,79" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,89L108,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,99L108,99" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,29L89,29" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,39L89,39" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,49L89,49" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,59L89,59" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,69L89,69" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,79L89,79" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,19L29,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,19L39,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,19L49,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,19L59,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,19L69,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,19L79,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
| </vector> | ||||
| @ -0,0 +1,4 @@ | ||||
| <vector android:height="48dp" android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="#FF000000" android:pathData="M15,9L9,9v6h6L15,9zM13,13h-2v-2h2v2zM21,11L21,9h-2L19,7c0,-1.1 -0.9,-2 -2,-2h-2L15,3h-2v2h-2L11,3L9,3v2L7,5c-1.1,0 -2,0.9 -2,2v2L3,9v2h2v2L3,13v2h2v2c0,1.1 0.9,2 2,2h2v2h2v-2h2v2h2v-2h2c1.1,0 2,-0.9 2,-2v-2h2v-2h-2v-2h2zM17,17L7,17L7,7h10v10z"/> | ||||
| </vector> | ||||
| @ -0,0 +1,6 @@ | ||||
| <vector android:height="64dp" android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="#FFFF0000" android:pathData="M15.5,9.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/> | ||||
|     <path android:fillColor="#FFFF0000" android:pathData="M8.5,9.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/> | ||||
|     <path android:fillColor="#FFFF0000" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12,16c-1.48,0 -2.75,-0.81 -3.45,-2L6.88,14c0.8,2.05 2.79,3.5 5.12,3.5s4.32,-1.45 5.12,-3.5h-1.67c-0.7,1.19 -1.97,2 -3.45,2z"/> | ||||
| </vector> | ||||
| @ -0,0 +1,16 @@ | ||||
| <?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 | ||||
|         android:id="@+id/text_view" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:padding="8dip" | ||||
|         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|         tools:text="@string/input"/> | ||||
| 
 | ||||
| </ScrollView> | ||||
| @ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@drawable/ic_launcher_background" /> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground" /> | ||||
| </adaptive-icon> | ||||
| @ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@drawable/ic_launcher_background" /> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground" /> | ||||
| </adaptive-icon> | ||||
							
								
								
									
										
											BIN
										
									
								
								sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.0 KiB | 
| After Width: | Height: | Size: 4.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 KiB | 
| After Width: | Height: | Size: 2.8 KiB | 
| After Width: | Height: | Size: 4.5 KiB | 
| After Width: | Height: | Size: 6.9 KiB | 
| After Width: | Height: | Size: 6.3 KiB | 
| After Width: | Height: | Size: 10 KiB | 
| After Width: | Height: | Size: 9.0 KiB | 
| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										13
									
								
								sample-custom-extension/src/main/res/values/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,13 @@ | ||||
| <resources> | ||||
| 
 | ||||
|     <string name="app_name">Markwon-SampleCustomExtension</string> | ||||
| 
 | ||||
|     <string name="input"><![CDATA[ | ||||
|         # Hello! @ic-android-black-24\n\n | ||||
|         Home 36 black: @ic-home-black-36\n\n | ||||
|         Memory 48 black: @ic-memory-black-48\n\n | ||||
|         Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64 | ||||
|     ]]> | ||||
|     </string> | ||||
| 
 | ||||
| </resources> | ||||
							
								
								
									
										5
									
								
								sample-custom-extension/src/main/res/values/styles.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,5 @@ | ||||
| <resources> | ||||
| 
 | ||||
|     <style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar"/> | ||||
| 
 | ||||
| </resources> | ||||
| @ -1 +1 @@ | ||||
| include ':app', ':library', ':library-image-loader', ':library-view' | ||||
| include ':app', ':library', ':library-image-loader', ':library-view', ':sample-custom-extension' | ||||
|  | ||||
 Dimitry
						Dimitry