Sample app, moving more samples
This commit is contained in:
parent
bc58790704
commit
0b1544feae
@ -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",
|
||||||
|
@ -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)))
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
}
|
}
|
@ -1,11 +1,19 @@
|
|||||||
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() {
|
||||||
|
|
||||||
protected lateinit var context: Context
|
protected lateinit var context: Context
|
||||||
protected lateinit var editText: EditText
|
protected lateinit var editText: EditText
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
|
@ -45,5 +45,7 @@ public class AdditionalSpacingSample extends MarkwonTextViewSample {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -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> (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> </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> </p>\n" +
|
||||||
|
" <p> </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> </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> </p>\n" +
|
||||||
|
" <hr>\n" +
|
||||||
|
" <p> </p>\n" +
|
||||||
|
" <p>Source: RIAJ press release</p>\n" +
|
||||||
|
" <p> </p>\n" +
|
||||||
|
" <p><em>©SACRA MUSIC</em></p>\n" +
|
||||||
|
" <p> </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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" +
|
||||||
|
"\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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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("<", "<")
|
||||||
|
.replaceAll(">", ">");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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\"> </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&utm_medium=news&utm_campaign=article_driven&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\"> </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&utm_medium=news&utm_campaign=article_driven&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&utm_medium=news&utm_campaign=article_driven&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&utm_medium=news&utm_campaign=article_driven&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\"> </span></p>\n" +
|
||||||
|
"<p class=\"p2\"> </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\"> </p>\n" +
|
||||||
|
"<p class=\"p1\">Character Pass 2 promo:</p>\n" +
|
||||||
|
"<p class=\"p2\"> </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\"> </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\"> </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\"> </span></p>";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.usePlugin(HtmlPlugin.create())
|
||||||
|
.usePlugin(new IFrameHtmlPlugin())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" +
|
||||||
|
"\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);
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "[](https://www.youtube.com/watch?v=gs1I8_m4AOM)";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(GlideImagesPlugin.create(context))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "[](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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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](#)  `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 (&)
|
||||||
|
// * 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
4
app-sample/src/main/res/drawable/ic_home_black_36dp.xml
Normal file
4
app-sample/src/main/res/drawable/ic_home_black_36dp.xml
Normal 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>
|
@ -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>
|
@ -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>
|
12
app-sample/src/main/res/layout/activity_html_details.xml
Normal file
12
app-sample/src/main/res/layout/activity_html_details.xml
Normal 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>
|
@ -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" />
|
|
||||||
|
|
||||||
<io.noties.markwon.app.widget.SearchBar
|
</LinearLayout>
|
||||||
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>
|
@ -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" />
|
Loading…
x
Reference in New Issue
Block a user