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",
"id": "202006181171212",
@ -136,13 +317,12 @@
]
},
{
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample",
"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",
"INLINE_PARSER"
"CORE"
],
"tags": [
"parsing",
@ -151,12 +331,13 @@
]
},
{
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample",
"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"
"CORE",
"INLINE_PARSER"
],
"tags": [
"parsing",
@ -177,7 +358,7 @@
]
},
{
"javaClassName": "io.noties.markwon.app.samples.TableOfContentsSample",
"javaClassName": "io.noties.markwon.app.samples.plugins.TableOfContentsSample",
"id": "202006181161226",
"title": "Table of contents",
"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",
"title": "Anchor plugin",
"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.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
@ -144,8 +145,12 @@ class ReadMeActivity : Activity() {
data class Failure(val throwable: Throwable) : Result()
}
private companion object {
fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try {
companion object {
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) {
callback.invoke(Result.Success(loadReadMe(context)))

View File

@ -8,10 +8,10 @@ import android.widget.TextView
import io.noties.adapt.Item
import io.noties.markwon.Markwon
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.hidden
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.utils.NoCopySpannableFactory

View File

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

View File

@ -1,11 +1,19 @@
package io.noties.markwon.app.sample.ui
import android.content.Context
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.StrikethroughSpan
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
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() {
protected lateinit var context: Context
protected lateinit var editText: EditText
@ -16,8 +24,80 @@ abstract class MarkwonEditTextSample: MarkwonSample() {
override fun onViewCreated(view: View) {
context = view.context
editText = view.findViewById(R.id.edit_text)
initBottomBar(view)
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.app.App
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.SampleItem
import io.noties.markwon.app.sample.SampleManager
import io.noties.markwon.app.sample.SampleSearch
import io.noties.markwon.app.utils.Cancellable
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.recyclerView
import io.noties.markwon.app.utils.tagDisplayName
@ -83,20 +85,21 @@ class SampleListFragment : Fragment() {
recyclerView.setHasFixedSize(true)
recyclerView.adapter = adapt
// additional padding for RecyclerView
searchBar.onPreDraw {
recyclerView.setPadding(
recyclerView.paddingLeft,
recyclerView.paddingTop + searchBar.height,
recyclerView.paddingRight,
recyclerView.paddingBottom
)
recyclerView.post {
recyclerView.scrollToPosition(0)
}
}
// // additional padding for RecyclerView
// greatly complicates state restoration (items jump and a lot of times state cannot be
// even restored (layout manager scrolls to top item and that's it)
// searchBar.onPreDraw {
// recyclerView.setPadding(
// recyclerView.paddingLeft,
// recyclerView.paddingTop + searchBar.height,
// recyclerView.paddingRight,
// recyclerView.paddingBottom
// )
// }
val state: State? = arguments?.getParcelable(STATE)
Debug.i(state)
val state: State? = savedInstanceState?.getParcelable(STATE)
pendingRecyclerScrollPosition = state?.recyclerScrollPosition
if (state?.search != null) {
searchBar.search(state.search)
@ -106,6 +109,14 @@ class SampleListFragment : Fragment() {
}
override fun onDestroyView() {
val state = State(
search,
adapt.recyclerView?.scrollPosition
)
Debug.i(state)
arguments?.putParcelable(STATE, state)
val cancellable = this.cancellable
if (cancellable != null && !cancellable.isCancelled) {
cancellable.cancel()
@ -114,24 +125,38 @@ class SampleListFragment : Fragment() {
super.onDestroyView()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val state = State(
search,
adapt.recyclerView?.scrollPosition
)
outState.putParcelable(STATE, state)
}
// not called? yeah, whatever
// override fun onSaveInstanceState(outState: Bundle) {
// super.onSaveInstanceState(outState)
//
// val state = State(
// search,
// adapt.recyclerView?.scrollPosition
// )
// Debug.i(state)
// outState.putParcelable(STATE, state)
// }
private fun initAppBar(view: View) {
val appBar = view.findViewById<View>(R.id.app_bar)
val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon)
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
if (type is Type.All) {
appBarIconReadme.setOnClickListener {
context?.let {
val intent = ReadMeActivity.makeIntent(it)
it.startActivity(intent)
}
}
return
}
@ -162,14 +187,23 @@ class SampleListFragment : Fragment() {
}
adapt.setItems(items)
val recyclerView = adapt.recyclerView ?: return
val scrollPosition = pendingRecyclerScrollPosition
Debug.i(scrollPosition)
Debug.trace()
if (scrollPosition != null) {
pendingRecyclerScrollPosition = null
val recyclerView = adapt.recyclerView ?: return
recyclerView.onPreDraw {
(recyclerView.layoutManager as? LinearLayoutManager)
?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset)
}
} else {
recyclerView.onPreDraw {
recyclerView.scrollToPosition(0)
}
}
}
@ -207,6 +241,9 @@ class SampleListFragment : Fragment() {
else -> SampleSearch.All(search)
}
Debug.i(sampleSearch)
Debug.trace()
// clear current
cancellable?.let {
if (!it.isCancelled) {
@ -277,12 +314,21 @@ class SampleListFragment : Fragment() {
private val RecyclerView.scrollPosition: RecyclerScrollPosition?
get() {
val holder = findViewHolderForLayoutPosition(0) ?: return null
val holder = findFirstVisibleViewHolder() ?: return null
val position = holder.adapterPosition
val offset = holder.itemView.top
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 {
class Artifact(val artifact: MarkwonArtifact) : Type()
class Tag(val tag: String) : Type()

View File

@ -45,5 +45,7 @@ public class AdditionalSpacingSample extends MarkwonTextViewSample {
}
})
.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,
// like images, tables, latex, etc
registry.require(ImagesPlugin.class);
registry.require(TablePlugin.class);
// registry.require(TablePlugin.class);
}
@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.widget.ScrollView;
@ -21,6 +21,7 @@ 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.app.samples.plugins.shared.AnchorHeadingPlugin;
import io.noties.markwon.core.SimpleBlockNodeVisitor;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
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.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 class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
public interface ScrollTo {
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;
}
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver(new AnchorLinkResolver(scrollTo));
builder.linkResolver(new AnchorHeadingPlugin.AnchorLinkResolver(scrollTo));
}
@Override
@ -84,7 +40,7 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
final int end = spannable.getSpanEnd(span);
final int flags = spannable.getSpanFlags(span);
spannable.setSpan(
new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
new AnchorHeadingPlugin.AnchorSpan(createAnchor(spannable.subSequence(start, end))),
start,
end,
flags
@ -95,9 +51,9 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
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;
}
@ -106,10 +62,10 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
if (link.startsWith("#")) {
final TextView textView = (TextView) view;
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) {
final String anchor = link.substring(1);
for (AnchorSpan span : spans) {
for (AnchorHeadingPlugin.AnchorSpan span : spans) {
if (anchor.equals(span.anchor)) {
final int start = spanned.getSpanStart(span);
final int line = textView.getLayout().getLineForOffset(start);
@ -139,4 +95,3 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
.toLowerCase();
}
}

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import io.noties.markwon.app.R
import io.noties.markwon.app.utils.hidden
import kotlin.math.max
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) {
val child = getChildAt(i)
if (child.hidden) {
continue
}
// measure
child.measure(childWidthSpec, childHeightSpec)

View File

@ -73,6 +73,9 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
textField.setText("")
looseFocus()
}
isSaveEnabled = false
textField.isSaveEnabled = false
}
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"
android:orientation="horizontal">
<ImageView
style="@style/AppBarIcon"
android:src="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
<FrameLayout
android:layout_width="@dimen/app_bar_height"
android:layout_height="@dimen/app_bar_height">
<ImageView
style="@style/AppBarIcon"
android:src="@drawable/ic_arrow_back_white_24dp"
android:visibility="gone"
tools:ignore="ContentDescription" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_width="0px"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/app_bar_height">
android:layout_weight="1">
<TextView
style="@style/AppBarTitle"
@ -26,11 +33,32 @@
</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>
<FrameLayout
<LinearLayout
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
android:id="@+id/recycler_view"
@ -39,17 +67,8 @@
android:clipChildren="false"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingBottom="36dip"
tools:layout_marginTop="56dip" />
android:paddingBottom="36dip" />
<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" />