actions = new LinkedHashMap<>();
+
+ @NonNull
+ public MenuOptions add(@NonNull String title, @NonNull Runnable action) {
+ actions.put(title, action);
+ return this;
+ }
+
+ boolean onCreateOptionsMenu(Menu menu) {
+ if (!actions.isEmpty()) {
+ for (String key : actions.keySet()) {
+ menu.add(key);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Nullable
+ Option onOptionsItemSelected(MenuItem item) {
+ final String title = String.valueOf(item.getTitle());
+ final Runnable action = actions.get(title);
+ if (action != null) {
+ return new Option(title, action);
+ }
+ return null;
+ }
+}
diff --git a/sample/src/main/java/io/noties/markwon/sample/Sample.java b/sample/src/main/java/io/noties/markwon/sample/Sample.java
index 36b13cd2..f18ed25b 100644
--- a/sample/src/main/java/io/noties/markwon/sample/Sample.java
+++ b/sample/src/main/java/io/noties/markwon/sample/Sample.java
@@ -27,7 +27,13 @@ public enum Sample {
INLINE_PARSER(R.string.sample_inline_parser),
- HTML_DETAILS(R.string.sample_html_details);
+ HTML_DETAILS(R.string.sample_html_details),
+
+ TASK_LIST(R.string.sample_task_list),
+
+ IMAGES(R.string.sample_images),
+
+ REMOTE_VIEWS(R.string.sample_remote_views);
private final int textResId;
diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java
index df22cf06..e8bb4761 100644
--- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java
@@ -1,77 +1,94 @@
package io.noties.markwon.sample.basicplugins;
-import android.app.Activity;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
-import android.text.Layout;
+import android.text.Spannable;
+import android.text.Spanned;
import android.text.TextUtils;
-import android.text.style.AlignmentSpan;
import android.text.style.ForegroundColorSpan;
+import android.view.View;
+import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Heading;
+import org.commonmark.node.Node;
import org.commonmark.node.Paragraph;
import java.util.Collection;
import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin;
+import io.noties.markwon.BlockHandlerDef;
+import io.noties.markwon.LinkResolverDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
-import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
-import io.noties.markwon.RenderProps;
+import io.noties.markwon.SoftBreakAddsNewLinePlugin;
+import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme;
-import io.noties.markwon.html.HtmlPlugin;
-import io.noties.markwon.html.HtmlTag;
-import io.noties.markwon.html.tag.SimpleTagHandler;
+import io.noties.markwon.core.spans.HeadingSpan;
+import io.noties.markwon.core.spans.LastLineSpacingSpan;
import io.noties.markwon.image.ImageItem;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.image.SchemeHandler;
import io.noties.markwon.image.network.NetworkSchemeHandler;
import io.noties.markwon.movement.MovementMethodPlugin;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
+import io.noties.markwon.sample.R;
-public class BasicPluginsActivity extends Activity {
+public class BasicPluginsActivity extends ActivityWithMenuOptions {
private TextView textView;
+ private ScrollView scrollView;
+
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("paragraphSpan", this::paragraphSpan)
+ .add("disableNode", this::disableNode)
+ .add("customizeTheme", this::customizeTheme)
+ .add("linkWithMovementMethod", this::linkWithMovementMethod)
+ .add("imagesPlugin", this::imagesPlugin)
+ .add("softBreakAddsSpace", this::softBreakAddsSpace)
+ .add("softBreakAddsNewLine", this::softBreakAddsNewLine)
+ .add("additionalSpacing", this::additionalSpacing)
+ .add("headingNoSpace", this::headingNoSpace)
+ .add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler)
+ .add("allBlocksNoForcedLine", this::allBlocksNoForcedLine)
+ .add("anchor", this::anchor);
+ }
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_text_view);
- textView = new TextView(this);
- setContentView(textView);
+ textView = findViewById(R.id.text_view);
+ scrollView = findViewById(R.id.scroll_view);
- step_1();
-
- step_2();
-
- step_3();
-
- step_4();
-
- step_5();
-
- step_6();
+ paragraphSpan();
+//
+// disableNode();
+//
+// customizeTheme();
+//
+// linkWithMovementMethod();
+//
+// imagesPlugin();
}
/**
* 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_1() {
+ private void paragraphSpan() {
final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!";
@@ -91,7 +108,7 @@ public class BasicPluginsActivity extends Activity {
/**
* To disable some nodes from rendering another custom plugin can be used
*/
- private void step_2() {
+ private void disableNode() {
final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)";
@@ -116,7 +133,7 @@ public class BasicPluginsActivity extends Activity {
/**
* To customize core theme plugin can be used again
*/
- private void step_3() {
+ private void customizeTheme() {
final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```";
@@ -145,7 +162,7 @@ public class BasicPluginsActivity extends Activity {
*
* In order to customize them a custom plugin should be used
*/
- private void step_4() {
+ private void linkWithMovementMethod() {
final String markdown = "[a link without scheme](github.com)";
@@ -178,7 +195,7 @@ public class BasicPluginsActivity extends Activity {
* images handling (parsing markdown containing images, obtain an image from network
* file system or assets). Please note that
*/
- private void step_5() {
+ private void imagesPlugin() {
final String markdown = "";
@@ -220,33 +237,269 @@ public class BasicPluginsActivity extends Activity {
markwon.setMarkdown(textView, markdown);
}
- public void step_6() {
+ private void softBreakAddsSpace() {
+ // default behavior
+
+ final String md = "" +
+ "Hello there ->(line)\n(break)<- going on and on";
+
+ Markwon.create(this).setMarkdown(textView, md);
+ }
+
+ private void softBreakAddsNewLine() {
+ // insert a new line when markdown has a soft break
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(SoftBreakAddsNewLinePlugin.create())
+ .build();
+
+ final String md = "" +
+ "Hello there ->(line)\n(break)<- going on and on";
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void additionalSpacing() {
+
+ // please note that bottom line (after 1 & 2 levels) will be drawn _AFTER_ padding
+ final int spacing = (int) (128 * getResources().getDisplayMetrics().density + .5F);
final Markwon markwon = Markwon.builder(this)
- .usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
- public void configure(@NonNull Registry registry) {
- registry.require(HtmlPlugin.class, plugin -> plugin.addHandler(new SimpleTagHandler() {
- @Override
- public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
- return new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
- }
+ public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
+ builder.headingBreakHeight(0);
+ }
- @NonNull
- @Override
- public Collection supportedTags() {
- return Collections.singleton("center");
- }
- }));
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ builder.appendFactory(
+ Heading.class,
+ (configuration, props) -> new LastLineSpacingSpan(spacing));
}
})
.build();
+
+ final String md = "" +
+ "# Title title title title title title title title title title \n\ntext text text text";
+
+ markwon.setMarkdown(textView, md);
}
+ private void headingNoSpace() {
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
+ builder.headingBreakHeight(0);
+ }
+
+ @Override
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.on(Heading.class, (visitor, heading) -> {
+
+ visitor.ensureNewLine();
+
+ final int length = visitor.length();
+ visitor.visitChildren(heading);
+
+ CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
+
+ visitor.setSpansForNodeOptional(heading, length);
+
+ if (visitor.hasNext(heading)) {
+ visitor.ensureNewLine();
+// visitor.forceNewLine();
+ }
+ });
+ }
+ })
+ .build();
+
+ final String md = "" +
+ "# Title title title title title title title title title title \n\ntext text text text";
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void headingNoSpaceBlockHandler() {
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.blockHandler(new BlockHandlerDef() {
+ @Override
+ public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
+ if (node instanceof Heading) {
+ if (visitor.hasNext(node)) {
+ visitor.ensureNewLine();
+ // ensure new line but do not force insert one
+ }
+ } else {
+ super.blockEnd(visitor, node);
+ }
+ }
+ });
+ }
+ })
+ .build();
+
+ final String md = "" +
+ "# Title title title title title title title title title title \n\ntext text text text";
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void allBlocksNoForcedLine() {
+ final MarkwonVisitor.BlockHandler blockHandler = new BlockHandlerDef() {
+ @Override
+ public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
+ if (visitor.hasNext(node)) {
+ visitor.ensureNewLine();
+ }
+ }
+ };
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.blockHandler(blockHandler);
+ }
+ })
+ .build();
+
+ final String md = "" +
+ "# Hello there!\n\n" +
+ "* a first\n" +
+ "* second\n" +
+ "- third\n" +
+ "* * nested one\n\n" +
+ "> block quote\n\n" +
+ "> > and nested one\n\n" +
+ "```java\n" +
+ "final int i = 0;\n" +
+ "```\n\n";
+
+ markwon.setMarkdown(textView, md);
+ }
+
+// public void step_6() {
+//
+// final Markwon markwon = Markwon.builder(this)
+// .usePlugin(HtmlPlugin.create())
+// .usePlugin(new AbstractMarkwonPlugin() {
+// @Override
+// public void configure(@NonNull Registry registry) {
+// registry.require(HtmlPlugin.class, plugin -> plugin.addHandler(new SimpleTagHandler() {
+// @Override
+// public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
+// return new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
+// }
+//
+// @NonNull
+// @Override
+// public Collection supportedTags() {
+// return Collections.singleton("center");
+// }
+// }));
+// }
+// })
+// .build();
+// }
+
// text lifecycle (after/before)
// rendering lifecycle (before/after)
// renderProps
// process
- // priority
+
+ private static class AnchorSpan {
+ final String anchor;
+
+ AnchorSpan(@NonNull String anchor) {
+ this.anchor = anchor;
+ }
+ }
+
+ @NonNull
+ private String createAnchor(@NonNull CharSequence content) {
+ return String.valueOf(content)
+ .replaceAll("[^\\w]", "")
+ .toLowerCase();
+ }
+
+ private static class AnchorLinkResolver extends LinkResolverDef {
+
+ interface ScrollTo {
+ void scrollTo(@NonNull View view, int top);
+ }
+
+ private final ScrollTo scrollTo;
+
+ AnchorLinkResolver(@NonNull ScrollTo scrollTo) {
+ this.scrollTo = scrollTo;
+ }
+
+ @Override
+ public void resolve(@NonNull View view, @NonNull String link) {
+ if (link.startsWith("#")) {
+ final TextView textView = (TextView) view;
+ final Spanned spanned = (Spannable) textView.getText();
+ final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class);
+ if (spans != null) {
+ final String anchor = link.substring(1);
+ for (AnchorSpan span: spans) {
+ if (anchor.equals(span.anchor)) {
+ final int start = spanned.getSpanStart(span);
+ final int line = textView.getLayout().getLineForOffset(start);
+ final int top = textView.getLayout().getLineTop(line);
+ scrollTo.scrollTo(textView, top);
+ return;
+ }
+ }
+ }
+ }
+ super.resolve(view, link);
+ }
+ }
+
+ private void anchor() {
+ final String lorem = getString(R.string.lorem);
+ final String md = "" +
+ "Hello [there](#there)!\n\n\n" +
+ lorem + "\n\n" +
+ "# There!\n\n" +
+ lorem;
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.linkResolver(new AnchorLinkResolver((view, top) -> scrollView.smoothScrollTo(0, top)));
+ }
+
+ @Override
+ public void afterSetText(@NonNull TextView textView) {
+ final Spannable spannable = (Spannable) textView.getText();
+ // obtain heading spans
+ final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class);
+ if (spans != null) {
+ for (HeadingSpan span : spans) {
+ final int start = spannable.getSpanStart(span);
+ final int end = spannable.getSpanEnd(span);
+ final int flags = spannable.getSpanFlags(span);
+ spannable.setSpan(
+ new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
+ start,
+ end,
+ flags
+ );
+ }
+ }
+ }
+ })
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
}
diff --git a/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java b/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java
index f1a67f50..19c6d3dd 100644
--- a/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java
@@ -1,36 +1,48 @@
package io.noties.markwon.sample.core;
-import android.app.Activity;
import android.os.Bundle;
import android.text.Spanned;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Node;
import io.noties.markwon.Markwon;
import io.noties.markwon.core.CorePlugin;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
+import io.noties.markwon.sample.R;
-public class CoreActivity extends Activity {
+public class CoreActivity extends ActivityWithMenuOptions {
private TextView textView;
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("simple", this::simple)
+ .add("toast", this::toast)
+ .add("alreadyParsed", this::alreadyParsed);
+ }
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_text_view);
- textView = new TextView(this);
- setContentView(textView);
+ textView = findViewById(R.id.text_view);
- step_1();
+// step_1();
- step_2();
+ simple();
- step_3();
-
- step_4();
+// toast();
+//
+// alreadyParsed();
}
/**
@@ -70,7 +82,7 @@ public class CoreActivity extends Activity {
/**
* To simply apply raw (non-parsed) markdown call {@link Markwon#setMarkdown(TextView, String)}
*/
- private void step_2() {
+ private void simple() {
// this is raw markdown
final String markdown = "Hello **markdown**!";
@@ -91,7 +103,7 @@ public class CoreActivity extends Activity {
* 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() {
+ private void toast() {
final String markdown = "*Toast* __here__!\n\n> And a quote!";
@@ -105,7 +117,7 @@ public class CoreActivity extends Activity {
/**
* To apply already parsed markdown use {@link Markwon#setParsedMarkdown(TextView, Spanned)}
*/
- private void step_4() {
+ private void alreadyParsed() {
final String markdown = "This **is** pre-parsed [markdown](#)";
diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java b/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java
index cb4f178f..cd286198 100644
--- a/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java
+++ b/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java
@@ -1,6 +1,5 @@
package io.noties.markwon.sample.customextension2;
-import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
@@ -25,34 +24,45 @@ import io.noties.markwon.core.CorePlugin;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.inlineparser.InlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
-public class CustomExtensionActivity2 extends Activity {
+public class CustomExtensionActivity2 extends ActivityWithMenuOptions {
+
+ private static final String MD = "" +
+ "# Custom Extension 2\n" +
+ "\n" +
+ "This is an issue #1\n" +
+ "Done by @noties";
+
+ private TextView textView;
+
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("text_added", this::text_added)
+ .add("inline_parsing", this::inline_parsing);
+ }
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
- final TextView textView = findViewById(R.id.text_view);
+ textView = findViewById(R.id.text_view);
// let's look for github special links:
// * `#1` - an issue or a pull request
// * `@user` link to a user
-
- final String md = "# Custom Extension 2\n" +
- "\n" +
- "This is an issue #1\n" +
- "Done by @noties";
-
-
// inline_parsing(textView, md);
- text_added(textView, md);
+ text_added();
}
- private void text_added(@NonNull TextView textView, @NonNull String md) {
+ private void text_added() {
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@@ -64,10 +74,10 @@ public class CustomExtensionActivity2 extends Activity {
})
.build();
- markwon.setMarkdown(textView, md);
+ markwon.setMarkdown(textView, MD);
}
- private void inline_parsing(@NonNull TextView textView, @NonNull String md) {
+ private void inline_parsing() {
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
// include all current defaults (otherwise will be empty - contain only our inline-processors)
@@ -86,7 +96,7 @@ public class CustomExtensionActivity2 extends Activity {
})
.build();
- markwon.setMarkdown(textView, md);
+ markwon.setMarkdown(textView, MD);
}
private static class IssueInlineProcessor extends InlineProcessor {
diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
index 5553c9f8..e1181a7f 100644
--- a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
@@ -1,11 +1,11 @@
package io.noties.markwon.sample.editor;
-import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
+import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.text.style.MetricAffectingSpan;
@@ -41,30 +41,61 @@ import io.noties.markwon.inlineparser.BangInlineProcessor;
import io.noties.markwon.inlineparser.EntityInlineProcessor;
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
+import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.linkify.LinkifyPlugin;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
-public class EditorActivity extends Activity {
+public class EditorActivity extends ActivityWithMenuOptions {
private EditText editText;
+ private String pendingInput;
+
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("simpleProcess", this::simple_process)
+ .add("simplePreRender", this::simple_pre_render)
+ .add("customPunctuationSpan", this::custom_punctuation_span)
+ .add("additionalEditSpan", this::additional_edit_span)
+ .add("additionalPlugins", this::additional_plugins)
+ .add("multipleEditSpans", this::multiple_edit_spans)
+ .add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
+ .add("pluginRequire", this::plugin_require)
+ .add("pluginNoDefaults", this::plugin_no_defaults);
+ }
+
+ @Override
+ protected void beforeOptionSelected(@NonNull String option) {
+ // we cannot _clear_ editText of text-watchers without keeping a reference to them...
+ pendingInput = editText != null
+ ? editText.getText().toString()
+ : null;
+
+ createView();
+ }
+
+ @Override
+ protected void afterOptionSelected(@NonNull String option) {
+ if (!TextUtils.isEmpty(pendingInput)) {
+ editText.setText(pendingInput);
+ }
+ }
+
+ private void createView() {
+ setContentView(R.layout.activity_editor);
+
+ this.editText = findViewById(R.id.edit_text);
+
+ initBottomBar();
+ }
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_editor);
-
- this.editText = findViewById(R.id.edit_text);
- initBottomBar();
-
-// simple_process();
-
-// simple_pre_render();
-
-// custom_punctuation_span();
-
-// additional_edit_span();
-
-// additional_plugins();
+ createView();
multiple_edit_spans();
}
@@ -216,6 +247,76 @@ public class EditorActivity extends Activity {
editor, Executors.newSingleThreadExecutor(), editText));
}
+ private void multiple_edit_spans_plugin() {
+ // inline parsing is configured via MarkwonInlineParserPlugin
+
+ // for links to be clickable
+ editText.setMovementMethod(LinkMovementMethod.getInstance());
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(StrikethroughPlugin.create())
+ .usePlugin(LinkifyPlugin.create())
+ .usePlugin(MarkwonInlineParserPlugin.create(builder -> {
+ builder
+ .excludeInlineProcessor(BangInlineProcessor.class)
+ .excludeInlineProcessor(HtmlInlineProcessor.class)
+ .excludeInlineProcessor(EntityInlineProcessor.class);
+ }))
+ .build();
+
+ final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
+
+ final MarkwonEditor editor = MarkwonEditor.builder(markwon)
+ .useEditHandler(new EmphasisEditHandler())
+ .useEditHandler(new StrongEmphasisEditHandler())
+ .useEditHandler(new StrikethroughEditHandler())
+ .useEditHandler(new CodeEditHandler())
+ .useEditHandler(new BlockQuoteEditHandler())
+ .useEditHandler(new LinkEditHandler(onClick))
+ .build();
+
+ editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
+ editor, Executors.newSingleThreadExecutor(), editText));
+ }
+
+ private void plugin_require() {
+ // usage of plugin from other plugins
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(MarkwonInlineParserPlugin.create())
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ registry.require(MarkwonInlineParserPlugin.class)
+ .factoryBuilder()
+ .excludeInlineProcessor(HtmlInlineProcessor.class);
+ }
+ })
+ .build();
+
+ final MarkwonEditor editor = MarkwonEditor.create(markwon);
+
+ editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
+ editor, Executors.newSingleThreadExecutor(), editText));
+ }
+
+ private void plugin_no_defaults() {
+ // a plugin with no defaults registered
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
+// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults(), factoryBuilder -> {
+// // if anything, they can be included here
+//// factoryBuilder.includeDefaults()
+// }))
+ .build();
+
+ final MarkwonEditor editor = MarkwonEditor.create(markwon);
+
+ editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
+ editor, Executors.newSingleThreadExecutor(), editText));
+ }
+
private void initBottomBar() {
// all except block-quote wraps if have selection, or inserts at current cursor position
diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java b/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java
index 743428d0..3a6d60fd 100644
--- a/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java
+++ b/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java
@@ -40,24 +40,28 @@ class LinkEditHandler extends AbstractEditHandler {
final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class);
editLinkSpan.link = span.getLink();
- final int s;
- final int e;
+ // First first __letter__ to find link content (scheme start in URL, receiver in email address)
+ // NB! do not use phone number auto-link (via LinkifyPlugin) as we cannot guarantee proper link
+ // display. For example, we _could_ also look for a digit, but:
+ // * if phone number start with special symbol, we won't have it (`+`, `(`)
+ // * it might interfere with an ordered-list
+ int start = -1;
- // markdown link vs. autolink
- if ('[' == input.charAt(spanStart)) {
- s = spanStart + 1;
- e = spanStart + 1 + spanTextLength;
- } else {
- s = spanStart;
- e = spanStart + spanTextLength;
+ for (int i = spanStart, length = input.length(); i < length; i++) {
+ if (Character.isLetter(input.charAt(i))) {
+ start = i;
+ break;
+ }
}
- editable.setSpan(
- editLinkSpan,
- s,
- e,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
- );
+ if (start > -1) {
+ editable.setSpan(
+ editLinkSpan,
+ start,
+ start + spanTextLength,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ }
}
@NonNull
diff --git a/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java b/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java
index 21613985..db5ca541 100644
--- a/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java
@@ -1,6 +1,5 @@
package io.noties.markwon.sample.html;
-import android.app.Activity;
import android.os.Bundle;
import android.text.Layout;
import android.text.TextUtils;
@@ -12,6 +11,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
+import org.commonmark.node.Paragraph;
+
import java.util.Collection;
import java.util.Collections;
import java.util.Random;
@@ -27,9 +28,24 @@ import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import io.noties.markwon.html.TagHandler;
import io.noties.markwon.html.tag.SimpleTagHandler;
+import io.noties.markwon.image.ImagesPlugin;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
-public class HtmlActivity extends Activity {
+public class HtmlActivity extends ActivityWithMenuOptions {
+
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("align", this::align)
+ .add("randomCharSize", this::randomCharSize)
+ .add("enhance", this::enhance)
+ .add("image", this::image);
+ }
+
+ private TextView textView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -39,35 +55,9 @@ public class HtmlActivity extends Activity {
// let's define some custom tag-handlers
- final TextView textView = findViewById(R.id.text_view);
+ textView = findViewById(R.id.text_view);
- final Markwon markwon = Markwon.builder(this)
- .usePlugin(HtmlPlugin.create())
- .usePlugin(new AbstractMarkwonPlugin() {
- @Override
- public void configure(@NonNull Registry registry) {
- registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
- .addHandler(new AlignTagHandler())
- .addHandler(new RandomCharSize(new Random(42L), textView.getTextSize()))
- .addHandler(new EnhanceTagHandler((int) (textView.getTextSize() * 2 + .05F))));
- }
- })
- .build();
-
- final String markdown = "# Hello, HTML\n" +
- "\n" +
- "We are centered\n" +
- "\n" +
- "We are at the end\n" +
- "\n" +
- "We should be at the start\n" +
- "\n" +
- "\n" +
- "This message should have a jumpy feeling because of different sizes of characters\n" +
- "\n\n" +
- "This is text that must be enhanced, at least a part of it";
-
- markwon.setMarkdown(textView, markdown);
+ align();
}
// we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content
@@ -105,6 +95,31 @@ public class HtmlActivity extends Activity {
}
}
+ private void align() {
+
+ final String md = "" +
+ "We are centered\n" +
+ "\n" +
+ "We are at the end\n" +
+ "\n" +
+ "We should be at the start\n" +
+ "\n";
+
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(HtmlPlugin.create())
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
+ .addHandler(new AlignTagHandler()));
+ }
+ })
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
// each character will have random size
private static class RandomCharSize extends TagHandler {
@@ -139,6 +154,27 @@ public class HtmlActivity extends Activity {
}
}
+ private void randomCharSize() {
+
+ final String md = "" +
+ "\n" +
+ "This message should have a jumpy feeling because of different sizes of characters\n" +
+ "\n\n";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(HtmlPlugin.create())
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
+ .addHandler(new RandomCharSize(new Random(42L), textView.getTextSize())));
+ }
+ })
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
private static class EnhanceTagHandler extends TagHandler {
private final int enhanceTextSize;
@@ -187,4 +223,49 @@ public class HtmlActivity extends Activity {
return position;
}
}
+
+ private void enhance() {
+
+ final String md = "" +
+ "This is text that must be enhanced, at least a part of it";
+
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(HtmlPlugin.create())
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
+ .addHandler(new EnhanceTagHandler((int) (textView.getTextSize() * 2 + .05F))));
+ }
+ })
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void image() {
+ // treat unclosed/void `img` tag as HTML inline
+ final String md = "" +
+ "## Try CommonMark\n" +
+ "\n" +
+ "Markwon IMG:\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "New lines...\n" +
+ "\n" +
+ "HTML IMG:\n" +
+ "\n" +
+ "
\n" +
+ "\n" +
+ "New lines\n\n";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(ImagesPlugin.create())
+ .usePlugin(HtmlPlugin.create())
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
}
diff --git a/sample/src/main/java/io/noties/markwon/sample/images/ImagesActivity.java b/sample/src/main/java/io/noties/markwon/sample/images/ImagesActivity.java
new file mode 100644
index 00000000..6206a2c4
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/images/ImagesActivity.java
@@ -0,0 +1,99 @@
+package io.noties.markwon.sample.images;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.request.target.Target;
+
+import io.noties.markwon.Markwon;
+import io.noties.markwon.image.AsyncDrawable;
+import io.noties.markwon.image.ImagesPlugin;
+import io.noties.markwon.image.glide.GlideImagesPlugin;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
+import io.noties.markwon.sample.R;
+
+public class ImagesActivity extends ActivityWithMenuOptions {
+
+ private TextView textView;
+
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ // todo: same for other plugins
+ return MenuOptions.create()
+ .add("glide-singleImage", this::glideSingleImage)
+ .add("glide-singleImageWithPlaceholder", this::glideSingleImageWithPlaceholder)
+ .add("click", this::click);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_text_view);
+ textView = findViewById(R.id.text_view);
+
+ glideSingleImageWithPlaceholder();
+ }
+
+ private void glideSingleImage() {
+ final String md = "[](https://www.youtube.com/watch?v=gs1I8_m4AOM)";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(GlideImagesPlugin.create(this))
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ // can be checked when used first, otherwise works as expected...
+ private void glideSingleImageWithPlaceholder() {
+ final String md = "[](https://www.youtube.com/watch?v=gs1I8_m4AOM)";
+
+ final Context context = this;
+
+ final Markwon markwon = Markwon.builder(context)
+ .usePlugin(GlideImagesPlugin.create(new GlideImagesPlugin.GlideStore() {
+ @NonNull
+ @Override
+ public RequestBuilder load(@NonNull AsyncDrawable drawable) {
+// final Drawable placeholder = ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp);
+// placeholder.setBounds(0, 0, 100, 100);
+ return Glide.with(context)
+ .load(drawable.getDestination())
+// .placeholder(ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp));
+// .placeholder(placeholder);
+ .placeholder(R.drawable.ic_home_black_36dp);
+ }
+
+ @Override
+ public void cancel(@NonNull Target> target) {
+ Glide.with(context)
+ .clear(target);
+ }
+ }))
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void click() {
+
+ textView.setMovementMethod(LinkMovementMethod.getInstance());
+
+ final String md = "[](https://www.mdeditor.com/images/logos/markdown.png)";
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(ImagesPlugin.create())
+ .build();
+ markwon.setMarkdown(textView, md);
+ }
+}
diff --git a/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java b/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java
index 27d069eb..4e7c87da 100644
--- a/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java
@@ -1,6 +1,5 @@
package io.noties.markwon.sample.inlineparser;
-import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
@@ -25,19 +24,32 @@ import io.noties.markwon.Markwon;
import io.noties.markwon.inlineparser.BackticksInlineProcessor;
import io.noties.markwon.inlineparser.CloseBracketInlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
+import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
-public class InlineParserActivity extends Activity {
+public class InlineParserActivity extends ActivityWithMenuOptions {
private TextView textView;
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("links_only", this::links_only)
+ .add("disable_code", this::disable_code)
+ .add("pluginWithDefaults", this::pluginWithDefaults)
+ .add("pluginNoDefaults", this::pluginNoDefaults);
+ }
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
- this.textView = findViewById(R.id.text_view);
+ textView = findViewById(R.id.text_view);
// links_only();
@@ -115,4 +127,50 @@ public class InlineParserActivity extends Activity {
"**Good day!**";
markwon.setMarkdown(textView, md);
}
+
+ private void pluginWithDefaults() {
+ // a plugin with defaults registered
+
+ final String md = "no [links](#) for **you** `code`!";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(MarkwonInlineParserPlugin.create())
+ // the same as:
+// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilder()))
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ registry.require(MarkwonInlineParserPlugin.class, plugin -> {
+ plugin.factoryBuilder()
+ .excludeInlineProcessor(OpenBracketInlineProcessor.class);
+ });
+ }
+ })
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void pluginNoDefaults() {
+ // a plugin with NO defaults registered
+
+ final String md = "no [links](#) for **you** `code`!";
+
+ final Markwon markwon = Markwon.builder(this)
+ // pass `MarkwonInlineParser.factoryBuilderNoDefaults()` no disable all
+ .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ registry.require(MarkwonInlineParserPlugin.class, plugin -> {
+ plugin.factoryBuilder()
+ .addInlineProcessor(new BackticksInlineProcessor());
+ });
+ }
+ })
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
}
diff --git a/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java b/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java
index 44d9aac3..b8a7d9e3 100644
--- a/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java
@@ -1,29 +1,31 @@
package io.noties.markwon.sample.latex;
-import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import io.noties.debug.Debug;
import io.noties.markwon.Markwon;
import io.noties.markwon.ext.latex.JLatexMathPlugin;
+import io.noties.markwon.ext.latex.JLatexMathTheme;
+import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
-import ru.noties.jlatexmath.JLatexMathDrawable;
-public class LatexActivity extends Activity {
+public class LatexActivity extends ActivityWithMenuOptions {
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_text_view);
-
- final TextView textView = findViewById(R.id.text_view);
+ private static final String LATEX_ARRAY;
+ static {
String latex = "\\begin{array}{l}";
latex += "\\forall\\varepsilon\\in\\mathbb{R}_+^*\\ \\exists\\eta>0\\ |x-x_0|\\leq\\eta\\Longrightarrow|f(x)-f(x_0)|\\leq\\varepsilon\\\\";
latex += "\\det\\begin{bmatrix}a_{11}&a_{12}&\\cdots&a_{1n}\\\\a_{21}&\\ddots&&\\vdots\\\\\\vdots&&\\ddots&\\vdots\\\\a_{n1}&\\cdots&\\cdots&a_{nn}\\end{bmatrix}\\overset{\\mathrm{def}}{=}\\sum_{\\sigma\\in\\mathfrak{S}_n}\\varepsilon(\\sigma)\\prod_{k=1}^n a_{k\\sigma(k)}\\\\";
@@ -34,61 +36,222 @@ public class LatexActivity extends Activity {
latex += "L = \\int_a^b \\sqrt{ \\left|\\sum_{i,j=1}^ng_{ij}(\\gamma(t))\\left(\\frac{d}{dt}x^i\\circ\\gamma(t)\\right)\\left(\\frac{d}{dt}x^j\\circ\\gamma(t)\\right)\\right|}\\,dt\\\\";
latex += "\\begin{array}{rl} s &= \\int_a^b\\left\\|\\frac{d}{dt}\\vec{r}\\,(u(t),v(t))\\right\\|\\,dt \\\\ &= \\int_a^b \\sqrt{u'(t)^2\\,\\vec{r}_u\\cdot\\vec{r}_u + 2u'(t)v'(t)\\, \\vec{r}_u\\cdot\\vec{r}_v+ v'(t)^2\\,\\vec{r}_v\\cdot\\vec{r}_v}\\,\\,\\, dt. \\end{array}\\\\";
latex += "\\end{array}";
+ LATEX_ARRAY = latex;
+ }
-// String latex = "\\text{A long division \\longdiv{12345}{13}";
-// String latex = "{a \\bangle b} {c \\brace d} {e \\brack f} {g \\choose h}";
+ private static final String LATEX_LONG_DIVISION = "\\text{A long division \\longdiv{12345}{13}";
+ private static final String LATEX_BANGLE = "{a \\bangle b} {c \\brace d} {e \\brack f} {g \\choose h}";
+ private static final String LATEX_BOXES;
-// String latex = "\\begin{array}{cc}";
-// latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr";
-// latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr";
-// latex += "\\end{array}";
+ static {
+ String latex = "\\begin{array}{cc}";
+ latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr";
+ latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr";
+ latex += "\\end{array}";
+ LATEX_BOXES = latex;
+ }
- final String markdown = "# Example of LaTeX\n\n$$"
- + latex + "$$\n\n something like **this**";
+ private TextView textView;
+ private View parent;
+
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("array", this::array)
+ .add("longDivision", this::longDivision)
+ .add("bangle", this::bangle)
+ .add("boxes", this::boxes)
+ .add("insideBlockQuote", this::insideBlockQuote)
+ .add("error", this::error)
+ .add("legacy", this::legacy)
+ .add("textColor", this::textColor)
+ .add("defaultTextColor", this::defaultTextColor)
+ .add("inlineAndBlock", this::inlineAndBlock)
+ .add("dark", this::dark);
+ }
+
+ @Override
+ protected void beforeOptionSelected(@NonNull String option) {
+ super.beforeOptionSelected(option);
+
+ // reset text color
+ textView.setTextColor(0xFF000000);
+
+ // reset background
+ parent.setBackgroundColor(0xFFffffff);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_text_view);
+
+ textView = findViewById(R.id.text_view);
+ parent = findViewById(R.id.scroll_view);
+
+// array();
+ longDivision();
+ }
+
+ private void array() {
+ renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_ARRAY));
+ }
+
+ private void longDivision() {
+ renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION));
+ }
+
+ private void bangle() {
+ renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_BANGLE));
+ }
+
+ private void boxes() {
+ renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_BOXES));
+ }
+
+ private void insideBlockQuote() {
+ String latex = "W=W_1+W_2=F_1X_1-F_2X_2";
+ final String md = "" +
+ "# LaTeX inside a blockquote\n" +
+ "> $$" + latex + "$$\n";
+ renderWithBlocksAndInlines(md);
+ }
+
+ private void error() {
+ final String md = wrapLatexInSampleMarkdown("\\sum_{i=0}^\\infty x \\cdot 0 \\rightarrow \\iMightNotExist{0}");
final Markwon markwon = Markwon.builder(this)
-// .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() {
-// @Override
-// public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
-// builder
-// .backgroundProvider(new JLatexMathPlugin.BackgroundProvider() {
-// @NonNull
-// @Override
-// public Drawable provide() {
-// return new ColorDrawable(0x40ff0000);
-// }
-// })
-// .fitCanvas(true)
-// .align(JLatexMathDrawable.ALIGN_LEFT)
-// .padding(48)
-// ;
-// }
-// }))
- .usePlugin(JLatexMathPlugin.create(textView.getTextSize()))
+ .usePlugin(MarkwonInlineParserPlugin.create())
+ .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
+ builder.inlinesEnabled(true);
+ //noinspection Convert2Lambda
+ builder.errorHandler(new JLatexMathPlugin.ErrorHandler() {
+ @Nullable
+ @Override
+ public Drawable handleError(@Nullable String latex, @NonNull Throwable error) {
+ Debug.e(error, latex);
+ return ContextCompat.getDrawable(LatexActivity.this, R.drawable.ic_android_black_24dp);
+ }
+ });
+ }))
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void legacy() {
+ final String md = wrapLatexInSampleMarkdown(LATEX_BANGLE);
+
+ final Markwon markwon = Markwon.builder(this)
+ // LEGACY does not require inline parser
+ .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
+ builder.blocksLegacy(true);
+ builder.theme()
+ .backgroundProvider(() -> new ColorDrawable(0x100000ff))
+ .padding(JLatexMathTheme.Padding.all(48));
+ }))
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void textColor() {
+ final String md = wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION);
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(MarkwonInlineParserPlugin.create())
+ .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
+ builder.inlinesEnabled(true);
+ builder.theme()
+ .inlineTextColor(Color.RED)
+ .blockTextColor(Color.GREEN)
+ .inlineBackgroundProvider(() -> new ColorDrawable(Color.YELLOW))
+ .blockBackgroundProvider(() -> new ColorDrawable(Color.GRAY));
+ }))
+ .build();
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void defaultTextColor() {
+ // @since 4.3.0 text color is automatically taken from textView
+ // (if it's not specified explicitly via configuration)
+ textView.setTextColor(0xFFff0000);
+
+ final String md = wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION);
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(MarkwonInlineParserPlugin.create())
+ .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() {
+ @Override
+ public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
+ builder.inlinesEnabled(true);
+ // override default text color
+ builder.theme()
+ .inlineTextColor(0xFF00ffff);
+ }
+ }))
+ .build();
+
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void inlineAndBlock() {
+ final String md = "" +
+ "# Inline and block\n\n" +
+ "$$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$\n\n" +
+ "this was **inline** _LaTeX_ $$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$ and once again it was\n\n" +
+ "Now a block:\n\n" +
+ "$$\n" +
+ "\\int_{a}^{b} f(x)dx = F(b) - F(a)\n" +
+ "$$\n\n" +
+ "Not a block (content on delimited line), but inline instead:\n\n" +
+ "$$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$" +
+ "\n\n" +
+ "that's it";
+ renderWithBlocksAndInlines(md);
+ }
+
+ private void dark() {
+ parent.setBackgroundColor(0xFF000000);
+ textView.setTextColor(0xFFffffff);
+
+ String latex = "W=W_1+W_2=F_1X_1-F_2X_2";
+ final String md = "" +
+ "# LaTeX inside a blockquote\n" +
+ "> $$" + latex + "$$\n";
+ renderWithBlocksAndInlines(md);
+ }
+
+ @NonNull
+ private static String wrapLatexInSampleMarkdown(@NonNull String latex) {
+ return "" +
+ "# Example of LaTeX\n\n" +
+ "(inline): $$" + latex + "$$ so nice, really-really really-really really-really? Now, (block):\n\n" +
+ "$$\n" +
+ "" + latex + "\n" +
+ "$$\n\n" +
+ "the end";
+ }
+
+ private void renderWithBlocksAndInlines(@NonNull String markdown) {
+
+ final float textSize = textView.getTextSize();
+ final Resources r = getResources();
+
+ final Markwon markwon = Markwon.builder(this)
+ // NB! `MarkwonInlineParserPlugin` is required in order to parse inlines
+ .usePlugin(MarkwonInlineParserPlugin.create())
+ .usePlugin(JLatexMathPlugin.create(textSize, textSize * 1.25F, builder -> {
+ // Important thing to do is to enable inlines (by default disabled)
+ builder.inlinesEnabled(true);
+ builder.theme()
+ .inlineBackgroundProvider(() -> new ColorDrawable(0x1000ff00))
+ .blockBackgroundProvider(() -> new ColorDrawable(0x10ff0000))
+ .blockPadding(JLatexMathTheme.Padding.symmetric(
+ r.getDimensionPixelSize(R.dimen.latex_block_padding_vertical),
+ r.getDimensionPixelSize(R.dimen.latex_block_padding_horizontal)
+ ));
+ }))
.build();
-//
-// if (true) {
-//// final String l = "$$\n" +
-//// " P(X=r)=\\frac{\\lambda^r e^{-\\lambda}}{r!}\n" +
-//// "$$\n" +
-//// "\n" +
-//// "$$\n" +
-//// " P(Xr)=1-P(X new StyleSpan(Typeface.BOLD))
+ .setFactory(Emphasis.class, (configuration, props) -> new StyleSpan(Typeface.ITALIC));
+ }
+ })
+ .build();
+ display(markwon.toMarkdown(md));
+ }
+
+ private void heading() {
+
+ // please note that heading doesn't seem to be working in remote views,
+ // tried both `RelativeSizeSpan` and `AbsoluteSizeSpan` with no effect
+
+ final float base = 12;
+
+ final float[] sizes = {
+ 2.F, 1.5F, 1.17F, 1.F, .83F, .67F,
+ };
+
+ final String md = "" +
+ "# H1\n" +
+ "## H2\n" +
+ "### H3\n" +
+ "#### H4\n" +
+ "##### H5\n" +
+ "###### H6\n\n";
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ builder.setFactory(Heading.class, (configuration, props) -> {
+ final Integer level = CoreProps.HEADING_LEVEL.get(props);
+ Debug.i(level);
+ if (level != null && level > 0 && level <= sizes.length) {
+// return new RelativeSizeSpan(sizes[level - 1]);
+ final Object span = new AbsoluteSizeSpan((int) (base * sizes[level - 1] + .5F), true);
+ return new Object[]{
+ span,
+ new StyleSpan(Typeface.BOLD)
+ };
+ }
+ return null;
+ });
+ }
+ })
+ .build();
+ display(markwon.toMarkdown(md));
+ }
+
+ private void lists() {
+ final String md = "" +
+ "* bullet 1\n" +
+ "* bullet 2\n" +
+ "* * bullet 2 1\n" +
+ " * bullet 2 0 1\n" +
+ "1) order 1\n" +
+ "1) order 2\n" +
+ "1) order 3\n";
+
+ // ordered lists _could_ be translated to raw text representation (`1.`, `1)` etc) in resulting markdown
+ // or they could be _disabled_ all together... (can ordered lists be disabled in parser?)
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ builder.setFactory(ListItem.class, (configuration, props) -> {
+ final CoreProps.ListItemType type = CoreProps.LIST_ITEM_TYPE.get(props);
+ if (type != null) {
+ // bullet and ordered list share the same markdown node
+ return new BulletSpan();
+ }
+ return null;
+ });
+ }
+ })
+ .build();
+
+ display(markwon.toMarkdown(md));
+ }
+
+ private void image() {
+ // please note that image _could_ be supported only if it would be available immediately
+ // debugging possibility
+ //
+ // doesn't seem to be working
+
+ final Bitmap bitmap = Bitmap.createBitmap(128, 256, Bitmap.Config.ARGB_4444);
+ final Canvas canvas = new Canvas(bitmap);
+ canvas.drawColor(0xFFAD1457);
+
+ final SpannableStringBuilder builder = new SpannableStringBuilder();
+ builder.append("An image: ");
+
+ final int length = builder.length();
+ builder.append("[bitmap]");
+ builder.setSpan(
+ new ImageSpan(this, bitmap, DynamicDrawableSpan.ALIGN_BOTTOM),
+ length,
+ builder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+
+ builder.append(" okay, and ");
+
+ final int start = builder.length();
+ builder.append("[resource]");
+ builder.setSpan(
+ new ImageSpan(this, R.drawable.ic_memory_black_48dp),
+ start,
+ builder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+
+ display(builder);
+ }
+
+ private void link() {
+ final String md = "" +
+ "[a link](https://isa.link/) is here, styling yes, clicking - no";
+ display(Markwon.create(this).toMarkdown(md));
+ }
+
+ private void blockquote() {
+ final String md = "" +
+ "> This was once said by me\n" +
+ "> > And this one also\n\n" +
+ "Me";
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ builder.setFactory(BlockQuote.class, (configuration, props) -> new QuoteSpan());
+ }
+ })
+ .build();
+ display(markwon.toMarkdown(md));
+ }
+
+ private void strikethrough() {
+ final String md = "~~strike that!~~";
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new StrikethroughPlugin())
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ builder.setFactory(Strikethrough.class, (configuration, props) -> new StrikethroughSpan());
+ }
+ })
+ .build();
+ display(markwon.toMarkdown(md));
+ }
+
+ private void display(@NonNull CharSequence cs) {
+ final NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ if (manager == null) {
+ throw new IllegalStateException("No NotificationManager is available");
+ }
+
+ ensureChannel(manager);
+
+ final Notification.Builder builder = new Notification.Builder(this)
+ .setSmallIcon(R.drawable.ic_stat_name)
+ .setContentTitle("Markwon")
+ .setContentText(cs)
+ .setStyle(new Notification.BigTextStyle().bigText(cs));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ builder.setChannelId(CHANNEL_ID);
+ }
+
+ manager.notify(1, builder.build());
+ }
+
+ private void ensureChannel(@NonNull NotificationManager manager) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return;
+ }
+
+ final NotificationChannel channel = manager.getNotificationChannel(CHANNEL_ID);
+ if (channel == null) {
+ manager.createNotificationChannel(new NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_ID,
+ NotificationManager.IMPORTANCE_DEFAULT));
+ }
+ }
+}
diff --git a/sample/src/main/java/io/noties/markwon/sample/tasklist/TaskListActivity.java b/sample/src/main/java/io/noties/markwon/sample/tasklist/TaskListActivity.java
new file mode 100644
index 00000000..b5c421d6
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/tasklist/TaskListActivity.java
@@ -0,0 +1,169 @@
+package io.noties.markwon.sample.tasklist;
+
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.style.ClickableSpan;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import java.util.Objects;
+
+import io.noties.debug.Debug;
+import io.noties.markwon.AbstractMarkwonPlugin;
+import io.noties.markwon.Markwon;
+import io.noties.markwon.MarkwonSpansFactory;
+import io.noties.markwon.SpanFactory;
+import io.noties.markwon.ext.tasklist.TaskListItem;
+import io.noties.markwon.ext.tasklist.TaskListPlugin;
+import io.noties.markwon.ext.tasklist.TaskListSpan;
+import io.noties.markwon.sample.ActivityWithMenuOptions;
+import io.noties.markwon.sample.MenuOptions;
+import io.noties.markwon.sample.R;
+
+public class TaskListActivity extends ActivityWithMenuOptions {
+
+ private static final String MD = "" +
+ "- [ ] Not done here!\n" +
+ "- [x] and done\n" +
+ "- [X] and again!\n" +
+ "* [ ] **and** syntax _included_ `code`\n" +
+ "- [ ] [link](#)\n" +
+ "- [ ] [a check box](https://goog.le)\n" +
+ "- [x] [test]()\n" +
+ "- [List](https://goog.le) 3";
+
+ private TextView textView;
+
+ @NonNull
+ @Override
+ public MenuOptions menuOptions() {
+ return MenuOptions.create()
+ .add("regular", this::regular)
+ .add("customColors", this::customColors)
+ .add("customDrawableResources", this::customDrawableResources)
+ .add("mutate", this::mutate);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_text_view);
+
+ textView = findViewById(R.id.text_view);
+
+// mutate();
+ regular();
+ }
+
+ private void regular() {
+ // default theme
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(TaskListPlugin.create(this))
+ .build();
+
+ markwon.setMarkdown(textView, MD);
+ }
+
+ private void customColors() {
+
+ final int checkedFillColor = Color.RED;
+ final int normalOutlineColor = Color.GREEN;
+ final int checkMarkColor = Color.BLUE;
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor))
+ .build();
+
+ markwon.setMarkdown(textView, MD);
+ }
+
+ private void customDrawableResources() {
+ // drawable **must** be stateful
+
+ final Drawable drawable = Objects.requireNonNull(
+ ContextCompat.getDrawable(this, R.drawable.custom_task_list));
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(TaskListPlugin.create(drawable))
+ .build();
+
+ markwon.setMarkdown(textView, MD);
+ }
+
+ private void mutate() {
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(TaskListPlugin.create(this))
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ // obtain origin task-list-factory
+ final SpanFactory origin = builder.getFactory(TaskListItem.class);
+ if (origin == null) {
+ return;
+ }
+
+ builder.setFactory(TaskListItem.class, (configuration, props) -> {
+ // maybe it's better to validate the actual type here also
+ // and not force cast to task-list-span
+ final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props);
+ if (span == null) {
+ return null;
+ }
+
+ // NB, toggle click will intercept possible links inside task-list-item
+ return new Object[]{
+ span,
+ new TaskListToggleSpan(span)
+ };
+ });
+ }
+ })
+ .build();
+
+ markwon.setMarkdown(textView, MD);
+ }
+
+ private static class TaskListToggleSpan extends ClickableSpan {
+
+ private final TaskListSpan span;
+
+ TaskListToggleSpan(@NonNull TaskListSpan span) {
+ this.span = span;
+ }
+
+ @Override
+ public void onClick(@NonNull View widget) {
+ // toggle span (this is a mere visual change)
+ span.setDone(!span.isDone());
+ // request visual update
+ widget.invalidate();
+
+ // it must be a TextView
+ final TextView textView = (TextView) widget;
+ // it must be spanned
+ final Spanned spanned = (Spanned) textView.getText();
+
+ // actual text of the span (this can be used along with the `span`)
+ final CharSequence task = spanned.subSequence(
+ spanned.getSpanStart(this),
+ spanned.getSpanEnd(this)
+ );
+
+ Debug.i("task done: %s, '%s'", span.isDone(), task);
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint ds) {
+ // no op, so text is not rendered as a link
+ }
+ }
+}
diff --git a/sample/src/main/res/drawable-anydpi-v24/ic_stat_name.xml b/sample/src/main/res/drawable-anydpi-v24/ic_stat_name.xml
new file mode 100644
index 00000000..fd7cefc2
--- /dev/null
+++ b/sample/src/main/res/drawable-anydpi-v24/ic_stat_name.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/sample/src/main/res/drawable-hdpi/ic_stat_name.png b/sample/src/main/res/drawable-hdpi/ic_stat_name.png
new file mode 100644
index 00000000..19e7a26b
Binary files /dev/null and b/sample/src/main/res/drawable-hdpi/ic_stat_name.png differ
diff --git a/sample/src/main/res/drawable-mdpi/ic_stat_name.png b/sample/src/main/res/drawable-mdpi/ic_stat_name.png
new file mode 100644
index 00000000..0525d874
Binary files /dev/null and b/sample/src/main/res/drawable-mdpi/ic_stat_name.png differ
diff --git a/sample/src/main/res/drawable-xhdpi/ic_stat_name.png b/sample/src/main/res/drawable-xhdpi/ic_stat_name.png
new file mode 100644
index 00000000..c5f2f076
Binary files /dev/null and b/sample/src/main/res/drawable-xhdpi/ic_stat_name.png differ
diff --git a/sample/src/main/res/drawable-xxhdpi/ic_stat_name.png b/sample/src/main/res/drawable-xxhdpi/ic_stat_name.png
new file mode 100644
index 00000000..993df1f0
Binary files /dev/null and b/sample/src/main/res/drawable-xxhdpi/ic_stat_name.png differ
diff --git a/sample/src/main/res/drawable/custom_task_list.xml b/sample/src/main/res/drawable/custom_task_list.xml
new file mode 100644
index 00000000..43c2e2a8
--- /dev/null
+++ b/sample/src/main/res/drawable/custom_task_list.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/layout/activity_text_view.xml b/sample/src/main/res/layout/activity_text_view.xml
index 9828f257..e557a4bc 100644
--- a/sample/src/main/res/layout/activity_text_view.xml
+++ b/sample/src/main/res/layout/activity_text_view.xml
@@ -1,13 +1,16 @@
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:padding="8dip">
+
+ 8dip
+ 16dip
+
\ No newline at end of file
diff --git a/sample/src/main/res/values/strings-samples.xml b/sample/src/main/res/values/strings-samples.xml
index d87585fd..0305b471 100644
--- a/sample/src/main/res/values/strings-samples.xml
+++ b/sample/src/main/res/values/strings-samples.xml
@@ -29,6 +29,12 @@
# \# Inline Parser\n\nUsage of custom inline parser
- # \# HTML <details> tag\n\n<details> tag parsed and rendered
+ # \# HTML\n\n`details` tag parsed and rendered
+
+ # \# TaskList\n\nUsage of TaskListPlugin
+
+ # \# Images\n\nUsage of different images plugins
+
+ # \# Notification\n\nExample usage in notifications and other remote views
\ No newline at end of file
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
index 5505eb0c..36d27f2a 100644
--- a/sample/src/main/res/values/strings.xml
+++ b/sample/src/main/res/values/strings.xml
@@ -12,4 +12,16 @@ Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64
]]>
+
+