Sample app, adding moving samples

This commit is contained in:
Dimitry Ivanov 2020-06-29 20:21:18 +03:00
parent 25740d7389
commit bc58790704
42 changed files with 3361 additions and 6 deletions

View File

@ -1,4 +1,401 @@
[ [
{
"javaClassName": "io.noties.markwon.app.samples.NoParsingSample",
"id": "202006181171212",
"title": "No parsing",
"description": "All commonmark parsing is disabled (both inlines and blocks)",
"artifacts": [
"CORE"
],
"tags": [
"parsing",
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.InlinePluginNoDefaultsSample",
"id": "202006181170857",
"title": "Inline parsing without defaults",
"description": "Configure inline parser plugin to **not** have any **inline** parsing",
"artifacts": [
"INLINE_PARSER"
],
"tags": [
"parsing"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.editor.EditorNewLineContinuationSample",
"id": "202006181170348",
"title": "Editor new line continuation",
"description": "Sample of how new line character can be handled in order to add a _continuation_, for example adding a new bullet list item if current line starts with one",
"artifacts": [
"EDITOR",
"INLINE_PARSER"
],
"tags": [
"editor"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.editor.EditorMultipleEditSpansSample",
"id": "202006181165920",
"title": "Multiple edit spans",
"description": "Additional multiple edit spans for editor",
"artifacts": [
"EDITOR",
"INLINE_PARSER"
],
"tags": [
"editor"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.editor.EditorAdditionalPluginSample",
"id": "202006181165347",
"title": "Additional plugin",
"description": "Additional plugin for editor",
"artifacts": [
"EDITOR",
"EXT_STRIKETHROUGH",
"INLINE_PARSER"
],
"tags": [
"editor"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.editor.EditorAdditionalEditSpan",
"id": "202006181165136",
"title": "Additional edit span",
"description": "Additional _edit_ span (span that is present in `EditText` along with punctuation",
"artifacts": [
"EDITOR",
"INLINE_PARSER"
],
"tags": [
"editor",
"span"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.editor.EditorCustomPunctuationSample",
"id": "202006181164627",
"title": "Custom punctuation span",
"description": "Custom span for punctuation in editor",
"artifacts": [
"EDITOR",
"INLINE_PARSER"
],
"tags": [
"editor",
"span"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.editor.EditorPreRenderSample",
"id": "202006181164422",
"title": "Editor with pre-render (async)",
"description": "Editor functionality with highlight taking place in another thread",
"artifacts": [
"EDITOR",
"INLINE_PARSER"
],
"tags": [
"editor"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.editor.EditorSimpleSample",
"id": "202006181164227",
"title": "Simple editor",
"description": "Simple usage of editor with markdown highlight",
"artifacts": [
"EDITOR",
"INLINE_PARSER"
],
"tags": [
"editor"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.CustomExtensionSample",
"id": "202006181163248",
"title": "Custom extension",
"description": "Custom extension that adds an icon from resources and renders it as image with `@ic-name` syntax",
"artifacts": [
"CORE"
],
"tags": [
"extension",
"image",
"parsing",
"plugin",
"rendering",
"span"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample",
"id": "202006181162024",
"title": "User mention and issue (via text)",
"description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`",
"artifacts": [
"CORE",
"INLINE_PARSER"
],
"tags": [
"parsing",
"rendering",
"text-added-listener"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample",
"id": "202006181162024",
"title": "User mention and issue (via text)",
"description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`",
"artifacts": [
"CORE"
],
"tags": [
"parsing",
"rendering",
"text-added-listener"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.ReadMorePluginSample",
"id": "202006181161505",
"title": "Read more plugin",
"description": "Plugin that adds expand/collapse (\"show all\"/\"show less\")",
"artifacts": [
"CORE"
],
"tags": [
"plugin"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.TableOfContentsSample",
"id": "202006181161226",
"title": "Table of contents",
"description": "Sample plugin that adds a table of contents header",
"artifacts": [
"CORE"
],
"tags": [
"plugin",
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.LetterOrderedListSample",
"id": "202006181130954",
"title": "Letter ordered list",
"description": "Render bullet list inside an ordered list with letters instead of bullets",
"artifacts": [
"CORE"
],
"tags": [
"lists",
"plugin",
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.AnchorSample",
"id": "202006181130728",
"title": "Anchor plugin",
"description": "HTML-like anchor links plugin, which scrolls to clicked anchor",
"artifacts": [
"CORE"
],
"tags": [
"anchor",
"links",
"plugin"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.AllBlocksNoForcedNewLineSample",
"id": "202006181130227",
"title": "All blocks no padding",
"description": "Do not render new lines (padding) after all blocks",
"artifacts": [
"CORE"
],
"tags": [
"block",
"padding",
"rendering",
"spacing"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.HeadingNoSpaceBlockHandlerSample",
"id": "202006181125924",
"title": "Heading no padding (block handler)",
"description": "Process padding (spacing) after heading with a `BlockHandler`",
"artifacts": [
"CORE"
],
"tags": [
"block",
"heading",
"padding",
"rendering",
"spacing"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.HeadingNoSpaceSample",
"id": "202006181125622",
"title": "Heading no padding",
"description": "Do not add a new line after heading node",
"artifacts": [
"CORE"
],
"tags": [
"padding",
"rendering",
"spacing"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.AdditionalSpacingSample",
"id": "202006181125321",
"title": "Additional spacing after block",
"description": "Add additional spacing (padding) after last line of a block",
"artifacts": [
"CORE"
],
"tags": [
"padding",
"spacing",
"span"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.SoftBreakAddsNewLineSample",
"id": "202006181125040",
"title": "Soft break new line",
"description": "Add a new line for a markdown soft-break node",
"artifacts": [
"CORE"
],
"tags": [
"new-line",
"soft-break"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.SoftBreakAddsSpace",
"id": "202006181124706",
"title": "Soft break adds space",
"description": "By default a soft break (`\n`) will add a space character instead of new line",
"artifacts": [
"CORE"
],
"tags": [
"defaults",
"new-line",
"soft-break"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.ImagesCustomSchemeSample",
"id": "202006181124201",
"title": "Image destination custom scheme",
"description": "Example of handling custom scheme (`https`, `ftp`, `whatever`, etc.) for images destination URLs with `ImagesPlugin`",
"artifacts": [
"IMAGE"
],
"tags": [
"image"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.LinkWithoutSchemeSample",
"id": "202006181124005",
"title": "Links without scheme",
"description": "Links without scheme are considered to be `https`",
"artifacts": [
"CORE"
],
"tags": [
"defaults",
"links"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.CustomizeThemeSample",
"id": "202006181123617",
"title": "Customize theme",
"description": "Customize `MarkwonTheme` styling",
"artifacts": [
"CORE"
],
"tags": [
"plugin",
"style",
"theme"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.DisableNodeSample",
"id": "202006181123308",
"title": "Disable node from rendering",
"description": "Disable _parsed_ node from being rendered (markdown syntax is still consumed)",
"artifacts": [
"CORE"
],
"tags": [
"parsing",
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.ParagraphSpanStyle",
"id": "202006181122647",
"title": "Paragraph style",
"description": "Apply a style (via span) to a paragraph",
"artifacts": [
"CORE"
],
"tags": [
"paragraph",
"span",
"style"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.LinkTitleSample",
"id": "202006181122230",
"title": "Obtain link title",
"description": "Obtain title (text) of clicked link, `[title](#destination)`",
"artifacts": [
"CORE"
],
"tags": [
"links",
"span"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.movementmethod.DisableImplicitMovementMethodPluginSample",
"id": "202006181121803",
"title": "Disable implicit movement method via plugin",
"description": "Disable implicit movement method via `MovementMethodPlugin`",
"artifacts": [
"CORE"
],
"tags": [
"links",
"movement-method",
"recycler-view"
]
},
{ {
"javaClassName": "io.noties.markwon.app.samples.movementmethod.MovementMethodPluginSample", "javaClassName": "io.noties.markwon.app.samples.movementmethod.MovementMethodPluginSample",
"id": "202006179081631", "id": "202006179081631",
@ -17,7 +414,7 @@
"javaClassName": "io.noties.markwon.app.samples.movementmethod.DisableImplicitMovementMethodSample", "javaClassName": "io.noties.markwon.app.samples.movementmethod.DisableImplicitMovementMethodSample",
"id": "202006179081256", "id": "202006179081256",
"title": "Disable implicit movement method", "title": "Disable implicit movement method",
"description": "Configure `Markwon` to **not** apply implicit movement method", "description": "Configure `Markwon` to **not** apply implicit movement method, which consumes touch events when used in a `RecyclerView` even when markdown does not contain links",
"artifacts": [ "artifacts": [
"CORE" "CORE"
], ],
@ -50,6 +447,7 @@
"CORE" "CORE"
], ],
"tags": [ "tags": [
"defaults",
"links", "links",
"movement-method" "movement-method"
] ]

View File

@ -10,4 +10,21 @@ object Tags {
const val links = "links" const val links = "links"
const val plugin = "plugin" const val plugin = "plugin"
const val recyclerView = "recycler-view" const val recyclerView = "recycler-view"
const val paragraph = "paragraph"
const val rendering = "rendering"
const val style = "style"
const val theme = "theme"
const val image = "image"
const val newLine = "new-line"
const val softBreak = "soft-break"
const val defaults = "defaults"
const val spacing = "spacing"
const val padding = "padding"
const val heading = "heading"
const val anchor = "anchor"
const val lists = "lists"
const val extension = "extension"
const val textAddedListener = "text-added-listener"
const val editor = "editor"
const val span = "span"
} }

View File

@ -0,0 +1,23 @@
package io.noties.markwon.app.sample.ui
import android.content.Context
import android.view.View
import android.widget.EditText
import io.noties.markwon.app.R
abstract class MarkwonEditTextSample: MarkwonSample() {
protected lateinit var context: Context
protected lateinit var editText: EditText
override val layoutResId: Int
get() = R.layout.activity_edit_text
override fun onViewCreated(view: View) {
context = view.context
editText = view.findViewById(R.id.edit_text)
render()
}
abstract fun render()
}

View File

@ -13,7 +13,7 @@ abstract class MarkwonTextViewSample : MarkwonSample() {
override val layoutResId: Int = R.layout.sample_text_view override val layoutResId: Int = R.layout.sample_text_view
final override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
context = view.context context = view.context
textView = view.findViewById(R.id.text_view) textView = view.findViewById(R.id.text_view)
render() render()

View File

@ -20,15 +20,15 @@ import io.noties.markwon.Markwon
import io.noties.markwon.app.App import io.noties.markwon.app.App
import io.noties.markwon.app.R import io.noties.markwon.app.R
import io.noties.markwon.app.sample.Sample import io.noties.markwon.app.sample.Sample
import io.noties.markwon.app.sample.SampleItem
import io.noties.markwon.app.sample.SampleManager import io.noties.markwon.app.sample.SampleManager
import io.noties.markwon.app.sample.SampleSearch import io.noties.markwon.app.sample.SampleSearch
import io.noties.markwon.app.sample.SampleItem
import io.noties.markwon.app.widget.SearchBar
import io.noties.markwon.app.utils.Cancellable import io.noties.markwon.app.utils.Cancellable
import io.noties.markwon.app.utils.displayName import io.noties.markwon.app.utils.displayName
import io.noties.markwon.app.utils.onPreDraw import io.noties.markwon.app.utils.onPreDraw
import io.noties.markwon.app.utils.recyclerView import io.noties.markwon.app.utils.recyclerView
import io.noties.markwon.app.utils.tagDisplayName import io.noties.markwon.app.utils.tagDisplayName
import io.noties.markwon.app.widget.SearchBar
import io.noties.markwon.movement.MovementMethodPlugin import io.noties.markwon.movement.MovementMethodPlugin
import io.noties.markwon.sample.annotations.MarkwonArtifact import io.noties.markwon.sample.annotations.MarkwonArtifact
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -91,6 +91,9 @@ class SampleListFragment : Fragment() {
recyclerView.paddingRight, recyclerView.paddingRight,
recyclerView.paddingBottom recyclerView.paddingBottom
) )
recyclerView.post {
recyclerView.scrollToPosition(0)
}
} }
val state: State? = savedInstanceState?.getParcelable(STATE) val state: State? = savedInstanceState?.getParcelable(STATE)

View File

@ -0,0 +1,49 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.LastLineSpacingSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181125321",
title = "Additional spacing after block",
description = "Add additional spacing (padding) after last line of a block",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.spacing, Tags.padding, Tags.span}
)
public class AdditionalSpacingSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Title title title title title title title title title title \n\ntext text text text";
// please note that bottom line (after 1 & 2 levels) will be drawn _AFTER_ padding
final int spacing = (int) (128 * context.getResources().getDisplayMetrics().density + .5F);
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder.headingBreakHeight(0);
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.appendFactory(
Heading.class,
(configuration, props) -> new LastLineSpacingSpan(spacing));
}
})
.build();
}
}

View File

@ -0,0 +1,59 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181130227",
title = "All blocks no padding",
description = "Do not render new lines (padding) after all blocks",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.block, Tags.spacing, Tags.padding, Tags.rendering}
)
public class AllBlocksNoForcedNewLineSample extends MarkwonTextViewSample {
@Override
public void render() {
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";
// extend default block handler
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(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(blockHandler);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,142 @@
package io.noties.markwon.app.samples;
import android.text.Spannable;
import android.text.Spanned;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.jetbrains.annotations.NotNull;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.LinkResolverDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.app.R;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.spans.HeadingSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181130728",
title = "Anchor plugin",
description = "HTML-like anchor links plugin, which scrolls to clicked anchor",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.links, Tags.anchor, Tags.plugin}
)
public class AnchorSample extends MarkwonTextViewSample {
private ScrollView scrollView;
@Override
public void onViewCreated(@NotNull View view) {
scrollView = view.findViewById(R.id.scroll_view);
super.onViewCreated(view);
}
@Override
public void render() {
final String lorem = context.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(context)
.usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
.build();
markwon.setMarkdown(textView, md);
}
}
class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
public interface ScrollTo {
void scrollTo(@NonNull TextView view, int top);
}
private final ScrollTo scrollTo;
AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) {
this.scrollTo = scrollTo;
}
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver(new AnchorLinkResolver(scrollTo));
}
@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
);
}
}
}
private static class AnchorLinkResolver extends LinkResolverDef {
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 static class AnchorSpan {
final String anchor;
AnchorSpan(@NonNull String anchor) {
this.anchor = anchor;
}
}
@NonNull
public static String createAnchor(@NonNull CharSequence content) {
return String.valueOf(content)
.replaceAll("[^\\w]", "")
.toLowerCase();
}
}

View File

@ -0,0 +1,436 @@
package io.noties.markwon.app.samples;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.text.style.ReplacementSpan;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.CustomNode;
import org.commonmark.node.Delimited;
import org.commonmark.node.Node;
import org.commonmark.node.Text;
import org.commonmark.parser.Parser;
import org.commonmark.parser.delimiter.DelimiterProcessor;
import org.commonmark.parser.delimiter.DelimiterRun;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181163248",
title = "Custom extension",
description = "Custom extension that adds an " +
"icon from resources and renders it as image with " +
"`@ic-name` syntax",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.parsing, Tags.rendering, Tags.plugin, Tags.image, Tags.extension, Tags.span}
)
public class CustomExtensionSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# 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" +
"" +
"### I AM ANOTHER HEADER\n\n" +
"" +
"Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64" +
"";
// note that we haven't registered CorePlugin, as it's the only one that can be
// implicitly deducted and added automatically. All other plugins require explicit
// `usePlugin` call
final Markwon markwon = Markwon.builder(context)
.usePlugin(IconPlugin.create(IconSpanProvider.create(context, 0)))
.build();
markwon.setMarkdown(textView, md);
}
}
class IconPlugin extends AbstractMarkwonPlugin {
@NonNull
public static IconPlugin create(@NonNull IconSpanProvider iconSpanProvider) {
return new IconPlugin(iconSpanProvider);
}
private final IconSpanProvider iconSpanProvider;
IconPlugin(@NonNull IconSpanProvider iconSpanProvider) {
this.iconSpanProvider = iconSpanProvider;
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.customDelimiterProcessor(IconProcessor.create());
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(IconNode.class, (visitor, iconNode) -> {
final String name = iconNode.name();
final String color = iconNode.color();
final String size = iconNode.size();
if (!TextUtils.isEmpty(name)
&& !TextUtils.isEmpty(color)
&& !TextUtils.isEmpty(size)) {
final int length = visitor.length();
visitor.builder().append(name);
visitor.setSpans(length, iconSpanProvider.provide(name, color, size));
visitor.builder().append(' ');
}
});
}
@NonNull
@Override
public String processMarkdown(@NonNull String markdown) {
return IconProcessor.prepare(markdown);
}
}
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 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) {
//noinspection ConstantConditions
return context.getDrawable(resId);
}
}
}
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);
}
}
}
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();
}
}
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
@NonNull
public String toString() {
return "IconNode{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", size='" + size + '\'' +
'}';
}
}
class IconGroupNode extends CustomNode {
}

View File

@ -0,0 +1,41 @@
package io.noties.markwon.app.samples;
import android.graphics.Color;
import androidx.annotation.NonNull;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181123617",
title = "Customize theme",
description = "Customize `MarkwonTheme` styling",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.style, Tags.theme, Tags.plugin}
)
public class CustomizeThemeSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "`A code` that is rendered differently\n\n```\nHello!\n```";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder
.codeBackgroundColor(Color.BLACK)
.codeTextColor(Color.RED);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,44 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181123308",
title = "Disable node from rendering",
description = "Disable _parsed_ node from being rendered (markdown syntax is still consumed)",
artifacts = {MarkwonArtifact.CORE},
tags = {Tags.parsing, Tags.rendering}
)
public class DisableNodeSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)";
final Markwon markwon = Markwon.builder(context)
.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);
// same method can be used to override existing visitor by specifying
// a new NodeVisitor instance
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,109 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import org.commonmark.parser.InlineParserFactory;
import org.commonmark.parser.Parser;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.inlineparser.InlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181162024",
title = "User mention and issue (via text)",
description = "Github-like user mention and issue " +
"rendering via `CorePlugin.OnTextAddedListener`",
artifacts = {MarkwonArtifact.CORE, MarkwonArtifact.INLINE_PARSER},
tags = {Tags.parsing, Tags.textAddedListener, Tags.rendering}
)
public class GithubUserIssueInlineParsingSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Custom Extension 2\n" +
"\n" +
"This is an issue #1\n" +
"Done by @noties";
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
// include all current defaults (otherwise will be empty - contain only our inline-processors)
// included by default, to create factory-builder without defaults call `factoryBuilderNoDefaults`
// .includeDefaults()
.addInlineProcessor(new IssueInlineProcessor())
.addInlineProcessor(new UserInlineProcessor())
.build();
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.inlineParserFactory(inlineParserFactory);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class IssueInlineProcessor extends InlineProcessor {
private static final Pattern RE = Pattern.compile("\\d+");
@Override
public char specialCharacter() {
return '#';
}
@Override
protected Node parse() {
final String id = match(RE);
if (id != null) {
final Link link = new Link(createIssueOrPullRequestLinkDestination(id), null);
link.appendChild(text("#" + id));
return link;
}
return null;
}
@NonNull
private static String createIssueOrPullRequestLinkDestination(@NonNull String id) {
return "https://github.com/noties/Markwon/issues/" + id;
}
}
class UserInlineProcessor extends InlineProcessor {
private static final Pattern RE = Pattern.compile("\\w+");
@Override
public char specialCharacter() {
return '@';
}
@Override
protected Node parse() {
final String user = match(RE);
if (user != null) {
final Link link = new Link(createUserLinkDestination(user), null);
link.appendChild(text("@" + user));
return link;
}
return null;
}
@NonNull
private static String createUserLinkDestination(@NonNull String user) {
return "https://github.com/" + user;
}
}

View File

@ -0,0 +1,120 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Link;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CorePlugin;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181162024",
title = "User mention and issue (via text)",
description = "Github-like user mention and issue " +
"rendering via `CorePlugin.OnTextAddedListener`",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.parsing, Tags.textAddedListener, Tags.rendering}
)
public class GithubUserIssueOnTextAddedSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Custom Extension 2\n" +
"\n" +
"This is an issue #1\n" +
"Done by @noties";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(CorePlugin.class, corePlugin ->
corePlugin.addOnTextAddedListener(new GithubLinkifyRegexTextAddedListener()));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class GithubLinkifyRegexTextAddedListener implements CorePlugin.OnTextAddedListener {
private static final Pattern PATTERN = Pattern.compile("((#\\d+)|(@\\w+))", Pattern.MULTILINE);
@Override
public void onTextAdded(@NonNull MarkwonVisitor visitor, @NonNull String text, int start) {
final Matcher matcher = PATTERN.matcher(text);
String value;
String url;
int index;
while (matcher.find()) {
value = matcher.group(1);
// detect which one it is
if ('#' == value.charAt(0)) {
url = createIssueOrPullRequestLink(value.substring(1));
} else {
url = createUserLink(value.substring(1));
}
// it's important to use `start` value (represents start-index of `text` in the visitor)
index = start + matcher.start();
setLink(visitor, url, index, index + value.length());
}
}
@NonNull
private String createIssueOrPullRequestLink(@NonNull String number) {
// issues and pull-requests on github follow the same pattern and we
// cannot know for sure which one it is, but if we use issues for all types,
// github will automatically redirect to pull-request if it's the one which is opened
return "https://github.com/noties/Markwon/issues/" + number;
}
@NonNull
private String createUserLink(@NonNull String user) {
return "https://github.com/" + user;
}
private void setLink(@NonNull MarkwonVisitor visitor, @NonNull String destination, int start, int end) {
// might a simpler one, but it doesn't respect possible changes to links
// visitor.builder().setSpan(
// new LinkSpan(visitor.configuration().theme(), destination, visitor.configuration().linkResolver()),
// start,
// end
// );
// use default handlers for links
final MarkwonConfiguration configuration = visitor.configuration();
final RenderProps renderProps = visitor.renderProps();
CoreProps.LINK_DESTINATION.set(renderProps, destination);
SpannableBuilder.setSpans(
visitor.builder(),
configuration.spansFactory().require(Link.class).getSpans(configuration, renderProps),
start,
end
);
}
}

View File

@ -0,0 +1,56 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import org.commonmark.node.Node;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181125924",
title = "Heading no padding (block handler)",
description = "Process padding (spacing) after heading with a " +
"`BlockHandler`",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.block, Tags.spacing, Tags.padding, Tags.heading, Tags.rendering}
)
public class HeadingNoSpaceBlockHandlerSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Title title title title title title title title title title\n\n" +
"text text text text" +
"";
final Markwon markwon = Markwon.builder(context)
.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();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,64 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181125622",
title = "Heading no padding",
description = "Do not add a new line after heading node",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.spacing, Tags.padding, Tags.spacing, Tags.rendering}
)
class HeadingNoSpaceSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Title title title title title title title title title title" +
"\n\ntext text text text" +
"";
final Markwon markwon = Markwon.builder(context)
.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();
// by default Markwon adds a new line here
// visitor.forceNewLine();
}
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,73 @@
package io.noties.markwon.app.samples;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.util.Collection;
import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
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.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181124201",
title = "Image destination custom scheme",
description = "Example of handling custom scheme " +
"(`https`, `ftp`, `whatever`, etc.) for images destination URLs " +
"with `ImagesPlugin`",
artifacts = {MarkwonArtifact.IMAGE},
tags = {Tags.image}
)
public class ImagesCustomSchemeSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "![image](myownscheme://en.wikipedia.org/static/images/project-logos/enwiki-2x.png)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
// use registry.require to obtain a plugin, does also
// a runtime validation if this plugin is registered
registry.require(ImagesPlugin.class, plugin -> plugin.addSchemeHandler(new SchemeHandler() {
// it's a sample only, most likely you won't need to
// use existing scheme-handler, this for demonstration purposes only
final NetworkSchemeHandler handler = NetworkSchemeHandler.create();
@NonNull
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
// just replace it with https for the sack of sample
final String url = raw.replace("myownscheme", "https");
return handler.handle(url, Uri.parse(url));
}
@NonNull
@Override
public Collection<String> supportedSchemes() {
return Collections.singleton("myownscheme");
}
}));
}
})
// or we can init plugin with this factory method
// .usePlugin(ImagesPlugin.create(plugin -> {
// plugin.addSchemeHandler(/**/)
// }))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,35 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181170857",
title = "Inline parsing without defaults",
description = "Configure inline parser plugin to **not** have any **inline** parsing",
artifacts = {MarkwonArtifact.INLINE_PARSER},
tags = {Tags.parsing}
)
public class InlinePluginNoDefaultsSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Heading\n" +
"`code` inlined and **bold** here";
final Markwon markwon = Markwon.builder(context)
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults(), factoryBuilder -> {
// // if anything, they can be included here
//// factoryBuilder.includeDefaults()
// }))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,188 @@
package io.noties.markwon.app.samples;
import android.text.TextUtils;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import org.commonmark.node.BulletList;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.OrderedList;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.Prop;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.spans.BulletListItemSpan;
import io.noties.markwon.core.spans.OrderedListItemSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181130954",
title = "Letter ordered list",
description = "Render bullet list inside an ordered list with letters instead of bullets",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.rendering, Tags.plugin, Tags.lists}
)
public class LetterOrderedListSample extends MarkwonTextViewSample {
@Override
public void render() {
// bullet list nested in ordered list renders letters instead of bullets
final String md = "" +
"1. Hello there!\n" +
"1. And here is how:\n" +
" - First\n" +
" - Second\n" +
" - Third\n" +
" 1. And first here\n\n";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin())
.build();
markwon.setMarkdown(textView, md);
}
}
class BulletListIsOrderedWithLettersWhenNestedPlugin extends AbstractMarkwonPlugin {
private static final Prop<String> BULLET_LETTER = Prop.of("my-bullet-letter");
// or introduce some kind of synchronization if planning to use from multiple threads,
// for example via ThreadLocal
private final SparseIntArray bulletCounter = new SparseIntArray();
@Override
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
// clear counter after render
bulletCounter.clear();
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
// NB that both ordered and bullet lists are represented
// by ListItem (must inspect parent to detect the type)
builder.on(ListItem.class, (visitor, listItem) -> {
// mimic original behaviour (copy-pasta from CorePlugin)
final int length = visitor.length();
visitor.visitChildren(listItem);
final Node parent = listItem.getParent();
if (parent instanceof OrderedList) {
final int start = ((OrderedList) parent).getStartNumber();
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED);
CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start);
// after we have visited the children increment start number
final OrderedList orderedList = (OrderedList) parent;
orderedList.setStartNumber(orderedList.getStartNumber() + 1);
} else {
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET);
if (isBulletOrdered(parent)) {
// obtain current count value
final int count = currentBulletCountIn(parent);
BULLET_LETTER.set(visitor.renderProps(), createBulletLetter(count));
// update current count value
setCurrentBulletCountIn(parent, count + 1);
} else {
CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
// clear letter info when regular bullet list is used
BULLET_LETTER.clear(visitor.renderProps());
}
}
visitor.setSpansForNodeOptional(listItem, length);
if (visitor.hasNext(listItem)) {
visitor.ensureNewLine();
}
});
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(ListItem.class, (configuration, props) -> {
final Object spans;
if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) {
final String letter = BULLET_LETTER.get(props);
if (!TextUtils.isEmpty(letter)) {
// NB, we are using OrderedListItemSpan here!
spans = new OrderedListItemSpan(
configuration.theme(),
letter
);
} else {
spans = new BulletListItemSpan(
configuration.theme(),
CoreProps.BULLET_LIST_ITEM_LEVEL.require(props)
);
}
} else {
final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props))
+ "." + '\u00a0';
spans = new OrderedListItemSpan(
configuration.theme(),
number
);
}
return spans;
});
}
private int currentBulletCountIn(@NonNull Node parent) {
return bulletCounter.get(parent.hashCode(), 0);
}
private void setCurrentBulletCountIn(@NonNull Node parent, int count) {
bulletCounter.put(parent.hashCode(), count);
}
@NonNull
private static String createBulletLetter(int count) {
// or lower `a`
// `'u00a0` is non-breakable space char
return ((char) ('A' + count)) + ".\u00a0";
}
private static int listLevel(@NonNull Node node) {
int level = 0;
Node parent = node.getParent();
while (parent != null) {
if (parent instanceof ListItem) {
level += 1;
}
parent = parent.getParent();
}
return level;
}
private static boolean isBulletOrdered(@NonNull Node node) {
node = node.getParent();
while (node != null) {
if (node instanceof OrderedList) {
return true;
}
if (node instanceof BulletList) {
return false;
}
node = node.getParent();
}
return false;
}
}

View File

@ -0,0 +1,97 @@
package io.noties.markwon.app.samples;
import android.text.Spanned;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Link;
import java.util.Locale;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.LinkResolver;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.LinkSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181122230",
title = "Obtain link title",
description = "Obtain title (text) of clicked link, `[title](#destination)`",
artifacts = {MarkwonArtifact.CORE},
tags = {Tags.links, Tags.span}
)
public class LinkTitleSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Links\n\n" +
"[link title](#)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(Link.class, (configuration, props) ->
// create a subclass of markwon LinkSpan
new ClickSelfSpan(
configuration.theme(),
CoreProps.LINK_DESTINATION.require(props),
configuration.linkResolver()
)
);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class ClickSelfSpan extends LinkSpan {
ClickSelfSpan(
@NonNull MarkwonTheme theme,
@NonNull String link,
@NonNull LinkResolver resolver) {
super(theme, link, resolver);
}
@Override
public void onClick(View widget) {
Toast.makeText(
widget.getContext(),
String.format(Locale.ROOT, "clicked link title: '%s'", linkTitle(widget)),
Toast.LENGTH_LONG
).show();
super.onClick(widget);
}
@Nullable
private CharSequence linkTitle(@NonNull View widget) {
if (!(widget instanceof TextView)) {
return null;
}
final Spanned spanned = (Spanned) ((TextView) widget).getText();
final int start = spanned.getSpanStart(this);
final int end = spanned.getSpanEnd(this);
if (start < 0 || end < 0) {
return null;
}
return spanned.subSequence(start, end);
}
}

View File

@ -0,0 +1,29 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181124005",
title = "Links without scheme",
description = "Links without scheme are considered to be `https`",
artifacts = {MarkwonArtifact.CORE},
tags = {Tags.links, Tags.defaults}
)
public class LinkWithoutSchemeSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Links without scheme\n" +
"[a link without scheme](github.com) is considered to be `https`.\n" +
"Override `LinkResolverDef` to change this functionality" +
"";
final Markwon markwon = Markwon.create(context);
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,49 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.parser.Parser;
import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181171212",
title = "No parsing",
description = "All commonmark parsing is disabled (both inlines and blocks)",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.parsing, Tags.rendering}
)
public class NoParsingSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Heading\n" +
"[link](#) was _here_ and `then` and it was:\n" +
"> a quote\n" +
"```java\n" +
"final int someJavaCode = 0;\n" +
"```\n";
final Markwon markwon = Markwon.builder(context)
// disable inline parsing
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.enabledBlockTypes(Collections.emptySet());
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,43 @@
package io.noties.markwon.app.samples;
import android.graphics.Color;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.NonNull;
import org.commonmark.node.Paragraph;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181122647",
title = "Paragraph style",
description = "Apply a style (via span) to a paragraph",
artifacts = {MarkwonArtifact.CORE},
tags = {Tags.paragraph, Tags.style, Tags.span}
)
public class ParagraphSpanStyle extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "# Hello!\n\nA paragraph?\n\nIt should be!";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
// apply a span to a Paragraph
builder.setFactory(Paragraph.class, (configuration, props) ->
new ForegroundColorSpan(Color.GREEN));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,209 @@
package io.noties.markwon.app.samples;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.text.style.ReplacementSpan;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181161505",
title = "Read more plugin",
description = "Plugin that adds expand/collapse (\"show all\"/\"show less\")",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.plugin}
)
public class ReadMorePluginSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"Lorem **ipsum** ![dolor](https://avatars2.githubusercontent.com/u/30618885?s=460&v=4) sit amet, consectetur adipiscing elit. Morbi vitae enim ut sem aliquet ultrices. Nunc a accumsan orci. Suspendisse tortor ante, lacinia ac scelerisque sed, dictum eget metus. Morbi ante augue, tristique eget quam in, vestibulum rutrum lacus. Nulla aliquam auctor cursus. Nulla at lacus condimentum, viverra lacus eget, sollicitudin ex. Cras efficitur leo dui, sit amet rutrum tellus venenatis et. Sed in facilisis libero. Etiam ultricies, nulla ut venenatis tincidunt, tortor erat tristique ante, non aliquet massa arcu eget nisl. Etiam gravida erat ante, sit amet lobortis mauris commodo nec. Praesent vitae sodales quam. Vivamus condimentum porta suscipit. Donec posuere id felis ac scelerisque. Vestibulum lacinia et leo id lobortis. Sed vitae dolor nec ligula dapibus finibus vel eu libero. Nam tincidunt maximus elit, sit amet tincidunt lacus laoreet malesuada.\n\n" +
"here we ![are](https://avatars2.githubusercontent.com/u/30618885?s=460&v=4)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.usePlugin(new ReadMorePlugin())
.build();
markwon.setMarkdown(textView, md);
}
}
/**
* Read more plugin based on text length. It is easier to implement than lines (need to adjust
* last line to include expand/collapse text).
*/
class ReadMorePlugin extends AbstractMarkwonPlugin {
@SuppressWarnings("FieldCanBeLocal")
private final int maxLength = 150;
@SuppressWarnings("FieldCanBeLocal")
private final String labelMore = "Show more...";
@SuppressWarnings("FieldCanBeLocal")
private final String labelLess = "...Show less";
@Override
public void configure(@NonNull Registry registry) {
// establish connections with all _dynamic_ content that your markdown supports,
// like images, tables, latex, etc
registry.require(ImagesPlugin.class);
registry.require(TablePlugin.class);
}
@Override
public void afterSetText(@NonNull TextView textView) {
final CharSequence text = textView.getText();
if (text.length() < maxLength) {
// everything is OK, no need to ellipsize)
return;
}
final int breakAt = breakTextAt(text, 0, maxLength);
final CharSequence cs = createCollapsedString(text, 0, breakAt);
textView.setText(cs);
}
@SuppressWarnings("SameParameterValue")
@NonNull
private CharSequence createCollapsedString(@NonNull CharSequence text, int start, int end) {
final SpannableStringBuilder builder = new SpannableStringBuilder(text, start, end);
// NB! each table row is represented as a space character and new-line (so length=2) no
// matter how many characters are inside table cells
// we can _clean_ this builder, for example remove all dynamic content (like images and tables,
// but keep them in full/expanded version)
//noinspection ConstantConditions
if (true) {
// it is an implementation detail but _mostly_ dynamic content is implemented as
// ReplacementSpans
final ReplacementSpan[] spans = builder.getSpans(0, builder.length(), ReplacementSpan.class);
if (spans != null) {
for (ReplacementSpan span : spans) {
builder.removeSpan(span);
}
}
// NB! if there will be a table in _preview_ (collapsed) then each row will be represented as a
// space and new-line
trim(builder);
}
final CharSequence fullText = createFullText(text, builder);
builder.append(' ');
final int length = builder.length();
builder.append(labelMore);
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
((TextView) widget).setText(fullText);
}
}, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return builder;
}
@NonNull
private CharSequence createFullText(@NonNull CharSequence text, @NonNull CharSequence collapsedText) {
// full/expanded text can also be different,
// for example it can be kept as-is and have no `collapse` functionality (once expanded cannot collapse)
// or can contain collapse feature
final CharSequence fullText;
//noinspection ConstantConditions
if (true) {
// for example let's allow collapsing
final SpannableStringBuilder builder = new SpannableStringBuilder(text);
builder.append(' ');
final int length = builder.length();
builder.append(labelLess);
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
((TextView) widget).setText(collapsedText);
}
}, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
fullText = builder;
} else {
fullText = text;
}
return fullText;
}
private static void trim(@NonNull SpannableStringBuilder builder) {
// NB! tables use `\u00a0` (non breaking space) which is not reported as white-space
char c;
for (int i = 0, length = builder.length(); i < length; i++) {
c = builder.charAt(i);
if (!Character.isWhitespace(c) && c != '\u00a0') {
if (i > 0) {
builder.replace(0, i, "");
}
break;
}
}
for (int i = builder.length() - 1; i >= 0; i--) {
c = builder.charAt(i);
if (!Character.isWhitespace(c) && c != '\u00a0') {
if (i < builder.length() - 1) {
builder.replace(i, builder.length(), "");
}
break;
}
}
}
// depending on your locale these can be different
// There is a BreakIterator in Android, but it is not reliable, still theoretically
// it should work better than hand-written and hardcoded rules
@SuppressWarnings("SameParameterValue")
private static int breakTextAt(@NonNull CharSequence text, int start, int max) {
int last = start;
// no need to check for _start_ (anyway will be ignored)
for (int i = start + max - 1; i > start; i--) {
final char c = text.charAt(i);
if (Character.isWhitespace(c)
|| c == '.'
|| c == ','
|| c == '!'
|| c == '?') {
// include this special character
last = i - 1;
break;
}
}
if (last <= start) {
// when used in subSequence last index is exclusive,
// so given max=150 would result in 0-149 subSequence
return start + max;
}
return last;
}
}

View File

@ -0,0 +1,29 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181125040",
title = "Soft break new line",
description = "Add a new line for a markdown soft-break node",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.newLine, Tags.softBreak}
)
class SoftBreakAddsNewLineSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"Hello there ->(line)\n(break)<- going on and on";
final Markwon markwon = Markwon.builder(context)
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,28 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181124706",
title = "Soft break adds space",
description = "By default a soft break (`\n`) will " +
"add a space character instead of new line",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.newLine, Tags.softBreak, Tags.defaults}
)
public class SoftBreakAddsSpace extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"Hello there ->(line)\n(break)<- going on and on";
// by default a soft break will add a space (instead of line break)
final Markwon markwon = Markwon.create(context);
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,167 @@
package io.noties.markwon.app.samples;
import android.view.View;
import android.widget.ScrollView;
import androidx.annotation.NonNull;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.BulletList;
import org.commonmark.node.CustomBlock;
import org.commonmark.node.Heading;
import org.commonmark.node.Link;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.Text;
import org.jetbrains.annotations.NotNull;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.R;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.SimpleBlockNodeVisitor;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181161226",
title = "Table of contents",
description = "Sample plugin that adds a table of contents header",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.rendering, Tags.plugin}
)
public class TableOfContentsSample extends MarkwonTextViewSample {
private ScrollView scrollView;
@Override
public void onViewCreated(@NotNull View view) {
scrollView = view.findViewById(R.id.scroll_view);
super.onViewCreated(view);
}
@Override
public void render() {
final String lorem = context.getString(R.string.lorem);
final String md = "" +
"# First\n" +
"" + lorem + "\n\n" +
"# Second\n" +
"" + lorem + "\n\n" +
"## Second level\n\n" +
"" + lorem + "\n\n" +
"### Level 3\n\n" +
"" + lorem + "\n\n" +
"# First again\n" +
"" + lorem + "\n\n";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new TableOfContentsPlugin())
// NB! plugin is defined in `AnchorSample` file
.usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
.build();
markwon.setMarkdown(textView, md);
}
}
class TableOfContentsPlugin extends AbstractMarkwonPlugin {
@Override
public void configure(@NonNull Registry registry) {
// just to make it explicit
registry.require(AnchorHeadingPlugin.class);
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(TableOfContentsBlock.class, new SimpleBlockNodeVisitor());
}
@Override
public void beforeRender(@NonNull Node node) {
// custom block to hold TOC
final TableOfContentsBlock block = new TableOfContentsBlock();
// create TOC title
{
final Text text = new Text("Table of contents");
final Heading heading = new Heading();
// important one - set TOC heading level
heading.setLevel(1);
heading.appendChild(text);
block.appendChild(heading);
}
final HeadingVisitor visitor = new HeadingVisitor(block);
node.accept(visitor);
// make it the very first node in rendered markdown
node.prependChild(block);
}
private static class HeadingVisitor extends AbstractVisitor {
private final BulletList bulletList = new BulletList();
private final StringBuilder builder = new StringBuilder();
private boolean isInsideHeading;
HeadingVisitor(@NonNull Node node) {
node.appendChild(bulletList);
}
@Override
public void visit(Heading heading) {
this.isInsideHeading = true;
try {
// reset build from previous content
builder.setLength(0);
// obtain level (can additionally filter by level, to skip lower ones)
final int level = heading.getLevel();
// build heading title
visitChildren(heading);
// initial list item
final ListItem listItem = new ListItem();
Node parent = listItem;
Node node = listItem;
for (int i = 1; i < level; i++) {
final ListItem li = new ListItem();
final BulletList bulletList = new BulletList();
bulletList.appendChild(li);
parent.appendChild(bulletList);
parent = li;
node = li;
}
final String content = builder.toString();
final Link link = new Link("#" + AnchorHeadingPlugin.createAnchor(content), null);
final Text text = new Text(content);
link.appendChild(text);
node.appendChild(link);
bulletList.appendChild(listItem);
} finally {
isInsideHeading = false;
}
}
@Override
public void visit(Text text) {
// can additionally check if we are building heading (to skip all other texts)
if (isInsideHeading) {
builder.append(text.getLiteral());
}
}
}
private static class TableOfContentsBlock extends CustomBlock {
}
}

View File

@ -0,0 +1,106 @@
package io.noties.markwon.app.samples.editor;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
import io.noties.markwon.core.spans.StrongEmphasisSpan;
import io.noties.markwon.editor.AbstractEditHandler;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.editor.MarkwonEditorUtils;
import io.noties.markwon.editor.PersistedSpans;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181165136",
title = "Additional edit span",
description = "Additional _edit_ span (span that is present in " +
"`EditText` along with punctuation",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tags.editor, Tags.span}
)
public class EditorAdditionalEditSpan extends MarkwonEditTextSample {
@Override
public void render() {
// An additional span is used to highlight strong-emphasis
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(context))
.useEditHandler(new BoldEditHandler())
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
}
class BoldEditHandler extends AbstractEditHandler<StrongEmphasisSpan> {
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
// Here we define which span is _persisted_ in EditText, it is not removed
// from EditText between text changes, but instead - reused (by changing
// position). Consider it as a cache for spans. We could use `StrongEmphasisSpan`
// here also, but I chose Bold to indicate that this span is not the same
// as in off-screen rendered markdown
builder.persistSpan(Bold.class, Bold::new);
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull StrongEmphasisSpan span,
int spanStart,
int spanTextLength) {
// Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4)
// because multiple inline markdown nodes can refer to the same text.
// For example, `**_~~hey~~_**` - we will receive `**_~~` in this method,
// and thus will have to manually find actual position in raw user input
final MarkwonEditorUtils.Match match =
MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__");
if (match != null) {
editable.setSpan(
// we handle StrongEmphasisSpan and represent it with Bold in EditText
// we still could use StrongEmphasisSpan, but it must be accessed
// via persistedSpans
persistedSpans.get(Bold.class),
match.start(),
match.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<StrongEmphasisSpan> markdownSpanType() {
return StrongEmphasisSpan.class;
}
}
class Bold extends MetricAffectingSpan {
public Bold() {
super();
}
@Override
public void updateDrawState(TextPaint tp) {
update(tp);
}
@Override
public void updateMeasureState(@NonNull TextPaint textPaint) {
update(textPaint);
}
private void update(@NonNull TextPaint paint) {
paint.setFakeBoldText(true);
}
}

View File

@ -0,0 +1,34 @@
package io.noties.markwon.app.samples.editor;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181165347",
title = "Additional plugin",
description = "Additional plugin for editor",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER, MarkwonArtifact.EXT_STRIKETHROUGH},
tags = {Tags.editor}
)
public class EditorAdditionalPluginSample extends MarkwonEditTextSample {
@Override
public void render() {
// As highlight works based on text-diff, everything that is present in input,
// but missing in resulting markdown is considered to be punctuation, this is why
// additional plugins do not need special handling
final Markwon markwon = Markwon.builder(context)
.usePlugin(StrikethroughPlugin.create())
.build();
final MarkwonEditor editor = MarkwonEditor.create(markwon);
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
}

View File

@ -0,0 +1,37 @@
package io.noties.markwon.app.samples.editor;
import android.text.style.ForegroundColorSpan;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181164627",
title = "Custom punctuation span",
description = "Custom span for punctuation in editor",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tags.editor, Tags.span}
)
public class EditorCustomPunctuationSample extends MarkwonEditTextSample {
@Override
public void render() {
// Use own punctuation span
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(context))
.punctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new)
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
}
class CustomPunctuationSpan extends ForegroundColorSpan {
CustomPunctuationSpan() {
super(0xFFFF0000); // RED
}
}

View File

@ -0,0 +1,87 @@
package io.noties.markwon.app.samples.editor;
import android.text.method.LinkMovementMethod;
import androidx.annotation.NonNull;
import org.commonmark.parser.InlineParserFactory;
import org.commonmark.parser.Parser;
import java.util.concurrent.Executors;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
import io.noties.markwon.app.samples.editor.shared.BlockQuoteEditHandler;
import io.noties.markwon.app.samples.editor.shared.CodeEditHandler;
import io.noties.markwon.app.samples.editor.shared.LinkEditHandler;
import io.noties.markwon.app.samples.editor.shared.StrikethroughEditHandler;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.editor.handler.EmphasisEditHandler;
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
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.linkify.LinkifyPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181165920",
title = "Multiple edit spans",
description = "Additional multiple edit spans for editor",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tags.editor}
)
public class EditorMultipleEditSpansSample extends MarkwonEditTextSample {
@Override
public void render() {
// for links to be clickable
editText.setMovementMethod(LinkMovementMethod.getInstance());
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
// no inline images will be parsed
.excludeInlineProcessor(BangInlineProcessor.class)
// no html tags will be parsed
.excludeInlineProcessor(HtmlInlineProcessor.class)
// no entities will be parsed (aka `&amp;` etc)
.excludeInlineProcessor(EntityInlineProcessor.class)
.build();
final Markwon markwon = Markwon.builder(context)
.usePlugin(StrikethroughPlugin.create())
.usePlugin(LinkifyPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
// disable all commonmark-java blocks, only inlines will be parsed
// builder.enabledBlockTypes(Collections.emptySet());
builder.inlineParserFactory(inlineParserFactory);
}
})
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.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));
}
}

View File

@ -0,0 +1,158 @@
package io.noties.markwon.app.samples.editor;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import androidx.annotation.NonNull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.debug.Debug;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181170348",
title = "Editor new line continuation",
description = "Sample of how new line character can be handled " +
"in order to add a _continuation_, for example adding a new " +
"bullet list item if current line starts with one",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tags.editor}
)
public class EditorNewLineContinuationSample extends MarkwonEditTextSample {
@Override
public void render() {
final Markwon markwon = Markwon.create(context);
final MarkwonEditor editor = MarkwonEditor.create(markwon);
final TextWatcher textWatcher = MarkdownNewLine
.wrap(MarkwonEditorTextWatcher.withProcess(editor));
editText.addTextChangedListener(textWatcher);
}
}
class MarkdownNewLine {
@NonNull
static TextWatcher wrap(@NonNull TextWatcher textWatcher) {
return new NewLineTextWatcher(textWatcher);
}
private MarkdownNewLine() {
}
private static class NewLineTextWatcher implements TextWatcher {
// NB! matches only bullet lists
private final Pattern RE = Pattern.compile("^( {0,3}[\\-+* ]+)(.+)*$");
private final TextWatcher wrapped;
private boolean selfChange;
// this content is pending to be inserted at the beginning
private String pendingNewLineContent;
private int pendingNewLineIndex;
// mark current edited line for removal (range start/end)
private int clearLineStart;
private int clearLineEnd;
NewLineTextWatcher(@NonNull TextWatcher wrapped) {
this.wrapped = wrapped;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// no op
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (selfChange) {
return;
}
// just one new character added
if (before == 0
&& count == 1
&& '\n' == s.charAt(start)) {
int end = -1;
for (int i = start - 1; i >= 0; i--) {
if ('\n' == s.charAt(i)) {
end = i + 1;
break;
}
}
// start at the very beginning
if (end < 0) {
end = 0;
}
final String pendingNewLineContent;
final int clearLineStart;
final int clearLineEnd;
final Matcher matcher = RE.matcher(s.subSequence(end, start));
if (matcher.matches()) {
// if second group is empty -> remove new line
final String content = matcher.group(2);
Debug.e("new line, content: '%s'", content);
if (TextUtils.isEmpty(content)) {
// another empty new line, remove this start
clearLineStart = end;
clearLineEnd = start;
pendingNewLineContent = null;
} else {
pendingNewLineContent = matcher.group(1);
clearLineStart = clearLineEnd = 0;
}
} else {
pendingNewLineContent = null;
clearLineStart = clearLineEnd = 0;
}
this.pendingNewLineContent = pendingNewLineContent;
this.pendingNewLineIndex = start + 1;
this.clearLineStart = clearLineStart;
this.clearLineEnd = clearLineEnd;
}
}
@Override
public void afterTextChanged(Editable s) {
if (selfChange) {
return;
}
if (pendingNewLineContent != null || clearLineStart < clearLineEnd) {
selfChange = true;
try {
if (pendingNewLineContent != null) {
s.insert(pendingNewLineIndex, pendingNewLineContent);
pendingNewLineContent = null;
} else {
s.replace(clearLineStart, clearLineEnd, "");
clearLineStart = clearLineEnd = 0;
}
} finally {
selfChange = false;
}
}
// NB, we assume MarkdownEditor text watcher that only listens for this event,
// other text-watchers must be interested in other events also
wrapped.afterTextChanged(s);
}
}
}

View File

@ -0,0 +1,34 @@
package io.noties.markwon.app.samples.editor;
import java.util.concurrent.Executors;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181164422",
title = "Editor with pre-render (async)",
description = "Editor functionality with highlight " +
"taking place in another thread",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tags.editor}
)
public class EditorPreRenderSample extends MarkwonEditTextSample {
@Override
public void render() {
// Process highlight in background thread
final Markwon markwon = Markwon.create(context);
final MarkwonEditor editor = MarkwonEditor.create(markwon);
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor,
Executors.newCachedThreadPool(),
editText));
}
}

View File

@ -0,0 +1,32 @@
package io.noties.markwon.app.samples.editor;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006181164227",
title = "Simple editor",
description = "Simple usage of editor with markdown highlight",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tags.editor}
)
public class EditorSimpleSample extends MarkwonEditTextSample {
@Override
public void render() {
// Process highlight in-place (right after text has changed)
// obtain Markwon instance
final Markwon markwon = Markwon.create(context);
// create editor
final MarkwonEditor editor = MarkwonEditor.create(markwon);
// set edit listener
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
}

View File

@ -0,0 +1,50 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.BlockQuoteSpan;
import io.noties.markwon.editor.EditHandler;
import io.noties.markwon.editor.PersistedSpans;
public class BlockQuoteEditHandler implements EditHandler<BlockQuoteSpan> {
private MarkwonTheme theme;
@Override
public void init(@NonNull Markwon markwon) {
this.theme = markwon.configuration().theme();
}
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder.persistSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull BlockQuoteSpan span,
int spanStart,
int spanTextLength) {
// todo: here we should actually find a proper ending of a block quote...
editable.setSpan(
persistedSpans.get(BlockQuoteSpan.class),
spanStart,
spanStart + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
@NonNull
@Override
public Class<BlockQuoteSpan> markdownSpanType() {
return BlockQuoteSpan.class;
}
}

View File

@ -0,0 +1,54 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.CodeSpan;
import io.noties.markwon.editor.EditHandler;
import io.noties.markwon.editor.MarkwonEditorUtils;
import io.noties.markwon.editor.PersistedSpans;
public class CodeEditHandler implements EditHandler<CodeSpan> {
private MarkwonTheme theme;
@Override
public void init(@NonNull Markwon markwon) {
this.theme = markwon.configuration().theme();
}
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder.persistSpan(CodeSpan.class, () -> new CodeSpan(theme));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull CodeSpan span,
int spanStart,
int spanTextLength) {
final MarkwonEditorUtils.Match match =
MarkwonEditorUtils.findDelimited(input, spanStart, "`");
if (match != null) {
editable.setSpan(
persistedSpans.get(CodeSpan.class),
match.start(),
match.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<CodeSpan> markdownSpanType() {
return CodeSpan.class;
}
}

View File

@ -0,0 +1,90 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.view.View;
import androidx.annotation.NonNull;
import io.noties.markwon.core.spans.LinkSpan;
import io.noties.markwon.editor.AbstractEditHandler;
import io.noties.markwon.editor.PersistedSpans;
public class LinkEditHandler extends AbstractEditHandler<LinkSpan> {
public interface OnClick {
void onClick(@NonNull View widget, @NonNull String link);
}
private final OnClick onClick;
public LinkEditHandler(@NonNull OnClick onClick) {
this.onClick = onClick;
}
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder.persistSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull LinkSpan span,
int spanStart,
int spanTextLength) {
final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class);
editLinkSpan.link = span.getLink();
// 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;
for (int i = spanStart, length = input.length(); i < length; i++) {
if (Character.isLetter(input.charAt(i))) {
start = i;
break;
}
}
if (start > -1) {
editable.setSpan(
editLinkSpan,
start,
start + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<LinkSpan> markdownSpanType() {
return LinkSpan.class;
}
static class EditLinkSpan extends ClickableSpan {
private final OnClick onClick;
String link;
EditLinkSpan(@NonNull OnClick onClick) {
this.onClick = onClick;
}
@Override
public void onClick(@NonNull View widget) {
if (link != null) {
onClick.onClick(widget, link);
}
}
}
}

View File

@ -0,0 +1,45 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import android.text.style.StrikethroughSpan;
import androidx.annotation.NonNull;
import io.noties.markwon.editor.AbstractEditHandler;
import io.noties.markwon.editor.MarkwonEditorUtils;
import io.noties.markwon.editor.PersistedSpans;
public class StrikethroughEditHandler extends AbstractEditHandler<StrikethroughSpan> {
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder.persistSpan(StrikethroughSpan.class, StrikethroughSpan::new);
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull StrikethroughSpan span,
int spanStart,
int spanTextLength) {
final MarkwonEditorUtils.Match match =
MarkwonEditorUtils.findDelimited(input, spanStart, "~~");
if (match != null) {
editable.setSpan(
persistedSpans.get(StrikethroughSpan.class),
match.start(),
match.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<StrikethroughSpan> markdownSpanType() {
return StrikethroughSpan.class;
}
}

View File

@ -0,0 +1,31 @@
package io.noties.markwon.app.samples.movementmethod
import io.noties.markwon.Markwon
import io.noties.markwon.app.sample.Tags
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.movement.MovementMethodPlugin
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
@MarkwonSampleInfo(
id = "202006181121803",
title = "Disable implicit movement method via plugin",
description = "Disable implicit movement method via `MovementMethodPlugin`",
artifacts = [MarkwonArtifact.CORE],
tags = [Tags.links, Tags.movementMethod, Tags.recyclerView]
)
class DisableImplicitMovementMethodPluginSample : MarkwonTextViewSample() {
override fun render() {
val md = """
# Disable implicit movement method via plugin
We can disable implicit movement method via `MovementMethodPlugin` &mdash;
[link-that-is-not-clickable](https://noties.io)
""".trimIndent()
val markwon = Markwon.builder(context)
.usePlugin(MovementMethodPlugin.none())
.build()
markwon.setMarkdown(textView, md)
}
}

View File

@ -12,7 +12,9 @@ import io.noties.markwon.sample.annotations.MarkwonSampleInfo
@MarkwonSampleInfo( @MarkwonSampleInfo(
id = "202006179081256", id = "202006179081256",
title = "Disable implicit movement method", title = "Disable implicit movement method",
description = "Configure `Markwon` to **not** apply implicit movement method", description = "Configure `Markwon` to **not** apply implicit movement method, " +
"which consumes touch events when used in a `RecyclerView` even when " +
"markdown does not contain links",
artifacts = [MarkwonArtifact.CORE], artifacts = [MarkwonArtifact.CORE],
tags = [Tags.plugin, Tags.movementMethod, Tags.links, Tags.recyclerView] tags = [Tags.plugin, Tags.movementMethod, Tags.links, Tags.recyclerView]
) )

View File

@ -11,7 +11,7 @@ import io.noties.markwon.sample.annotations.MarkwonSampleInfo
title = "Implicit movement method", title = "Implicit movement method",
description = "By default movement method is applied for links to be clickable", description = "By default movement method is applied for links to be clickable",
artifacts = [MarkwonArtifact.CORE], artifacts = [MarkwonArtifact.CORE],
tags = [Tags.movementMethod, Tags.links] tags = [Tags.movementMethod, Tags.links, Tags.defaults]
) )
class ImplicitMovementMethodSample : MarkwonTextViewSample() { class ImplicitMovementMethodSample : MarkwonTextViewSample() {
override fun render() { override fun render() {

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="8dip"
tools:ignore="HardcodedText">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1">
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="none"
android:hint="Markdown..."
android:inputType="text|textLongMessage|textMultiLine"
android:maxLines="100" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="ButtonStyle">
<Button
android:id="@+id/bold"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="B"
android:typeface="monospace" />
<Button
android:id="@+id/italic"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="I"
android:typeface="monospace" />
<Button
android:id="@+id/strike"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="S"
android:typeface="monospace" />
<Button
android:id="@+id/quote"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text=">"
android:typeface="monospace" />
<Button
android:id="@+id/code"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="`"
android:typeface="monospace" />
</LinearLayout>
</LinearLayout>

View File

@ -5,4 +5,16 @@
<string name="tab_bar_preview">Preview</string> <string name="tab_bar_preview">Preview</string>
<string name="tab_bar_code">Code</string> <string name="tab_bar_code">Code</string>
<string name="lorem"><![CDATA[
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.
Sed eu enim neque. Maecenas dictum faucibus ullamcorper. In ullamcorper orci in neque varius, nec rutrum nisl eleifend. Vestibulum tincidunt, ipsum at porta suscipit, est nibh commodo ex, et ultrices eros lacus vel neque. Praesent nulla velit, hendrerit sed sodales at, feugiat non lectus. Vivamus vel ultricies mi. Ut finibus commodo feugiat. Sed tempor lorem tortor, tempor sodales leo varius id. Curabitur rutrum sem at euismod rhoncus. Ut iaculis sem pharetra neque accumsan vestibulum. Nunc ultrices pharetra massa, at luctus nulla maximus et. Donec rhoncus in nisi eu pellentesque.
Sed consequat convallis massa quis bibendum. Phasellus vel suscipit velit. Pellentesque vel nisi at nisi facilisis condimentum. Cras feugiat magna ex, ut ultricies eros porttitor id. Quisque iaculis rutrum arcu eget placerat. Vestibulum pellentesque, urna eget consectetur commodo, est metus gravida nisl, id lacinia ligula ipsum porta nulla. Etiam aliquam convallis sollicitudin. Etiam sit amet mi aliquet purus faucibus hendrerit pharetra eu quam. Cras ut ornare sapien. Nam sapien diam, porttitor eu sagittis nec, vehicula nec mi. In fringilla turpis nec nisi fringilla, a facilisis eros ultrices. Proin eget arcu velit.
Sed gravida auctor malesuada. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus non justo sem. Donec dictum a elit quis pretium. Fusce accumsan sodales ornare. Nunc facilisis ligula eu ultrices faucibus. Proin vel molestie augue, ut convallis enim. Curabitur efficitur eget urna quis tempor. In non arcu non ex vulputate pulvinar. In laoreet aliquam mauris. Suspendisse vulputate magna at lorem bibendum, quis dapibus sapien malesuada. Curabitur at leo sit amet est egestas vestibulum. Sed hendrerit mi vel massa vestibulum, non semper nisl iaculis. Pellentesque feugiat at dolor a viverra. Sed ut consectetur tellus. Maecenas venenatis nunc a arcu convallis, at semper nulla cursus.
Curabitur placerat neque a congue pulvinar. Nulla non commodo est. Aenean nec gravida odio. Cras tincidunt accumsan pulvinar. Vestibulum non imperdiet velit. Sed ut mollis velit, vel ornare metus. Morbi consequat mi quis dui consectetur, sed condimentum lacus pulvinar.
]]></string>
</resources> </resources>