diff --git a/build.gradle b/build.gradle index 88a298fa..f0a79879 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,7 @@ ext { 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'prism4j' : 'ru.noties:prism4j:1.1.0', 'debug' : 'ru.noties:debug:3.0.0@jar', + 'adapt' : 'ru.noties:adapt:1.1.0', 'dagger' : "com.google.dagger:dagger:$daggerVersion" ] diff --git a/sample/build.gradle b/sample/build.gradle index 3d520a29..eb169fa3 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -12,11 +12,23 @@ android { versionCode 1 versionName version setProperty("archivesBaseName", "markwon-sample-$versionName") + + resConfig 'en' } lintOptions { abortOnError false } + + dexOptions { + preDexLibraries true + javaMaxHeapSize '5g' + } + + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } } dependencies { @@ -31,12 +43,20 @@ dependencies { implementation project(':markwon-syntax-highlight') deps.with { + implementation it['support-recycler-view'] implementation it['okhttp'] implementation it['prism4j'] implementation it['debug'] + implementation it['adapt'] } deps['annotationProcessor'].with { annotationProcessor it['prism4j-bundler'] } + + deps['test'].with { + testImplementation it['junit'] + testImplementation it['robolectric'] + testImplementation it['mockito'] + } } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 4907b9ea..73b776c3 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -18,6 +18,10 @@ + + + + \ No newline at end of file diff --git a/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java index 4670347f..d1fe2853 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java @@ -1,12 +1,96 @@ package ru.noties.markwon.sample; import android.app.Activity; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import java.util.Arrays; + +import ru.noties.adapt.Adapt; +import ru.noties.adapt.OnClickViewProcessor; +import ru.noties.debug.AndroidLogDebugOutput; +import ru.noties.debug.Debug; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.sample.core.CoreActivity; +import ru.noties.markwon.sample.customextension.CustomExtensionActivity; +import ru.noties.markwon.sample.latex.LatexActivity; public class MainActivity extends Activity { + static { + Debug.init(new AndroidLogDebugOutput(true)); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // obtain an instance of Markwon + // here we are creating as core markwon (no additional plugins are registered) + final Markwon markwon = Markwon.create(this); + + final Adapt adapt = Adapt.builder(SampleItem.class) + .include(SampleItem.class, new SampleItemView(markwon), new OnClickViewProcessor() { + @Override + public void onClick(@NonNull SampleItem item, @NonNull View view) { + showSample(item); + } + }) + .build(); + adapt.setItems(Arrays.asList(SampleItem.values())); + + final RecyclerView recyclerView = findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(createSampleItemDecoration()); + recyclerView.setAdapter(adapt.recyclerViewAdapter()); + } + + @NonNull + private SampleItemDecoration createSampleItemDecoration() { + final float density = getResources().getDisplayMetrics().density; + return new SampleItemDecoration( + 0xffeeeeee, + (int) (24 * density + .5F), + (int) (1 * density + .5F), + 0xFFBDBDBD + ); + } + + private void showSample(@NonNull SampleItem item) { + startActivity(sampleItemIntent(this, item)); + } + + @VisibleForTesting + static Intent sampleItemIntent(@NonNull Context context, @NonNull SampleItem item) { + + final Class activity; + + switch (item) { + + case CORE: + activity = CoreActivity.class; + break; + + case LATEX: + activity = LatexActivity.class; + break; + + case CUSTOM_EXTENSION: + activity = CustomExtensionActivity.class; + break; + + default: + throw new IllegalStateException("No Activity is associated with sample-item: " + item); + } + + return new Intent(context, activity); } } diff --git a/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java b/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java new file mode 100644 index 00000000..bf3038ac --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java @@ -0,0 +1,26 @@ +package ru.noties.markwon.sample; + +import android.support.annotation.StringRes; + +public enum SampleItem { + + // all usages of markwon without plugins (parse, render, setMarkwon, etc) + CORE(R.string.sample_core), + + LATEX(R.string.sample_latex), + + CUSTOM_EXTENSION(R.string.sample_custom_extension), + + ; + + private final int textResId; + + SampleItem(@StringRes int textResId) { + this.textResId = textResId; + } + + @StringRes + public int textResId() { + return textResId; + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/SampleItemDecoration.java b/sample/src/main/java/ru/noties/markwon/sample/SampleItemDecoration.java new file mode 100644 index 00000000..580b2763 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItemDecoration.java @@ -0,0 +1,97 @@ +package ru.noties.markwon.sample; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.annotation.ColorInt; +import android.support.annotation.Px; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +class SampleItemDecoration extends RecyclerView.ItemDecoration { + + private final Rect rect = new Rect(); + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final int oddItemBackgroundColor; + + private final int bottomPadding; + + private final int dividerHeight; + private final int dividerColor; + + SampleItemDecoration( + @ColorInt int oddItemBackgroundColor, + @Px int bottomPadding, + @Px int dividerHeight, + @ColorInt int dividerColor) { + this.oddItemBackgroundColor = oddItemBackgroundColor; + this.bottomPadding = bottomPadding; + this.dividerHeight = dividerHeight; + this.dividerColor = dividerColor; + + paint.setStyle(Paint.Style.FILL); + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + + // if bottom < parent.getBottom() -> draw bottom background + + paint.setColor(dividerColor); + + View view; + + // we will use this flag afterwards (if we will have to draw bottom background) + // so, if last item is even (no background) -> draw odd + // if last item is odd -> draw no background + // + // let's start with true, so if we have no items no background will be drawn + boolean isOdd = true; + + for (int i = 0, count = parent.getChildCount(); i < count; i++) { + + view = parent.getChildAt(i); + isOdd = parent.getChildAdapterPosition(view) % 2 != 0; + + // odd + if (isOdd) { + rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + paint.setColor(oddItemBackgroundColor); + c.drawRect(rect, paint); + + // set divider color back + paint.setColor(dividerColor); + } + + rect.set(0, view.getBottom(), c.getWidth(), view.getBottom() + dividerHeight); + c.drawRect(rect, paint); + } + + if (!isOdd && rect.bottom < parent.getBottom()) { + + paint.setColor(oddItemBackgroundColor); + + rect.set(0, rect.bottom, c.getWidth(), parent.getBottom()); + c.drawRect(rect, paint); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + + // divider to bottom + // + {if last} -> bottomPadding + + final int position = parent.getChildAdapterPosition(view); + + final RecyclerView.Adapter adapter = parent.getAdapter(); + final boolean isLast = adapter != null && position == adapter.getItemCount() - 1; + + final int bottom = isLast + ? bottomPadding + dividerHeight + : dividerHeight; + + outRect.set(0, 0, 0, bottom); + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/SampleItemView.java b/sample/src/main/java/ru/noties/markwon/sample/SampleItemView.java new file mode 100644 index 00000000..76289022 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItemView.java @@ -0,0 +1,84 @@ +package ru.noties.markwon.sample; + +import android.support.annotation.NonNull; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.EnumMap; + +import ru.noties.adapt.Holder; +import ru.noties.adapt.ItemView; +import ru.noties.markwon.Markwon; + +class SampleItemView extends ItemView { + + private final Markwon markwon; + + // instance specific factory + private final NoCopySpannableFactory factory; + + // instance specific cache + private final EnumMap cache; + + SampleItemView(@NonNull Markwon markwon) { + this.markwon = markwon; + this.factory = new NoCopySpannableFactory(); + this.cache = new EnumMap<>(SampleItem.class); + } + + @NonNull + @Override + public SampleHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + + final SampleHolder holder = new SampleHolder(inflater.inflate( + R.layout.adapt_sample_item, + parent, + false)); + + // set Spannable.Factory so when TextView will receive a new content + // it won't create new Spannable and copy all the spans but instead + // re-use existing Spannable thus improving performance + holder.textView.setSpannableFactory(factory); + + return holder; + } + + @Override + public void bindHolder(@NonNull SampleHolder holder, @NonNull SampleItem item) { + + // retrieve an item from cache or create new one + // simple lazy loading pattern (cache on first call then re-use) + Spanned spanned = cache.get(item); + if (spanned == null) { + spanned = markwon.toMarkdown(context(holder).getString(item.textResId())); + cache.put(item, spanned); + } + + holder.textView.setText(spanned); + } + + static class SampleHolder extends Holder { + + final TextView textView; + + SampleHolder(@NonNull View view) { + super(view); + + this.textView = requireView(R.id.text); + } + } + + private static class NoCopySpannableFactory extends Spannable.Factory { + @Override + public Spannable newSpannable(CharSequence source) { + return source instanceof Spannable + ? (Spannable) source + : new SpannableString(source); + } + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java b/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java new file mode 100644 index 00000000..79687390 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java @@ -0,0 +1,208 @@ +package ru.noties.markwon.sample.core; + +import android.app.Activity; +import android.graphics.Color; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.widget.TextView; +import android.widget.Toast; + +import org.commonmark.node.Heading; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonPlugin; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.core.MarkwonTheme; + +public class CoreActivity extends Activity { + + private TextView textView; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + textView = new TextView(this); + setContentView(textView); + + step_1(); + + step_2(); + + step_3(); + + step_4(); + + step_5(); + + step_6(); + + step_7(); + } + + /** + * Create a simple instance of Markwon with only Core plugin registered + * this will handle all _natively_ supported by commonmark-java nodes: + *
    + *
  • StrongEmphasis
  • + *
  • Emphasis
  • + *
  • BlockQuote
  • + *
  • Code
  • + *
  • FencedCodeBlock
  • + *
  • IndentedCodeBlock
  • + *
  • ListItem (bullet-list and ordered list
  • + *
  • Heading
  • + *
  • Link
  • + *
  • ThematicBreak
  • + *
  • Paragraph (please note that there is no default span for a paragraph registered)
  • + *
+ *

+ * and basic core functionality: + *

    + *
  • Append text
  • + *
  • Insert new lines (soft and hard breaks)
  • + *
+ */ + private void step_1() { + + // short call + final Markwon markwon = Markwon.create(this); + + // this is the same as calling + final Markwon markwon2 = Markwon.builder(this) + .usePlugin(CorePlugin.create()) + .build(); + } + + /** + * To simply apply raw (non-parsed) markdown call {@link Markwon#setMarkdown(TextView, String)} + */ + private void step_2() { + + // this is raw markdown + final String markdown = "Hello **markdown**!"; + + final Markwon markwon = Markwon.create(this); + + // this will parse raw markdown and set parsed content to specified TextView + markwon.setMarkdown(textView, markdown); + } + + /** + * To apply markdown in a different context (other than textView) use {@link Markwon#toMarkdown(String)} + *

+ * Please note that some features won't work unless they are used in a TextView context. For example + * there might be misplaced ordered lists (ordered list must have TextPaint in order to properly measure + * its number). But also images and tables (they belong to independent modules now). Images and tables + * are using some work-arounds in order to be displayed in relatively limited context without proper way + * of invalidation. But if a Toast for example is created with a custom view + * ({@code new Toast(this).setView(...) }) and has access to a TextView everything should work. + */ + private void step_3() { + + final String markdown = "*Toast* __here__!\n\n> And a quote!"; + + final Markwon markwon = Markwon.create(this); + + final Spanned spanned = markwon.toMarkdown(markdown); + + Toast.makeText(this, spanned, Toast.LENGTH_LONG).show(); + } + + /** + * To apply already parsed markdown use {@link Markwon#setParsedMarkdown(TextView, Spanned)} + */ + private void step_4() { + + final String markdown = "This **is** pre-parsed [markdown](#)"; + + final Markwon markwon = Markwon.create(this); + + // parse markdown to obtain a Node + final Node node = markwon.parse(markdown); + + // create a spanned content from parsed node + final Spanned spanned = markwon.render(node); + + // apply parsed markdown + markwon.setParsedMarkdown(textView, spanned); + } + + /** + * In order to apply paragraph spans a custom plugin should be created (CorePlugin will take care + * of everything else). + *

+ * Please note that when a plugin is registered and it depends on CorePlugin, there is no + * need to explicitly specify it. By default all plugins that extend AbstractMarkwonPlugin do declare + * it\'s dependency on CorePlugin ({@link MarkwonPlugin#priority()}). + *

+ * Order in which plugins are specified to the builder is of little importance as long as each + * plugin clearly states what dependencies it has + */ + private void step_5() { + + final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Paragraph.class, (configuration, props) -> + new ForegroundColorSpan(Color.GREEN)); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * To disable some nodes from rendering another custom plugin can be used + */ + private void step_6() { + + final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + // for example to disable rendering of heading: + // try commenting this out to see that otherwise headings will be rendered + builder.on(Heading.class, null); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * To customize core theme plugins can be used again + */ + private void step_7() { + + final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder + .codeBackgroundColor(Color.BLACK) + .codeTextColor(Color.RED); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java new file mode 100644 index 00000000..1b35c0c7 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java @@ -0,0 +1,6 @@ +package ru.noties.markwon.sample.customextension; + +import android.app.Activity; + +public class CustomExtensionActivity extends Activity { +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java new file mode 100644 index 00000000..179113ea --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java @@ -0,0 +1,15 @@ +package ru.noties.markwon.sample.latex; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; + +public class LatexActivity extends Activity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + + } +} diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..26bc9906 --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/adapt_sample_item.xml b/sample/src/main/res/layout/adapt_sample_item.xml new file mode 100644 index 00000000..3dfe4e21 --- /dev/null +++ b/sample/src/main/res/layout/adapt_sample_item.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/values-v21/styles.xml b/sample/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..191be162 --- /dev/null +++ b/sample/src/main/res/values-v21/styles.xml @@ -0,0 +1,6 @@ + + + + +