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",
|
||||
"id": "202006181171212",
|
||||
@ -136,13 +317,12 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample",
|
||||
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample",
|
||||
"id": "202006181162024",
|
||||
"title": "User mention and issue (via text)",
|
||||
"description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`",
|
||||
"artifacts": [
|
||||
"CORE",
|
||||
"INLINE_PARSER"
|
||||
"CORE"
|
||||
],
|
||||
"tags": [
|
||||
"parsing",
|
||||
@ -151,12 +331,13 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample",
|
||||
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample",
|
||||
"id": "202006181162024",
|
||||
"title": "User mention and issue (via text)",
|
||||
"description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`",
|
||||
"artifacts": [
|
||||
"CORE"
|
||||
"CORE",
|
||||
"INLINE_PARSER"
|
||||
],
|
||||
"tags": [
|
||||
"parsing",
|
||||
@ -177,7 +358,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"javaClassName": "io.noties.markwon.app.samples.TableOfContentsSample",
|
||||
"javaClassName": "io.noties.markwon.app.samples.plugins.TableOfContentsSample",
|
||||
"id": "202006181161226",
|
||||
"title": "Table of contents",
|
||||
"description": "Sample plugin that adds a table of contents header",
|
||||
@ -204,7 +385,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"javaClassName": "io.noties.markwon.app.samples.AnchorSample",
|
||||
"javaClassName": "io.noties.markwon.app.samples.plugins.AnchorSample",
|
||||
"id": "202006181130728",
|
||||
"title": "Anchor plugin",
|
||||
"description": "HTML-like anchor links plugin, which scrolls to clicked anchor",
|
||||
|
@ -2,6 +2,7 @@ package io.noties.markwon.app.readme
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@ -144,8 +145,12 @@ class ReadMeActivity : Activity() {
|
||||
data class Failure(val throwable: Throwable) : Result()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try {
|
||||
companion object {
|
||||
fun makeIntent(context: Context): Intent {
|
||||
return Intent(context, ReadMeActivity::class.java)
|
||||
}
|
||||
|
||||
private fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try {
|
||||
|
||||
if (data == null) {
|
||||
callback.invoke(Result.Success(loadReadMe(context)))
|
||||
|
@ -8,10 +8,10 @@ import android.widget.TextView
|
||||
import io.noties.adapt.Item
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.widget.FlowLayout
|
||||
import io.noties.markwon.app.utils.displayName
|
||||
import io.noties.markwon.app.utils.hidden
|
||||
import io.noties.markwon.app.utils.tagDisplayName
|
||||
import io.noties.markwon.app.widget.FlowLayout
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||
|
||||
|
@ -27,4 +27,8 @@ object Tags {
|
||||
const val textAddedListener = "text-added-listener"
|
||||
const val editor = "editor"
|
||||
const val span = "span"
|
||||
const val svg = "SVG"
|
||||
const val gif = "GIF"
|
||||
const val inline = "inline"
|
||||
const val html = "HTML"
|
||||
}
|
@ -1,11 +1,19 @@
|
||||
package io.noties.markwon.app.sample.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.core.spans.EmphasisSpan
|
||||
import io.noties.markwon.core.spans.StrongEmphasisSpan
|
||||
import java.util.ArrayList
|
||||
|
||||
abstract class MarkwonEditTextSample: MarkwonSample() {
|
||||
abstract class MarkwonEditTextSample : MarkwonSample() {
|
||||
|
||||
protected lateinit var context: Context
|
||||
protected lateinit var editText: EditText
|
||||
@ -16,8 +24,80 @@ abstract class MarkwonEditTextSample: MarkwonSample() {
|
||||
override fun onViewCreated(view: View) {
|
||||
context = view.context
|
||||
editText = view.findViewById(R.id.edit_text)
|
||||
initBottomBar(view)
|
||||
render()
|
||||
}
|
||||
|
||||
abstract fun render()
|
||||
|
||||
private fun initBottomBar(view: View) {
|
||||
// all except block-quote wraps if have selection, or inserts at current cursor position
|
||||
val bold: Button = view.findViewById(R.id.bold)
|
||||
val italic: Button = view.findViewById(R.id.italic)
|
||||
val strike: Button = view.findViewById(R.id.strike)
|
||||
val quote: Button = view.findViewById(R.id.quote)
|
||||
val code: Button = view.findViewById(R.id.code)
|
||||
|
||||
addSpan(bold, StrongEmphasisSpan())
|
||||
addSpan(italic, EmphasisSpan())
|
||||
addSpan(strike, StrikethroughSpan())
|
||||
|
||||
bold.setOnClickListener(InsertOrWrapClickListener(editText, "**"))
|
||||
italic.setOnClickListener(InsertOrWrapClickListener(editText, "_"))
|
||||
strike.setOnClickListener(InsertOrWrapClickListener(editText, "~~"))
|
||||
code.setOnClickListener(InsertOrWrapClickListener(editText, "`"))
|
||||
quote.setOnClickListener {
|
||||
val start = editText.selectionStart
|
||||
val end = editText.selectionEnd
|
||||
if (start < 0) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
if (start == end) {
|
||||
editText.text.insert(start, "> ")
|
||||
} else {
|
||||
// wrap the whole selected area in a quote
|
||||
val newLines: MutableList<Int> = ArrayList(3)
|
||||
newLines.add(start)
|
||||
val text = editText.text.subSequence(start, end).toString()
|
||||
var index = text.indexOf('\n')
|
||||
while (index != -1) {
|
||||
newLines.add(start + index + 1)
|
||||
index = text.indexOf('\n', index + 1)
|
||||
}
|
||||
for (i in newLines.indices.reversed()) {
|
||||
editText.text.insert(newLines[i], "> ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSpan(textView: TextView, vararg spans: Any) {
|
||||
val builder = SpannableStringBuilder(textView.text)
|
||||
val end = builder.length
|
||||
for (span in spans) {
|
||||
builder.setSpan(span, 0, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
textView.text = builder
|
||||
}
|
||||
|
||||
private class InsertOrWrapClickListener(
|
||||
private val editText: EditText,
|
||||
private val text: String
|
||||
) : View.OnClickListener {
|
||||
override fun onClick(v: View) {
|
||||
val start = editText.selectionStart
|
||||
val end = editText.selectionEnd
|
||||
if (start < 0) {
|
||||
return
|
||||
}
|
||||
if (start == end) {
|
||||
// insert at current position
|
||||
editText.text.insert(start, text)
|
||||
} else {
|
||||
editText.text.insert(end, text)
|
||||
editText.text.insert(start, text)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -19,12 +19,14 @@ import io.noties.debug.Debug
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.App
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.readme.ReadMeActivity
|
||||
import io.noties.markwon.app.sample.Sample
|
||||
import io.noties.markwon.app.sample.SampleItem
|
||||
import io.noties.markwon.app.sample.SampleManager
|
||||
import io.noties.markwon.app.sample.SampleSearch
|
||||
import io.noties.markwon.app.utils.Cancellable
|
||||
import io.noties.markwon.app.utils.displayName
|
||||
import io.noties.markwon.app.utils.hidden
|
||||
import io.noties.markwon.app.utils.onPreDraw
|
||||
import io.noties.markwon.app.utils.recyclerView
|
||||
import io.noties.markwon.app.utils.tagDisplayName
|
||||
@ -83,20 +85,21 @@ class SampleListFragment : Fragment() {
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.adapter = adapt
|
||||
|
||||
// additional padding for RecyclerView
|
||||
searchBar.onPreDraw {
|
||||
recyclerView.setPadding(
|
||||
recyclerView.paddingLeft,
|
||||
recyclerView.paddingTop + searchBar.height,
|
||||
recyclerView.paddingRight,
|
||||
recyclerView.paddingBottom
|
||||
)
|
||||
recyclerView.post {
|
||||
recyclerView.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
// // additional padding for RecyclerView
|
||||
// greatly complicates state restoration (items jump and a lot of times state cannot be
|
||||
// even restored (layout manager scrolls to top item and that's it)
|
||||
// searchBar.onPreDraw {
|
||||
// recyclerView.setPadding(
|
||||
// recyclerView.paddingLeft,
|
||||
// recyclerView.paddingTop + searchBar.height,
|
||||
// recyclerView.paddingRight,
|
||||
// recyclerView.paddingBottom
|
||||
// )
|
||||
// }
|
||||
|
||||
val state: State? = arguments?.getParcelable(STATE)
|
||||
Debug.i(state)
|
||||
|
||||
val state: State? = savedInstanceState?.getParcelable(STATE)
|
||||
pendingRecyclerScrollPosition = state?.recyclerScrollPosition
|
||||
if (state?.search != null) {
|
||||
searchBar.search(state.search)
|
||||
@ -106,6 +109,14 @@ class SampleListFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
||||
val state = State(
|
||||
search,
|
||||
adapt.recyclerView?.scrollPosition
|
||||
)
|
||||
Debug.i(state)
|
||||
arguments?.putParcelable(STATE, state)
|
||||
|
||||
val cancellable = this.cancellable
|
||||
if (cancellable != null && !cancellable.isCancelled) {
|
||||
cancellable.cancel()
|
||||
@ -114,24 +125,38 @@ class SampleListFragment : Fragment() {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
val state = State(
|
||||
search,
|
||||
adapt.recyclerView?.scrollPosition
|
||||
)
|
||||
outState.putParcelable(STATE, state)
|
||||
}
|
||||
// not called? yeah, whatever
|
||||
// override fun onSaveInstanceState(outState: Bundle) {
|
||||
// super.onSaveInstanceState(outState)
|
||||
//
|
||||
// val state = State(
|
||||
// search,
|
||||
// adapt.recyclerView?.scrollPosition
|
||||
// )
|
||||
// Debug.i(state)
|
||||
// outState.putParcelable(STATE, state)
|
||||
// }
|
||||
|
||||
private fun initAppBar(view: View) {
|
||||
val appBar = view.findViewById<View>(R.id.app_bar)
|
||||
|
||||
val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon)
|
||||
val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title)
|
||||
val appBarIconReadme: ImageView = appBar.findViewById(R.id.app_bar_icon_readme)
|
||||
|
||||
val isInitialScreen = type is Type.All
|
||||
|
||||
appBarIcon.hidden = isInitialScreen
|
||||
appBarIconReadme.hidden = !isInitialScreen
|
||||
|
||||
val type = this.type
|
||||
if (type is Type.All) {
|
||||
appBarIconReadme.setOnClickListener {
|
||||
context?.let {
|
||||
val intent = ReadMeActivity.makeIntent(it)
|
||||
it.startActivity(intent)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -162,14 +187,23 @@ class SampleListFragment : Fragment() {
|
||||
}
|
||||
adapt.setItems(items)
|
||||
|
||||
val recyclerView = adapt.recyclerView ?: return
|
||||
|
||||
val scrollPosition = pendingRecyclerScrollPosition
|
||||
|
||||
Debug.i(scrollPosition)
|
||||
Debug.trace()
|
||||
|
||||
if (scrollPosition != null) {
|
||||
pendingRecyclerScrollPosition = null
|
||||
val recyclerView = adapt.recyclerView ?: return
|
||||
recyclerView.onPreDraw {
|
||||
(recyclerView.layoutManager as? LinearLayoutManager)
|
||||
?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset)
|
||||
}
|
||||
} else {
|
||||
recyclerView.onPreDraw {
|
||||
recyclerView.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,6 +241,9 @@ class SampleListFragment : Fragment() {
|
||||
else -> SampleSearch.All(search)
|
||||
}
|
||||
|
||||
Debug.i(sampleSearch)
|
||||
Debug.trace()
|
||||
|
||||
// clear current
|
||||
cancellable?.let {
|
||||
if (!it.isCancelled) {
|
||||
@ -277,12 +314,21 @@ class SampleListFragment : Fragment() {
|
||||
|
||||
private val RecyclerView.scrollPosition: RecyclerScrollPosition?
|
||||
get() {
|
||||
val holder = findViewHolderForLayoutPosition(0) ?: return null
|
||||
val holder = findFirstVisibleViewHolder() ?: return null
|
||||
val position = holder.adapterPosition
|
||||
val offset = holder.itemView.top
|
||||
return RecyclerScrollPosition(position, offset)
|
||||
}
|
||||
|
||||
// because findViewHolderForLayoutPosition doesn't work :'(
|
||||
private fun RecyclerView.findFirstVisibleViewHolder(): RecyclerView.ViewHolder? {
|
||||
if (childCount > 0) {
|
||||
val child = getChildAt(0)
|
||||
return findContainingViewHolder(child)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private sealed class Type {
|
||||
class Artifact(val artifact: MarkwonArtifact) : Type()
|
||||
class Tag(val tag: String) : Type()
|
||||
|
@ -45,5 +45,7 @@ public class AdditionalSpacingSample extends MarkwonTextViewSample {
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class ReadMorePlugin extends AbstractMarkwonPlugin {
|
||||
// establish connections with all _dynamic_ content that your markdown supports,
|
||||
// like images, tables, latex, etc
|
||||
registry.require(ImagesPlugin.class);
|
||||
registry.require(TablePlugin.class);
|
||||
// registry.require(TablePlugin.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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.widget.ScrollView;
|
||||
@ -21,6 +21,7 @@ import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.R;
|
||||
import io.noties.markwon.app.sample.Tags;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.app.samples.plugins.shared.AnchorHeadingPlugin;
|
||||
import io.noties.markwon.core.SimpleBlockNodeVisitor;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
@ -1,76 +1,32 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
package io.noties.markwon.app.samples.plugins.shared;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.view.View;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.LinkResolverDef;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonConfiguration;
|
||||
import io.noties.markwon.app.R;
|
||||
import io.noties.markwon.app.sample.Tags;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.spans.HeadingSpan;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "202006181130728",
|
||||
title = "Anchor plugin",
|
||||
description = "HTML-like anchor links plugin, which scrolls to clicked anchor",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tags.links, Tags.anchor, Tags.plugin}
|
||||
)
|
||||
public class AnchorSample extends MarkwonTextViewSample {
|
||||
|
||||
private ScrollView scrollView;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NotNull View view) {
|
||||
scrollView = view.findViewById(R.id.scroll_view);
|
||||
super.onViewCreated(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
final String lorem = context.getString(R.string.lorem);
|
||||
final String md = "" +
|
||||
"Hello [there](#there)!\n\n\n" +
|
||||
lorem + "\n\n" +
|
||||
"# There!\n\n" +
|
||||
lorem;
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
|
||||
public class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
public interface ScrollTo {
|
||||
void scrollTo(@NonNull TextView view, int top);
|
||||
}
|
||||
|
||||
private final ScrollTo scrollTo;
|
||||
private final AnchorHeadingPlugin.ScrollTo scrollTo;
|
||||
|
||||
AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) {
|
||||
public AnchorHeadingPlugin(@NonNull AnchorHeadingPlugin.ScrollTo scrollTo) {
|
||||
this.scrollTo = scrollTo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.linkResolver(new AnchorLinkResolver(scrollTo));
|
||||
builder.linkResolver(new AnchorHeadingPlugin.AnchorLinkResolver(scrollTo));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -84,7 +40,7 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
|
||||
final int end = spannable.getSpanEnd(span);
|
||||
final int flags = spannable.getSpanFlags(span);
|
||||
spannable.setSpan(
|
||||
new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
|
||||
new AnchorHeadingPlugin.AnchorSpan(createAnchor(spannable.subSequence(start, end))),
|
||||
start,
|
||||
end,
|
||||
flags
|
||||
@ -95,9 +51,9 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
private static class AnchorLinkResolver extends LinkResolverDef {
|
||||
|
||||
private final ScrollTo scrollTo;
|
||||
private final AnchorHeadingPlugin.ScrollTo scrollTo;
|
||||
|
||||
AnchorLinkResolver(@NonNull ScrollTo scrollTo) {
|
||||
AnchorLinkResolver(@NonNull AnchorHeadingPlugin.ScrollTo scrollTo) {
|
||||
this.scrollTo = scrollTo;
|
||||
}
|
||||
|
||||
@ -106,10 +62,10 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
|
||||
if (link.startsWith("#")) {
|
||||
final TextView textView = (TextView) view;
|
||||
final Spanned spanned = (Spannable) textView.getText();
|
||||
final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class);
|
||||
final AnchorHeadingPlugin.AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorHeadingPlugin.AnchorSpan.class);
|
||||
if (spans != null) {
|
||||
final String anchor = link.substring(1);
|
||||
for (AnchorSpan span : spans) {
|
||||
for (AnchorHeadingPlugin.AnchorSpan span : spans) {
|
||||
if (anchor.equals(span.anchor)) {
|
||||
final int start = spanned.getSpanStart(span);
|
||||
final int line = textView.getLayout().getLineForOffset(start);
|
||||
@ -139,4 +95,3 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.utils.hidden
|
||||
import kotlin.math.max
|
||||
|
||||
class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) {
|
||||
@ -61,6 +62,9 @@ class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, att
|
||||
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
if (child.hidden) {
|
||||
continue
|
||||
}
|
||||
|
||||
// measure
|
||||
child.measure(childWidthSpec, childHeightSpec)
|
||||
|
@ -73,6 +73,9 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
|
||||
textField.setText("")
|
||||
looseFocus()
|
||||
}
|
||||
|
||||
isSaveEnabled = false
|
||||
textField.isSaveEnabled = false
|
||||
}
|
||||
|
||||
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"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="@dimen/app_bar_height"
|
||||
android:layout_height="@dimen/app_bar_height">
|
||||
|
||||
<ImageView
|
||||
style="@style/AppBarIcon"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:src="@drawable/ic_arrow_back_white_24dp"
|
||||
android:visibility="gone"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0px"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="@dimen/app_bar_height">
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
style="@style/AppBarTitle"
|
||||
@ -26,11 +33,32 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="@dimen/app_bar_height"
|
||||
android:layout_height="@dimen/app_bar_height">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/app_bar_icon_readme"
|
||||
style="@style/AppBarIcon"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:visibility="visible"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<io.noties.markwon.app.widget.SearchBar
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:padding="@dimen/content_padding" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
@ -39,17 +67,8 @@
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingBottom="36dip"
|
||||
tools:layout_marginTop="56dip" />
|
||||
android:paddingBottom="36dip" />
|
||||
|
||||
<io.noties.markwon.app.widget.SearchBar
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@color/search_bar_background_full"
|
||||
android:padding="@dimen/content_padding" />
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -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