Sample app, moving more samples

This commit is contained in:
Dimitry Ivanov 2020-06-30 20:28:09 +03:00
parent bc58790704
commit 0b1544feae
44 changed files with 2218 additions and 111 deletions

View File

@ -1,4 +1,185 @@
[ [
{
"javaClassName": "io.noties.markwon.app.samples.image.ErrorImageSample",
"id": "202006182165828",
"title": "Image error handler",
"description": "",
"artifacts": [
"IMAGE"
],
"tags": [
"image"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.image.PlaceholderImageSample",
"id": "202006182165504",
"title": "Image with placeholder",
"description": "",
"artifacts": [
"IMAGE"
],
"tags": [
"image"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.image.GifImageSample",
"id": "202006182162214",
"title": "GIF image",
"description": "",
"artifacts": [
"IMAGE"
],
"tags": [
"GIF",
"image"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.image.SvgImageSample",
"id": "202006182161952",
"title": "SVG image",
"description": "",
"artifacts": [
"IMAGE"
],
"tags": [
"SVG",
"image"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.image.ImageSample",
"id": "202006182144659",
"title": "Markdown image",
"description": "",
"artifacts": [
"IMAGE"
],
"tags": [
"image"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.html.HtmlDetailsSample",
"id": "202006182120752",
"title": "Details HTML tag",
"description": "Handling of `details` HTML tag",
"artifacts": [
"HTML",
"IMAGE"
],
"tags": [
"image",
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.html.HtmlCenterTagSample",
"id": "202006182120101",
"title": "Center HTML tag",
"description": "Handling of `center` HTML tag",
"artifacts": [
"HTML",
"IMAGE"
],
"tags": [
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.html.HtmlEmptyTagReplacementSample",
"id": "202006182115725",
"title": "HTML empty tag replacement",
"description": "Render custom content when HTML tag contents is empty, in case of self-closed HTML tags or tags without content (closed right after opened)",
"artifacts": [
"HTML"
],
"tags": [
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.html.HtmlIFrameSample",
"id": "202006182115521",
"title": "IFrame HTML tag",
"description": "Handling of `iframe` HTML tag",
"artifacts": [
"HTML",
"IMAGE"
],
"tags": [
"image",
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.html.HtmlImageSample",
"id": "202006182115300",
"title": "Html images",
"description": "Usage of HTML images",
"artifacts": [
"HTML",
"IMAGE"
],
"tags": [
"image",
"rendering"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.html.HtmlEnhanceSample",
"id": "202006182115103",
"title": "Enhance custom HTML tag",
"description": "Custom HTML tag implementation that _enhances_ a part of text given start and end indices",
"artifacts": [
"HTML"
],
"tags": [
"rendering",
"span"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.html.HtmlRandomCharSize",
"id": "202006182114923",
"title": "Random char size HTML tag",
"description": "Implementation of a custom HTML tag handler that assigns each character a random size",
"artifacts": [
"HTML"
],
"tags": [
"rendering",
"span"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.html.HtmlAlignSample",
"id": "202006182114630",
"title": "Align HTML tag",
"description": "Implement custom HTML tag handling",
"artifacts": [
"HTML"
],
"tags": [
"rendering",
"span"
]
},
{
"javaClassName": "io.noties.markwon.app.samples.editor.EditorHeadingSample",
"id": "202006182113954",
"title": "Heading edit handler",
"description": "Handling of heading node in editor",
"artifacts": [
"EDITOR",
"INLINE_PARSER"
],
"tags": [
"editor"
]
},
{ {
"javaClassName": "io.noties.markwon.app.samples.NoParsingSample", "javaClassName": "io.noties.markwon.app.samples.NoParsingSample",
"id": "202006181171212", "id": "202006181171212",
@ -136,13 +317,12 @@
] ]
}, },
{ {
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample", "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample",
"id": "202006181162024", "id": "202006181162024",
"title": "User mention and issue (via text)", "title": "User mention and issue (via text)",
"description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`",
"artifacts": [ "artifacts": [
"CORE", "CORE"
"INLINE_PARSER"
], ],
"tags": [ "tags": [
"parsing", "parsing",
@ -151,12 +331,13 @@
] ]
}, },
{ {
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample", "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample",
"id": "202006181162024", "id": "202006181162024",
"title": "User mention and issue (via text)", "title": "User mention and issue (via text)",
"description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`",
"artifacts": [ "artifacts": [
"CORE" "CORE",
"INLINE_PARSER"
], ],
"tags": [ "tags": [
"parsing", "parsing",
@ -177,7 +358,7 @@
] ]
}, },
{ {
"javaClassName": "io.noties.markwon.app.samples.TableOfContentsSample", "javaClassName": "io.noties.markwon.app.samples.plugins.TableOfContentsSample",
"id": "202006181161226", "id": "202006181161226",
"title": "Table of contents", "title": "Table of contents",
"description": "Sample plugin that adds a table of contents header", "description": "Sample plugin that adds a table of contents header",
@ -204,7 +385,7 @@
] ]
}, },
{ {
"javaClassName": "io.noties.markwon.app.samples.AnchorSample", "javaClassName": "io.noties.markwon.app.samples.plugins.AnchorSample",
"id": "202006181130728", "id": "202006181130728",
"title": "Anchor plugin", "title": "Anchor plugin",
"description": "HTML-like anchor links plugin, which scrolls to clicked anchor", "description": "HTML-like anchor links plugin, which scrolls to clicked anchor",

View File

@ -2,6 +2,7 @@ package io.noties.markwon.app.readme
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
@ -144,8 +145,12 @@ class ReadMeActivity : Activity() {
data class Failure(val throwable: Throwable) : Result() data class Failure(val throwable: Throwable) : Result()
} }
private companion object { companion object {
fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try { fun makeIntent(context: Context): Intent {
return Intent(context, ReadMeActivity::class.java)
}
private fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try {
if (data == null) { if (data == null) {
callback.invoke(Result.Success(loadReadMe(context))) callback.invoke(Result.Success(loadReadMe(context)))

View File

@ -8,10 +8,10 @@ import android.widget.TextView
import io.noties.adapt.Item import io.noties.adapt.Item
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.app.R import io.noties.markwon.app.R
import io.noties.markwon.app.widget.FlowLayout
import io.noties.markwon.app.utils.displayName import io.noties.markwon.app.utils.displayName
import io.noties.markwon.app.utils.hidden import io.noties.markwon.app.utils.hidden
import io.noties.markwon.app.utils.tagDisplayName import io.noties.markwon.app.utils.tagDisplayName
import io.noties.markwon.app.widget.FlowLayout
import io.noties.markwon.sample.annotations.MarkwonArtifact import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.utils.NoCopySpannableFactory import io.noties.markwon.utils.NoCopySpannableFactory

View File

@ -27,4 +27,8 @@ object Tags {
const val textAddedListener = "text-added-listener" const val textAddedListener = "text-added-listener"
const val editor = "editor" const val editor = "editor"
const val span = "span" const val span = "span"
const val svg = "SVG"
const val gif = "GIF"
const val inline = "inline"
const val html = "HTML"
} }

View File

@ -1,9 +1,17 @@
package io.noties.markwon.app.sample.ui package io.noties.markwon.app.sample.ui
import android.content.Context import android.content.Context
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.StrikethroughSpan
import android.view.View import android.view.View
import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.TextView
import io.noties.markwon.app.R import io.noties.markwon.app.R
import io.noties.markwon.core.spans.EmphasisSpan
import io.noties.markwon.core.spans.StrongEmphasisSpan
import java.util.ArrayList
abstract class MarkwonEditTextSample : MarkwonSample() { abstract class MarkwonEditTextSample : MarkwonSample() {
@ -16,8 +24,80 @@ abstract class MarkwonEditTextSample: MarkwonSample() {
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
context = view.context context = view.context
editText = view.findViewById(R.id.edit_text) editText = view.findViewById(R.id.edit_text)
initBottomBar(view)
render() render()
} }
abstract fun render() abstract fun render()
private fun initBottomBar(view: View) {
// all except block-quote wraps if have selection, or inserts at current cursor position
val bold: Button = view.findViewById(R.id.bold)
val italic: Button = view.findViewById(R.id.italic)
val strike: Button = view.findViewById(R.id.strike)
val quote: Button = view.findViewById(R.id.quote)
val code: Button = view.findViewById(R.id.code)
addSpan(bold, StrongEmphasisSpan())
addSpan(italic, EmphasisSpan())
addSpan(strike, StrikethroughSpan())
bold.setOnClickListener(InsertOrWrapClickListener(editText, "**"))
italic.setOnClickListener(InsertOrWrapClickListener(editText, "_"))
strike.setOnClickListener(InsertOrWrapClickListener(editText, "~~"))
code.setOnClickListener(InsertOrWrapClickListener(editText, "`"))
quote.setOnClickListener {
val start = editText.selectionStart
val end = editText.selectionEnd
if (start < 0) {
return@setOnClickListener
}
if (start == end) {
editText.text.insert(start, "> ")
} else {
// wrap the whole selected area in a quote
val newLines: MutableList<Int> = ArrayList(3)
newLines.add(start)
val text = editText.text.subSequence(start, end).toString()
var index = text.indexOf('\n')
while (index != -1) {
newLines.add(start + index + 1)
index = text.indexOf('\n', index + 1)
}
for (i in newLines.indices.reversed()) {
editText.text.insert(newLines[i], "> ")
}
}
}
}
private fun addSpan(textView: TextView, vararg spans: Any) {
val builder = SpannableStringBuilder(textView.text)
val end = builder.length
for (span in spans) {
builder.setSpan(span, 0, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
textView.text = builder
}
private class InsertOrWrapClickListener(
private val editText: EditText,
private val text: String
) : View.OnClickListener {
override fun onClick(v: View) {
val start = editText.selectionStart
val end = editText.selectionEnd
if (start < 0) {
return
}
if (start == end) {
// insert at current position
editText.text.insert(start, text)
} else {
editText.text.insert(end, text)
editText.text.insert(start, text)
}
}
}
} }

View File

@ -19,12 +19,14 @@ import io.noties.debug.Debug
import io.noties.markwon.Markwon 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.readme.ReadMeActivity
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.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.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.hidden
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
@ -83,20 +85,21 @@ class SampleListFragment : Fragment() {
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
recyclerView.adapter = adapt recyclerView.adapter = adapt
// additional padding for RecyclerView // // additional padding for RecyclerView
searchBar.onPreDraw { // greatly complicates state restoration (items jump and a lot of times state cannot be
recyclerView.setPadding( // even restored (layout manager scrolls to top item and that's it)
recyclerView.paddingLeft, // searchBar.onPreDraw {
recyclerView.paddingTop + searchBar.height, // recyclerView.setPadding(
recyclerView.paddingRight, // recyclerView.paddingLeft,
recyclerView.paddingBottom // recyclerView.paddingTop + searchBar.height,
) // recyclerView.paddingRight,
recyclerView.post { // recyclerView.paddingBottom
recyclerView.scrollToPosition(0) // )
} // }
}
val state: State? = arguments?.getParcelable(STATE)
Debug.i(state)
val state: State? = savedInstanceState?.getParcelable(STATE)
pendingRecyclerScrollPosition = state?.recyclerScrollPosition pendingRecyclerScrollPosition = state?.recyclerScrollPosition
if (state?.search != null) { if (state?.search != null) {
searchBar.search(state.search) searchBar.search(state.search)
@ -106,6 +109,14 @@ class SampleListFragment : Fragment() {
} }
override fun onDestroyView() { override fun onDestroyView() {
val state = State(
search,
adapt.recyclerView?.scrollPosition
)
Debug.i(state)
arguments?.putParcelable(STATE, state)
val cancellable = this.cancellable val cancellable = this.cancellable
if (cancellable != null && !cancellable.isCancelled) { if (cancellable != null && !cancellable.isCancelled) {
cancellable.cancel() cancellable.cancel()
@ -114,24 +125,38 @@ class SampleListFragment : Fragment() {
super.onDestroyView() super.onDestroyView()
} }
override fun onSaveInstanceState(outState: Bundle) { // not called? yeah, whatever
super.onSaveInstanceState(outState) // override fun onSaveInstanceState(outState: Bundle) {
// super.onSaveInstanceState(outState)
val state = State( //
search, // val state = State(
adapt.recyclerView?.scrollPosition // search,
) // adapt.recyclerView?.scrollPosition
outState.putParcelable(STATE, state) // )
} // Debug.i(state)
// outState.putParcelable(STATE, state)
// }
private fun initAppBar(view: View) { private fun initAppBar(view: View) {
val appBar = view.findViewById<View>(R.id.app_bar) val appBar = view.findViewById<View>(R.id.app_bar)
val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon) val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon)
val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title) val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title)
val appBarIconReadme: ImageView = appBar.findViewById(R.id.app_bar_icon_readme)
val isInitialScreen = type is Type.All
appBarIcon.hidden = isInitialScreen
appBarIconReadme.hidden = !isInitialScreen
val type = this.type val type = this.type
if (type is Type.All) { if (type is Type.All) {
appBarIconReadme.setOnClickListener {
context?.let {
val intent = ReadMeActivity.makeIntent(it)
it.startActivity(intent)
}
}
return return
} }
@ -162,14 +187,23 @@ class SampleListFragment : Fragment() {
} }
adapt.setItems(items) adapt.setItems(items)
val recyclerView = adapt.recyclerView ?: return
val scrollPosition = pendingRecyclerScrollPosition val scrollPosition = pendingRecyclerScrollPosition
Debug.i(scrollPosition)
Debug.trace()
if (scrollPosition != null) { if (scrollPosition != null) {
pendingRecyclerScrollPosition = null pendingRecyclerScrollPosition = null
val recyclerView = adapt.recyclerView ?: return
recyclerView.onPreDraw { recyclerView.onPreDraw {
(recyclerView.layoutManager as? LinearLayoutManager) (recyclerView.layoutManager as? LinearLayoutManager)
?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset) ?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset)
} }
} else {
recyclerView.onPreDraw {
recyclerView.scrollToPosition(0)
}
} }
} }
@ -207,6 +241,9 @@ class SampleListFragment : Fragment() {
else -> SampleSearch.All(search) else -> SampleSearch.All(search)
} }
Debug.i(sampleSearch)
Debug.trace()
// clear current // clear current
cancellable?.let { cancellable?.let {
if (!it.isCancelled) { if (!it.isCancelled) {
@ -277,12 +314,21 @@ class SampleListFragment : Fragment() {
private val RecyclerView.scrollPosition: RecyclerScrollPosition? private val RecyclerView.scrollPosition: RecyclerScrollPosition?
get() { get() {
val holder = findViewHolderForLayoutPosition(0) ?: return null val holder = findFirstVisibleViewHolder() ?: return null
val position = holder.adapterPosition val position = holder.adapterPosition
val offset = holder.itemView.top val offset = holder.itemView.top
return RecyclerScrollPosition(position, offset) return RecyclerScrollPosition(position, offset)
} }
// because findViewHolderForLayoutPosition doesn't work :'(
private fun RecyclerView.findFirstVisibleViewHolder(): RecyclerView.ViewHolder? {
if (childCount > 0) {
val child = getChildAt(0)
return findContainingViewHolder(child)
}
return null
}
private sealed class Type { private sealed class Type {
class Artifact(val artifact: MarkwonArtifact) : Type() class Artifact(val artifact: MarkwonArtifact) : Type()
class Tag(val tag: String) : Type() class Tag(val tag: String) : Type()

View File

@ -45,5 +45,7 @@ public class AdditionalSpacingSample extends MarkwonTextViewSample {
} }
}) })
.build(); .build();
markwon.setMarkdown(textView, md);
} }
} }

View File

@ -61,7 +61,7 @@ class ReadMorePlugin extends AbstractMarkwonPlugin {
// establish connections with all _dynamic_ content that your markdown supports, // establish connections with all _dynamic_ content that your markdown supports,
// like images, tables, latex, etc // like images, tables, latex, etc
registry.require(ImagesPlugin.class); registry.require(ImagesPlugin.class);
registry.require(TablePlugin.class); // registry.require(TablePlugin.class);
} }
@Override @Override

View File

@ -0,0 +1,32 @@
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.app.samples.editor.shared.HeadingEditHandler;
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 = "202006182113954",
title = "Heading edit handler",
description = "Handling of heading node in editor",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tags.editor}
)
public class EditorHeadingSample extends MarkwonEditTextSample {
@Override
public void render() {
final Markwon markwon = Markwon.create(context);
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
.useEditHandler(new HeadingEditHandler())
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor, Executors.newSingleThreadExecutor(), editText));
}
}

View File

@ -0,0 +1,82 @@
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.HeadingSpan;
import io.noties.markwon.editor.EditHandler;
import io.noties.markwon.editor.PersistedSpans;
public class HeadingEditHandler implements EditHandler<HeadingSpan> {
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(Head1.class, () -> new Head1(theme))
.persistSpan(Head2.class, () -> new Head2(theme));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull HeadingSpan span,
int spanStart,
int spanTextLength
) {
final Class<?> type;
switch (span.getLevel()) {
case 1:
type = Head1.class;
break;
case 2:
type = Head2.class;
break;
default:
type = null;
}
if (type != null) {
final int index = input.indexOf('\n', spanStart + spanTextLength);
final int end = index < 0
? input.length()
: index;
editable.setSpan(
persistedSpans.get(type),
spanStart,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<HeadingSpan> markdownSpanType() {
return HeadingSpan.class;
}
private static class Head1 extends HeadingSpan {
Head1(@NonNull MarkwonTheme theme) {
super(theme, 1);
}
}
private static class Head2 extends HeadingSpan {
Head2(@NonNull MarkwonTheme theme) {
super(theme, 2);
}
}
}

View File

@ -0,0 +1,87 @@
package io.noties.markwon.app.samples.html;
import android.text.Layout;
import android.text.style.AlignmentSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.tag.SimpleTagHandler;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182114630",
title = "Align HTML tag",
description = "Implement custom HTML tag handling",
artifacts = MarkwonArtifact.HTML,
tags = {Tags.rendering, Tags.span, Tags.html}
)
public class HtmlAlignSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"<align center>We are centered</align>\n" +
"\n" +
"<align end>We are at the end</align>\n" +
"\n" +
"<align>We should be at the start</align>\n" +
"\n";
final Markwon markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
.addHandler(new AlignTagHandler()));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class AlignTagHandler extends SimpleTagHandler {
@Nullable
@Override
public Object getSpans(
@NonNull MarkwonConfiguration configuration,
@NonNull RenderProps renderProps,
@NonNull HtmlTag tag) {
final Layout.Alignment alignment;
// html attribute without value, <align center></align>
if (tag.attributes().containsKey("center")) {
alignment = Layout.Alignment.ALIGN_CENTER;
} else if (tag.attributes().containsKey("end")) {
alignment = Layout.Alignment.ALIGN_OPPOSITE;
} else {
// empty value or any other will make regular alignment
alignment = Layout.Alignment.ALIGN_NORMAL;
}
return new AlignmentSpan.Standard(alignment);
}
@NonNull
@Override
public Collection<String> supportedTags() {
return Collections.singleton("align");
}
}

View File

@ -0,0 +1,115 @@
package io.noties.markwon.app.samples.html;
import android.text.Layout;
import android.text.style.AlignmentSpan;
import androidx.annotation.NonNull;
import java.util.Collection;
import java.util.Collections;
import io.noties.debug.Debug;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.app.samples.html.shared.IFrameHtmlPlugin;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import io.noties.markwon.html.TagHandler;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182120101",
title = "Center HTML tag",
description = "Handling of `center` HTML tag",
artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE},
tags = {Tags.rendering, Tags.html}
)
public class HtmlCenterTagSample extends MarkwonTextViewSample {
@Override
public void render() {
final String html = "<html>\n" +
"\n" +
"<head></head>\n" +
"\n" +
"<body>\n" +
" <p></p>\n" +
" <h3>LiSA's Sword Art Online: Alicization OP Song \"ADAMAS\" Certified Platinum with 250,000 Downloads</h3>\n" +
" <p></p>\n" +
" <h5>The upper tune was already certified Gold one month after its digital release</h5>\n" +
" <p>According to The Recording Industry Association of Japan (RIAJ)'s monthly report for April 2020, one of the <span\n" +
" style=\"color: #ff9900;\"><strong><a href=\"http://www.lxixsxa.com/\" target=\"_blank\"><span\n" +
" style=\"color: #ff9900;\">LiSA</span></a></strong></span>'s 14th single songs,\n" +
" <strong>\"ADAMAS\"</strong>&nbsp;(the first OP theme for the TV anime <a href=\"/sword-art-online\"\n" +
" target=\"_blank\"><span style=\"color: #ff9900;\"><strong><em>Sword Art Online:\n" +
" Alicization</em></strong></span></a>) has been certified <strong>Platinum</strong> for\n" +
" surpassing 250,000 downloads.</p>\n" +
" <p>&nbsp;</p>\n" +
" <p>As a double A-side single with <strong>\"Akai Wana (who loves it?),\"</strong> <strong>\"ADAMAS\"</strong> was\n" +
" released from SACRA Music in Japan on December 12, 2018. Its CD single ranked second in Oricon's weekly single\n" +
" chart by selling 35,000 copies in its first week. Meanwhile, the song was released digitally two months prior to\n" +
" its CD release, October 8, then reached Gold (100,000 downloads) in the following month.</p>\n" +
" <p>&nbsp;</p>\n" +
" <p>&nbsp;</p>\n" +
" <center>\n" +
" <p><strong>\"ADAMAS\"</strong> MV YouTube EDIT ver.:</p>\n" +
" <p><iframe src=\"https://www.youtube.com/embed/UeEIl4JlE-g\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe>\n" +
" </p>\n" +
" <p>&nbsp;</p>\n" +
" <p>Standard edition CD jacket:</p>\n" +
" <p><img src=\"https://img1.ak.crunchyroll.com/i/spire2/d7b1d6bc7563224388ef5ffc04a967581589950464_full.jpg\"\n" +
" alt=\"\" width=\"640\" height=\"635\"></p>\n" +
" </center>\n" +
" <p>&nbsp;&nbsp;</p>\n" +
" <hr>\n" +
" <p>&nbsp;</p>\n" +
" <p>Source: RIAJ press release</p>\n" +
" <p>&nbsp;</p>\n" +
" <p><em>©SACRA MUSIC</em></p>\n" +
" <p>&nbsp;</p>\n" +
" <p style=\"text-align: center;\"><a href=\"https://got.cr/PremiumTrial-NewsBanner4\"><em><img\n" +
" src=\"https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1559091520_full.png\"\n" +
" alt=\"\" width=\"640\" height=\"43\"></em></a></p>\n" +
"</body>\n" +
"\n" +
"</html>";
final Markwon markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create(plugin ->
plugin.addHandler(new CenterTagHandler())))
.usePlugin(new IFrameHtmlPlugin())
.usePlugin(ImagesPlugin.create())
.build();
markwon.setMarkdown(textView, html);
}
}
class CenterTagHandler extends TagHandler {
@Override
public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) {
Debug.e("center, isBlock: %s", tag.isBlock());
if (tag.isBlock()) {
visitChildren(visitor, renderer, tag.getAsBlock());
}
SpannableBuilder.setSpans(
visitor.builder(),
new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
tag.start(),
tag.end()
);
}
@NonNull
@Override
public Collection<String> supportedTags() {
return Collections.singleton("center");
}
}

View File

@ -0,0 +1,426 @@
package io.noties.markwon.app.samples.html;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.text.style.LeadingMarginSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.app.R;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonSample;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import io.noties.markwon.html.TagHandler;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.utils.LeadingMarginUtils;
import io.noties.markwon.utils.NoCopySpannableFactory;
@MarkwonSampleInfo(
id = "202006182120752",
title = "Details HTML tag",
description = "Handling of `details` HTML tag",
artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE},
tags = {Tags.image, Tags.rendering, Tags.html}
)
public class HtmlDetailsSample extends MarkwonSample {
private Context context;
private ViewGroup content;
@Override
protected int getLayoutResId() {
return R.layout.activity_html_details;
}
@Override
public void onViewCreated(@NotNull View view) {
context = view.getContext();
content = view.findViewById(R.id.content);
render();
}
private void render() {
final String md = "# Hello\n\n<details>\n" +
" <summary>stuff with \n\n*mark* **down**\n\n</summary>\n" +
" <p>\n\n" +
"<!-- the above p cannot start right at the beginning of the line and is mandatory for everything else to work -->\n" +
"## *formatted* **heading** with [a](link)\n" +
"```java\n" +
"code block\n" +
"```\n" +
"\n" +
" <details>\n" +
" <summary><small>nested</small> stuff</summary><p>\n" +
"<!-- alternative placement of p shown above -->\n" +
"\n" +
"* list\n" +
"* with\n" +
"\n\n" +
"![img](https://raw.githubusercontent.com/noties/Markwon/master/art/markwon_logo.png)\n\n" +
" 1. nested\n" +
" 1. items\n" +
"\n" +
" ```java\n" +
" // including code\n" +
" ```\n" +
" 1. blocks\n" +
"\n" +
"<details><summary>The 3rd!</summary>\n\n" +
"**bold** _em_\n</details>" +
" </p></details>\n" +
"</p></details>\n\n" +
"and **this** *is* how...";
final Markwon markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create(plugin ->
plugin.addHandler(new DetailsTagHandler())))
.usePlugin(ImagesPlugin.create())
.build();
final Spanned spanned = markwon.toMarkdown(md);
final DetailsParsingSpan[] spans = spanned.getSpans(0, spanned.length(), DetailsParsingSpan.class);
// if we have no details, proceed as usual (single text-view)
if (spans == null || spans.length == 0) {
// no details
final TextView textView = appendTextView();
markwon.setParsedMarkdown(textView, spanned);
return;
}
final List<DetailsElement> list = new ArrayList<>();
for (DetailsParsingSpan span : spans) {
final DetailsElement e = settle(new DetailsElement(spanned.getSpanStart(span), spanned.getSpanEnd(span), span.summary), list);
if (e != null) {
list.add(e);
}
}
for (DetailsElement element : list) {
initDetails(element, spanned);
}
sort(list);
TextView textView;
int start = 0;
for (DetailsElement element : list) {
if (element.start != start) {
// subSequence and add new TextView
textView = appendTextView();
textView.setText(subSequenceTrimmed(spanned, start, element.start));
}
// now add details TextView
textView = appendTextView();
initDetailsTextView(markwon, textView, element);
start = element.end;
}
if (start != spanned.length()) {
// another textView with rest content
textView = appendTextView();
textView.setText(subSequenceTrimmed(spanned, start, spanned.length()));
}
}
@NonNull
private TextView appendTextView() {
final View view = LayoutInflater.from(context)
.inflate(R.layout.view_html_details_text_view, content, false);
final TextView textView = view.findViewById(R.id.text);
content.addView(view);
return textView;
}
private void initDetailsTextView(
@NonNull Markwon markwon,
@NonNull TextView textView,
@NonNull DetailsElement element) {
// minor optimization
textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
// so, each element with children is a details tag
// there is a reason why we needed the SpannableBuilder in the first place -> we must revert spans
// final SpannableStringBuilder builder = new SpannableStringBuilder();
final SpannableBuilder builder = new SpannableBuilder();
append(builder, markwon, textView, element, element);
markwon.setParsedMarkdown(textView, builder.spannableStringBuilder());
}
private void append(
@NonNull SpannableBuilder builder,
@NonNull Markwon markwon,
@NonNull TextView textView,
@NonNull DetailsElement root,
@NonNull DetailsElement element) {
if (!element.children.isEmpty()) {
final int start = builder.length();
// builder.append(element.content);
builder.append(subSequenceTrimmed(element.content, 0, element.content.length()));
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
element.expanded = !element.expanded;
initDetailsTextView(markwon, textView, root);
}
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (element.expanded) {
for (DetailsElement child : element.children) {
append(builder, markwon, textView, root, child);
}
}
builder.setSpan(new DetailsSpan(markwon.configuration().theme(), element), start);
} else {
builder.append(element.content);
}
}
// if null -> remove from where it was processed,
// else replace from where it was processed with a new one (can become expandable)
@Nullable
private static DetailsElement settle(
@NonNull DetailsElement element,
@NonNull List<? extends DetailsElement> elements) {
for (DetailsElement e : elements) {
if (element.start > e.start && element.end <= e.end) {
final DetailsElement settled = settle(element, e.children);
if (settled != null) {
// the thing is we must balance children if done like this
// let's just create a tree actually, so we are easier to modify
final Iterator<DetailsElement> iterator = e.children.iterator();
while (iterator.hasNext()) {
final DetailsElement balanced = settle(iterator.next(), Collections.singletonList(element));
if (balanced == null) {
iterator.remove();
}
}
// add to our children
e.children.add(element);
}
return null;
}
}
return element;
}
private static void initDetails(@NonNull DetailsElement element, @NonNull Spanned spanned) {
int end = element.end;
for (int i = element.children.size() - 1; i >= 0; i--) {
final DetailsElement child = element.children.get(i);
if (child.end < end) {
element.children.add(new DetailsElement(child.end, end, spanned.subSequence(child.end, end)));
}
initDetails(child, spanned);
end = child.start;
}
final int start = (element.start + element.content.length());
if (end != start) {
element.children.add(new DetailsElement(start, end, spanned.subSequence(start, end)));
}
}
private static void sort(@NonNull List<DetailsElement> elements) {
Collections.sort(elements, (o1, o2) -> Integer.compare(o1.start, o2.start));
for (DetailsElement element : elements) {
sort(element.children);
}
}
@NonNull
private static CharSequence subSequenceTrimmed(@NonNull CharSequence cs, int start, int end) {
while (start < end) {
final boolean isStartEmpty = Character.isWhitespace(cs.charAt(start));
final boolean isEndEmpty = Character.isWhitespace(cs.charAt(end - 1));
if (!isStartEmpty && !isEndEmpty) {
break;
}
if (isStartEmpty) {
start += 1;
}
if (isEndEmpty) {
end -= 1;
}
}
return cs.subSequence(start, end);
}
private static class DetailsElement {
final int start;
final int end;
final CharSequence content;
final List<DetailsElement> children = new ArrayList<>(0);
boolean expanded;
DetailsElement(int start, int end, @NonNull CharSequence content) {
this.start = start;
this.end = end;
this.content = content;
}
@Override
@NonNull
public String toString() {
return "DetailsElement{" +
"start=" + start +
", end=" + end +
", content=" + toStringContent(content) +
", children=" + children +
", expanded=" + expanded +
'}';
}
@NonNull
private static String toStringContent(@NonNull CharSequence cs) {
return cs.toString().replaceAll("\n", "\\n");
}
}
private static class DetailsTagHandler extends TagHandler {
@Override
public void handle(
@NonNull MarkwonVisitor visitor,
@NonNull MarkwonHtmlRenderer renderer,
@NonNull HtmlTag tag) {
int summaryEnd = -1;
for (HtmlTag child : tag.getAsBlock().children()) {
if (!child.isClosed()) {
continue;
}
if ("summary".equals(child.name())) {
summaryEnd = child.end();
}
final TagHandler tagHandler = renderer.tagHandler(child.name());
if (tagHandler != null) {
tagHandler.handle(visitor, renderer, child);
} else if (child.isBlock()) {
visitChildren(visitor, renderer, child.getAsBlock());
}
}
if (summaryEnd > -1) {
visitor.builder().setSpan(new DetailsParsingSpan(
subSequenceTrimmed(visitor.builder(), tag.start(), summaryEnd)
), tag.start(), tag.end());
}
}
@NonNull
@Override
public Collection<String> supportedTags() {
return Collections.singleton("details");
}
}
private static class DetailsParsingSpan {
final CharSequence summary;
DetailsParsingSpan(@NonNull CharSequence summary) {
this.summary = summary;
}
}
private static class DetailsSpan implements LeadingMarginSpan {
private final DetailsElement element;
private final int blockMargin;
private final int blockQuoteWidth;
private final Rect rect = new Rect();
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
DetailsSpan(@NonNull MarkwonTheme theme, @NonNull DetailsElement element) {
this.element = element;
this.blockMargin = theme.getBlockMargin();
this.blockQuoteWidth = theme.getBlockQuoteWidth();
this.paint.setStyle(Paint.Style.FILL);
}
@Override
public int getLeadingMargin(boolean first) {
return blockMargin;
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
if (LeadingMarginUtils.selfStart(start, text, this)) {
rect.set(x, top, x + blockMargin, bottom);
if (element.expanded) {
paint.setColor(Color.GREEN);
} else {
paint.setColor(Color.RED);
}
paint.setStyle(Paint.Style.FILL);
c.drawRect(rect, paint);
} else {
if (element.expanded) {
final int l = (blockMargin - blockQuoteWidth) / 2;
rect.set(x + l, top, x + l + blockQuoteWidth, bottom);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.GRAY);
c.drawRect(rect, paint);
}
}
}
}
}

View File

@ -0,0 +1,42 @@
package io.noties.markwon.app.samples.html;
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.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182171424",
title = "Disable HTML",
description = "Disable HTML via replacing special `<` and `>` symbols",
artifacts = MarkwonArtifact.CORE,
tags = {Tags.html, Tags.rendering, Tags.parsing, Tags.plugin}
)
public class HtmlDisableSanitizeSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "# Html <b>disabled</b>\n\n" +
"<em>emphasis <strong>strong</strong>\n\n" +
"<p>paragraph <img src='hey.jpg' /></p>\n\n" +
"<test></test>\n\n" +
"<test>";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@NonNull
@Override
public String processMarkdown(@NonNull String markdown) {
return markdown
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,47 @@
package io.noties.markwon.app.samples.html;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.html.HtmlEmptyTagReplacement;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182115725",
title = "HTML empty tag replacement",
description = "Render custom content when HTML tag contents is empty, " +
"in case of self-closed HTML tags or tags without content (closed " +
"right after opened)",
artifacts = MarkwonArtifact.HTML,
tags = {Tags.rendering, Tags.html}
)
public class HtmlEmptyTagReplacementSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"<empty></empty> the `<empty></empty>` is replaced?";
final Markwon markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create(plugin -> {
plugin.emptyTagReplacement(new HtmlEmptyTagReplacement() {
@Nullable
@Override
public String replace(@NonNull HtmlTag tag) {
if ("empty".equals(tag.name())) {
return "REPLACED_EMPTY_WITH_IT";
}
return super.replace(tag);
}
});
}))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,102 @@
package io.noties.markwon.app.samples.html;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import java.util.Collection;
import java.util.Collections;
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.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import io.noties.markwon.html.TagHandler;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182115103",
title = "Enhance custom HTML tag",
description = "Custom HTML tag implementation " +
"that _enhances_ a part of text given start and end indices",
artifacts = MarkwonArtifact.HTML,
tags = {Tags.rendering, Tags.span, Tags.html}
)
public class HtmlEnhanceSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"<enhance start=\"5\" end=\"12\">This is text that must be enhanced, at least a part of it</enhance>";
final Markwon markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
.addHandler(new EnhanceTagHandler((int) (textView.getTextSize() * 2 + .05F))));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class EnhanceTagHandler extends TagHandler {
private final int enhanceTextSize;
EnhanceTagHandler(@Px int enhanceTextSize) {
this.enhanceTextSize = enhanceTextSize;
}
@Override
public void handle(
@NonNull MarkwonVisitor visitor,
@NonNull MarkwonHtmlRenderer renderer,
@NonNull HtmlTag tag) {
// we require start and end to be present
final int start = parsePosition(tag.attributes().get("start"));
final int end = parsePosition(tag.attributes().get("end"));
if (start > -1 && end > -1) {
visitor.builder().setSpan(
new AbsoluteSizeSpan(enhanceTextSize),
tag.start() + start,
tag.start() + end
);
}
}
@NonNull
@Override
public Collection<String> supportedTags() {
return Collections.singleton("enhance");
}
private static int parsePosition(@Nullable String value) {
int position;
if (!TextUtils.isEmpty(value)) {
try {
position = Integer.parseInt(value);
} catch (NumberFormatException e) {
e.printStackTrace();
position = -1;
}
} else {
position = -1;
}
return position;
}
}

View File

@ -0,0 +1,50 @@
package io.noties.markwon.app.samples.html;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.app.samples.html.shared.IFrameHtmlPlugin;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182115521",
title = "IFrame HTML tag",
description = "Handling of `iframe` HTML tag",
artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE},
tags = {Tags.image, Tags.rendering, Tags.html}
)
public class HtmlIFrameSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Hello iframe\n\n" +
"<p class=\"p1\"><img title=\"JUMP FORCE\" src=\"https://img1.ak.crunchyroll.com/i/spire1/f0c009039dd9f8dff5907fff148adfca1587067000_full.jpg\" alt=\"JUMP FORCE\" width=\"640\" height=\"362\" /></p>\n" +
"<p class=\"p2\">&nbsp;</p>\n" +
"<p class=\"p1\">Switch owners will soon get to take part in the ultimate <em>Shonen Jump </em>rumble. Bandai Namco announced plans to bring <strong><em>Jump Force </em></strong>to <strong>Switch</strong> as <strong><em>Jump Force Deluxe Edition</em></strong>, with a release set for sometime this year. This version will include all of the original playable characters and those from Character Pass 1, and <strong>Character Pass 2 is also in the works </strong>for all versions, starting with <strong>Shoto Todoroki from </strong><span style=\"color: #ff9900;\"><a href=\"/my-hero-academia?utm_source=editorial_cr&amp;utm_medium=news&amp;utm_campaign=article_driven&amp;referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><strong><em>My Hero Academia</em></strong></span></a></span>.</p>\n" +
"<p class=\"p2\">&nbsp;</p>\n" +
"<p class=\"p1\">Other than Todoroki, Bandai Namco hinted that the four other Character Pass 2 characters will hail from <span style=\"color: #ff9900;\"><a href=\"/hunter-x-hunter?utm_source=editorial_cr&amp;utm_medium=news&amp;utm_campaign=article_driven&amp;referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>Hunter x Hunter</em></span></a></span>, <em>Yu Yu Hakusho</em>, <span style=\"color: #ff9900;\"><a href=\"/bleach?utm_source=editorial_cr&amp;utm_medium=news&amp;utm_campaign=article_driven&amp;referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>Bleach</em></span></a></span>, and <span style=\"color: #ff9900;\"><a href=\"/jojos-bizarre-adventure?utm_source=editorial_cr&amp;utm_medium=news&amp;utm_campaign=article_driven&amp;referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>JoJo's Bizarre Adventure</em></span></a></span>. Character Pass 2 will be priced at $17.99, and Todoroki launches this spring.<span class=\"Apple-converted-space\">&nbsp;</span></p>\n" +
"<p class=\"p2\">&nbsp;</p>\n" +
"<p class=\"p1\"><iframe style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://www.youtube.com/embed/At1qTj-LWCc\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe></p>\n" +
"<p class=\"p2\">&nbsp;</p>\n" +
"<p class=\"p1\">Character Pass 2 promo:</p>\n" +
"<p class=\"p2\">&nbsp;</p>\n" +
"<p class=\"p1\"><iframe style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://www.youtube.com/embed/CukwN6kV4R4\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe></p>\n" +
"<p class=\"p2\">&nbsp;</p>\n" +
"<p class=\"p1\"><a href=\"https://got.cr/PremiumTrial-NewsBanner4\"><img style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1587067041_full.png\" alt=\"\" width=\"640\" height=\"43\" /></a></p>\n" +
"<p class=\"p2\">&nbsp;</p>\n" +
"<p class=\"p1\">-------</p>\n" +
"<p class=\"p1\"><em>Joseph Luster is the Games and Web editor at </em><a href=\"http://www.otakuusamagazine.com/ME2/Default.asp\"><em>Otaku USA Magazine</em></a><em>. You can read his webcomic, </em><a href=\"http://subhumanzoids.com/comics/big-dumb-fighting-idiots/\">BIG DUMB FIGHTING IDIOTS</a><em> at </em><a href=\"http://subhumanzoids.com/\"><em>subhumanzoids</em></a><em>. Follow him on Twitter </em><a href=\"https://twitter.com/Moldilox\"><em>@Moldilox</em></a><em>.</em><span class=\"Apple-converted-space\">&nbsp;</span></p>";
final Markwon markwon = Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.usePlugin(HtmlPlugin.create())
.usePlugin(new IFrameHtmlPlugin())
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,44 @@
package io.noties.markwon.app.samples.html;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182115300",
title = "Html images",
description = "Usage of HTML images",
artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE},
tags = {Tags.image, Tags.rendering, Tags.html}
)
public class HtmlImageSample extends MarkwonTextViewSample {
@Override
public void render() {
// treat unclosed/void `img` tag as HTML inline
final String md = "" +
"## Try CommonMark\n" +
"\n" +
"Markwon IMG:\n" +
"\n" +
"![](https://upload.wikimedia.org/wikipedia/it/thumb/c/c5/GTA_2.JPG/220px-GTA_2.JPG)\n" +
"\n" +
"New lines...\n" +
"\n" +
"HTML IMG:\n" +
"\n" +
"<img src=\"https://upload.wikimedia.org/wikipedia/it/thumb/c/c5/GTA_2.JPG/220px-GTA_2.JPG\"></img>\n" +
"\n" +
"New lines\n\n";
final Markwon markwon = Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.usePlugin(HtmlPlugin.create())
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,86 @@
package io.noties.markwon.app.samples.html;
import android.text.style.AbsoluteSizeSpan;
import androidx.annotation.NonNull;
import java.util.Collection;
import java.util.Collections;
import java.util.Random;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import io.noties.markwon.html.TagHandler;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182114923",
title = "Random char size HTML tag",
description = "Implementation of a custom HTML tag handler " +
"that assigns each character a random size",
artifacts = MarkwonArtifact.HTML,
tags = {Tags.rendering, Tags.span, Tags.html}
)
public class HtmlRandomCharSize extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"<random-char-size>\n" +
"This message should have a jumpy feeling because of different sizes of characters\n" +
"</random-char-size>\n\n";
final Markwon markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
.addHandler(new RandomCharSize(new Random(42L), textView.getTextSize())));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class RandomCharSize extends TagHandler {
private final Random random;
private final float base;
RandomCharSize(@NonNull Random random, float base) {
this.random = random;
this.base = base;
}
@Override
public void handle(
@NonNull MarkwonVisitor visitor,
@NonNull MarkwonHtmlRenderer renderer,
@NonNull HtmlTag tag) {
final SpannableBuilder builder = visitor.builder();
// text content is already added, we should only apply spans
for (int i = tag.start(), end = tag.end(); i < end; i++) {
final int size = (int) (base * (random.nextFloat() + 0.5F) + 0.5F);
builder.setSpan(new AbsoluteSizeSpan(size, false), i, i + 1);
}
}
@NonNull
@Override
public Collection<String> supportedTags() {
return Collections.singleton("random-char-size");
}
}

View File

@ -0,0 +1,52 @@
package io.noties.markwon.app.samples.html.shared;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Image;
import java.util.Collection;
import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.tag.SimpleTagHandler;
import io.noties.markwon.image.ImageProps;
import io.noties.markwon.image.ImageSize;
public class IFrameHtmlPlugin extends AbstractMarkwonPlugin {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin ->
htmlPlugin.addHandler(new IFrameHtmlPlugin.EmbedTagHandler()));
}
private static class EmbedTagHandler extends SimpleTagHandler {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
final ImageSize imageSize = new ImageSize(
new ImageSize.Dimension(640, "px"),
new ImageSize.Dimension(480, "px")
);
ImageProps.IMAGE_SIZE.set(renderProps, imageSize);
ImageProps.DESTINATION.set(
renderProps,
"https://img1.ak.crunchyroll.com/i/spire2/d7b1d6bc7563224388ef5ffc04a967581589950464_full.jpg");
return configuration.spansFactory().require(Image.class)
.getSpans(configuration, renderProps);
}
@NonNull
@Override
public Collection<String> supportedTags() {
return Collections.singleton("iframe");
}
}
}

View File

@ -0,0 +1,43 @@
package io.noties.markwon.app.samples.image;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import io.noties.markwon.Markwon;
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.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182165828",
title = "Image error handler",
artifacts = MarkwonArtifact.IMAGE,
tags = Tags.image
)
public class ErrorImageSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"![error](https://github.com/dcurtis/markdown-mark/raw/master/png/______1664x1024-solid.png)";
final Markwon markwon = Markwon.builder(context)
// error handler additionally allows to log/inspect errors during image loading
.usePlugin(ImagesPlugin.create(plugin ->
plugin.errorHandler(new ImagesPlugin.ErrorHandler() {
@Nullable
@Override
public Drawable handleError(@NonNull String url, @NonNull Throwable throwable) {
return ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp);
}
})))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,32 @@
package io.noties.markwon.app.samples.image;
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.ImagesPlugin;
import io.noties.markwon.image.gif.GifMediaDecoder;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182162214",
title = "GIF image",
artifacts = MarkwonArtifact.IMAGE,
tags = {Tags.image, Tags.gif}
)
public class GifImageSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"![gif-image](https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif)";
final Markwon markwon = Markwon.builder(context)
// GIF is handled by default if library is used in the app
// .usePlugin(ImagesPlugin.create())
.usePlugin(ImagesPlugin.create(plugin ->
plugin.addMediaDecoder(GifMediaDecoder.create())))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,27 @@
package io.noties.markwon.app.samples.image;
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.glide.GlideImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182170112",
title = "Glide image",
artifacts = MarkwonArtifact.IMAGE_GLIDE,
tags = Tags.image
)
public class GlideImageSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "[![undefined](https://img.youtube.com/vi/gs1I8_m4AOM/0.jpg)](https://www.youtube.com/watch?v=gs1I8_m4AOM)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(GlideImagesPlugin.create(context))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,58 @@
package io.noties.markwon.app.samples.image;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.request.target.Target;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.R;
import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.glide.GlideImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182170241",
title = "Glide image with placeholder",
artifacts = MarkwonArtifact.IMAGE_GLIDE,
tags = Tags.image
)
public class GlidePlaceholderImageSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "[![undefined](https://img.youtube.com/vi/gs1I8_m4AOM/0.jpg)](https://www.youtube.com/watch?v=gs1I8_m4AOM)";
final Context context = this.context;
final Markwon markwon = Markwon.builder(context)
.usePlugin(GlideImagesPlugin.create(new GlideImagesPlugin.GlideStore() {
@NonNull
@Override
public RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) {
// final Drawable placeholder = ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp);
// placeholder.setBounds(0, 0, 100, 100);
return Glide.with(context)
.load(drawable.getDestination())
// .placeholder(ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp));
// .placeholder(placeholder);
.placeholder(R.drawable.ic_home_black_36dp);
}
@Override
public void cancel(@NonNull Target<?> target) {
Glide.with(context)
.clear(target);
}
}))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,28 @@
package io.noties.markwon.app.samples.image;
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.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182144659",
title = "Markdown image",
artifacts = MarkwonArtifact.IMAGE,
tags = Tags.image
)
public class ImageSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"![image](https://github.com/dcurtis/markdown-mark/raw/master/png/208x128-solid.png)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,45 @@
package io.noties.markwon.app.samples.image;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import io.noties.markwon.Markwon;
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.image.AsyncDrawable;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182165504",
title = "Image with placeholder",
artifacts = MarkwonArtifact.IMAGE,
tags = Tags.image
)
public class PlaceholderImageSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"![image](https://github.com/dcurtis/markdown-mark/raw/master/png/1664x1024-solid.png)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(ImagesPlugin.create(plugin ->
plugin.placeholderProvider(new ImagesPlugin.PlaceholderProvider() {
@Nullable
@Override
public Drawable providePlaceholder(@NonNull AsyncDrawable drawable) {
// by default drawable intrinsic size will be used
// otherwise bounds can be applied explicitly
return ContextCompat.getDrawable(context, R.drawable.ic_android_black_24dp);
}
})))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,37 @@
package io.noties.markwon.app.samples.image;
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.ImagesPlugin;
import io.noties.markwon.image.svg.SvgPictureMediaDecoder;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182161952",
title = "SVG image",
artifacts = MarkwonArtifact.IMAGE,
tags = {Tags.image, Tags.svg}
)
public class SvgImageSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"![svg-image](https://github.com/dcurtis/markdown-mark/raw/master/svg/markdown-mark-solid.svg)";
final Markwon markwon = Markwon.builder(context)
// SVG and GIF are automatically handled if required
// libraries are in path (specified in dependencies block)
// .usePlugin(ImagesPlugin.create())
// let's make it implicit
.usePlugin(ImagesPlugin.create(plugin ->
// there 2 svg media decoders:
// - regular `SvgMediaDecoder`
// - special one when SVG doesn't have width and height specified - `SvgPictureMediaDecoder`
plugin.addMediaDecoder(SvgPictureMediaDecoder.create())))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,77 @@
package io.noties.markwon.app.samples.inlineparsing;
import androidx.annotation.NonNull;
import org.commonmark.node.Block;
import org.commonmark.node.BlockQuote;
import org.commonmark.node.Heading;
import org.commonmark.node.HtmlBlock;
import org.commonmark.node.ListBlock;
import org.commonmark.node.ThematicBreak;
import org.commonmark.parser.InlineParserFactory;
import org.commonmark.parser.Parser;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
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.BackticksInlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182170607",
title = "Disable code inline parsing",
artifacts = MarkwonArtifact.INLINE_PARSER,
tags = {Tags.inline, Tags.parsing}
)
public class InlineParsingDisableCodeSample extends MarkwonTextViewSample {
@Override
public void render() {
// parses all as usual, but ignores code (inline and block)
final String md = "# Head!\n\n" +
"* one\n" +
"+ two\n\n" +
"and **bold** to `you`!\n\n" +
"> a quote _em_\n\n" +
"```java\n" +
"final int i = 0;\n" +
"```\n\n" +
"**Good day!**";
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
.excludeInlineProcessor(BackticksInlineProcessor.class)
.build();
// unfortunately there is no _exclude_ method for parser-builder
final Set<Class<? extends Block>> enabledBlocks = new HashSet<Class<? extends Block>>() {{
// IndentedCodeBlock.class and FencedCodeBlock.class are missing
// this is full list (including above) that can be passed to `enabledBlockTypes` method
addAll(Arrays.asList(
BlockQuote.class,
Heading.class,
HtmlBlock.class,
ThematicBreak.class,
ListBlock.class));
}};
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder
.inlineParserFactory(inlineParserFactory)
.enabledBlockTypes(enabledBlocks);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,55 @@
package io.noties.markwon.app.samples.inlineparsing;
import androidx.annotation.NonNull;
import org.commonmark.parser.InlineParserFactory;
import org.commonmark.parser.Parser;
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.CloseBracketInlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182170412",
title = "Links only inline parsing",
artifacts = MarkwonArtifact.INLINE_PARSER,
tags = {Tags.parsing, Tags.inline}
)
public class InlineParsingLinksOnlySample extends MarkwonTextViewSample {
@Override
public void render() {
// note that image is considered a link now
final String md = "**bold_bold-italic_** <u>html-u</u>, [link](#) ![alt](#image) `code`";
// create an inline-parser-factory that will _ONLY_ parse links
// this would mean:
// * no emphasises (strong and regular aka bold and italics),
// * no images,
// * no code,
// * no HTML entities (&amp;)
// * no HTML tags
// markdown blocks are still parsed
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilderNoDefaults()
.referencesEnabled(true)
.addInlineProcessor(new OpenBracketInlineProcessor())
.addInlineProcessor(new CloseBracketInlineProcessor())
.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);
}
}

View File

@ -0,0 +1,44 @@
package io.noties.markwon.app.samples.inlineparsing;
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.inlineparser.BackticksInlineProcessor;
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 = "202006182170823",
title = "Inline parsing with defaults",
artifacts = MarkwonArtifact.INLINE_PARSER,
tags = {Tags.inline, Tags.parsing}
)
public class InlineParsingNoDefaultsSample extends MarkwonTextViewSample {
@Override
public void render() {
// a plugin with NO defaults registered
final String md = "no [links](#) for **you** `code`!";
final Markwon markwon = Markwon.builder(context)
// pass `MarkwonInlineParser.factoryBuilderNoDefaults()` no disable all
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(MarkwonInlineParserPlugin.class, plugin -> {
plugin.factoryBuilder()
.addInlineProcessor(new BackticksInlineProcessor());
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,59 @@
package io.noties.markwon.app.samples.inlineparsing;
import androidx.annotation.NonNull;
import org.commonmark.node.Block;
import org.commonmark.node.HtmlBlock;
import org.commonmark.parser.Parser;
import java.util.Set;
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.CorePlugin;
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182171239",
title = "Inline parsing exclude HTML",
artifacts = MarkwonArtifact.INLINE_PARSER,
tags = {Tags.parsing, Tags.inline, Tags.block}
)
public class InlineParsingNoHtmlSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "# Html <b>disabled</b>\n\n" +
"<em>emphasis <strong>strong</strong>\n\n" +
"<p>paragraph <img src='hey.jpg' /></p>\n\n" +
"<test></test>\n\n" +
"<test>";
final Markwon markwon = Markwon.builder(context)
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(MarkwonInlineParserPlugin.class, plugin -> {
plugin.factoryBuilder()
.excludeInlineProcessor(HtmlInlineProcessor.class);
});
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
final Set<Class<? extends Block>> blocks = CorePlugin.enabledBlockTypes();
blocks.remove(HtmlBlock.class);
builder.enabledBlockTypes(blocks);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,44 @@
package io.noties.markwon.app.samples.inlineparsing;
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.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo(
id = "202006182170723",
title = "Inline parsing with defaults",
artifacts = MarkwonArtifact.INLINE_PARSER,
tags = {Tags.inline, Tags.parsing}
)
public class InlineParsingWithDefaultsSample extends MarkwonTextViewSample {
@Override
public void render() {
// a plugin with defaults registered
final String md = "no [links](#) for **you** `code`!";
final Markwon markwon = Markwon.builder(context)
.usePlugin(MarkwonInlineParserPlugin.create())
// the same as:
// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilder()))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(MarkwonInlineParserPlugin.class, plugin -> {
plugin.factoryBuilder()
.excludeInlineProcessor(OpenBracketInlineProcessor.class);
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,50 @@
package io.noties.markwon.app.samples.plugins;
import android.view.View;
import android.widget.ScrollView;
import org.jetbrains.annotations.NotNull;
import io.noties.markwon.Markwon;
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.app.samples.plugins.shared.AnchorHeadingPlugin;
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);
}
}

View File

@ -1,4 +1,4 @@
package io.noties.markwon.app.samples; package io.noties.markwon.app.samples.plugins;
import android.view.View; import android.view.View;
import android.widget.ScrollView; import android.widget.ScrollView;
@ -21,6 +21,7 @@ import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.R; import io.noties.markwon.app.R;
import io.noties.markwon.app.sample.Tags; import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.app.samples.plugins.shared.AnchorHeadingPlugin;
import io.noties.markwon.core.SimpleBlockNodeVisitor; import io.noties.markwon.core.SimpleBlockNodeVisitor;
import io.noties.markwon.sample.annotations.MarkwonArtifact; import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo; import io.noties.markwon.sample.annotations.MarkwonSampleInfo;

View File

@ -1,76 +1,32 @@
package io.noties.markwon.app.samples; package io.noties.markwon.app.samples.plugins.shared;
import android.text.Spannable; import android.text.Spannable;
import android.text.Spanned; import android.text.Spanned;
import android.view.View; import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.jetbrains.annotations.NotNull;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.LinkResolverDef; import io.noties.markwon.LinkResolverDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration; 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.core.spans.HeadingSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@MarkwonSampleInfo( public class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
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 { public interface ScrollTo {
void scrollTo(@NonNull TextView view, int top); void scrollTo(@NonNull TextView view, int top);
} }
private final ScrollTo scrollTo; private final AnchorHeadingPlugin.ScrollTo scrollTo;
AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) { public AnchorHeadingPlugin(@NonNull AnchorHeadingPlugin.ScrollTo scrollTo) {
this.scrollTo = scrollTo; this.scrollTo = scrollTo;
} }
@Override @Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver(new AnchorLinkResolver(scrollTo)); builder.linkResolver(new AnchorHeadingPlugin.AnchorLinkResolver(scrollTo));
} }
@Override @Override
@ -84,7 +40,7 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
final int end = spannable.getSpanEnd(span); final int end = spannable.getSpanEnd(span);
final int flags = spannable.getSpanFlags(span); final int flags = spannable.getSpanFlags(span);
spannable.setSpan( spannable.setSpan(
new AnchorSpan(createAnchor(spannable.subSequence(start, end))), new AnchorHeadingPlugin.AnchorSpan(createAnchor(spannable.subSequence(start, end))),
start, start,
end, end,
flags flags
@ -95,9 +51,9 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
private static class AnchorLinkResolver extends LinkResolverDef { private static class AnchorLinkResolver extends LinkResolverDef {
private final ScrollTo scrollTo; private final AnchorHeadingPlugin.ScrollTo scrollTo;
AnchorLinkResolver(@NonNull ScrollTo scrollTo) { AnchorLinkResolver(@NonNull AnchorHeadingPlugin.ScrollTo scrollTo) {
this.scrollTo = scrollTo; this.scrollTo = scrollTo;
} }
@ -106,10 +62,10 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
if (link.startsWith("#")) { if (link.startsWith("#")) {
final TextView textView = (TextView) view; final TextView textView = (TextView) view;
final Spanned spanned = (Spannable) textView.getText(); final Spanned spanned = (Spannable) textView.getText();
final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class); final AnchorHeadingPlugin.AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorHeadingPlugin.AnchorSpan.class);
if (spans != null) { if (spans != null) {
final String anchor = link.substring(1); final String anchor = link.substring(1);
for (AnchorSpan span : spans) { for (AnchorHeadingPlugin.AnchorSpan span : spans) {
if (anchor.equals(span.anchor)) { if (anchor.equals(span.anchor)) {
final int start = spanned.getSpanStart(span); final int start = spanned.getSpanStart(span);
final int line = textView.getLayout().getLineForOffset(start); final int line = textView.getLayout().getLineForOffset(start);
@ -139,4 +95,3 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
.toLowerCase(); .toLowerCase();
} }
} }

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.ViewGroup import android.view.ViewGroup
import io.noties.markwon.app.R import io.noties.markwon.app.R
import io.noties.markwon.app.utils.hidden
import kotlin.math.max import kotlin.math.max
class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) { class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) {
@ -61,6 +62,9 @@ class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, att
for (i in 0 until childCount) { for (i in 0 until childCount) {
val child = getChildAt(i) val child = getChildAt(i)
if (child.hidden) {
continue
}
// measure // measure
child.measure(childWidthSpec, childHeightSpec) child.measure(childWidthSpec, childHeightSpec)

View File

@ -73,6 +73,9 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
textField.setText("") textField.setText("")
looseFocus() looseFocus()
} }
isSaveEnabled = false
textField.isSaveEnabled = false
} }
fun search(text: String) { fun search(text: String) {

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"/>
</vector>

View File

@ -0,0 +1,4 @@
<vector android:height="36dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@ -0,0 +1,4 @@
<vector android:height="48dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15,9L9,9v6h6L15,9zM13,13h-2v-2h2v2zM21,11L21,9h-2L19,7c0,-1.1 -0.9,-2 -2,-2h-2L15,3h-2v2h-2L11,3L9,3v2L7,5c-1.1,0 -2,0.9 -2,2v2L3,9v2h2v2L3,13v2h2v2c0,1.1 0.9,2 2,2h2v2h2v-2h2v2h2v-2h2c1.1,0 2,-0.9 2,-2v-2h2v-2h-2v-2h2zM17,17L7,17L7,7h10v10z"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="64dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFF0000" android:pathData="M15.5,9.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
<path android:fillColor="#FFFF0000" android:pathData="M8.5,9.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
<path android:fillColor="#FFFF0000" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12,16c-1.48,0 -2.75,-0.81 -3.45,-2L6.88,14c0.8,2.05 2.79,3.5 5.12,3.5s4.32,-1.45 5.12,-3.5h-1.67c-0.7,1.19 -1.97,2 -3.45,2z"/>
</vector>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>

View File

@ -10,15 +10,22 @@
style="@style/AppBarContainer" style="@style/AppBarContainer"
android:orientation="horizontal"> android:orientation="horizontal">
<FrameLayout
android:layout_width="@dimen/app_bar_height"
android:layout_height="@dimen/app_bar_height">
<ImageView <ImageView
style="@style/AppBarIcon" style="@style/AppBarIcon"
android:src="@mipmap/ic_launcher" android:src="@drawable/ic_arrow_back_white_24dp"
android:visibility="gone"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</FrameLayout>
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="0px"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginEnd="@dimen/app_bar_height"> android:layout_weight="1">
<TextView <TextView
style="@style/AppBarTitle" style="@style/AppBarTitle"
@ -26,11 +33,32 @@
</FrameLayout> </FrameLayout>
<FrameLayout
android:layout_width="@dimen/app_bar_height"
android:layout_height="@dimen/app_bar_height">
<ImageView
android:id="@+id/app_bar_icon_readme"
style="@style/AppBarIcon"
android:src="@mipmap/ic_launcher"
android:visibility="visible"
tools:ignore="ContentDescription" />
</FrameLayout>
</LinearLayout> </LinearLayout>
<FrameLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<io.noties.markwon.app.widget.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:padding="@dimen/content_padding" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
@ -39,17 +67,8 @@
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:overScrollMode="never" android:overScrollMode="never"
android:paddingBottom="36dip" android:paddingBottom="36dip" />
tools:layout_marginTop="56dip" />
</LinearLayout>
<io.noties.markwon.app.widget.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="@color/search_bar_background_full"
android:padding="@dimen/content_padding" />
</FrameLayout>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#000" />