Sample app, moving more samples
This commit is contained in:
		
							parent
							
								
									bc58790704
								
							
						
					
					
						commit
						0b1544feae
					
				| @ -1,4 +1,185 @@ | |||||||
| [ | [ | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.image.ErrorImageSample", | ||||||
|  |     "id": "202006182165828", | ||||||
|  |     "title": "Image error handler", | ||||||
|  |     "description": "", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "image" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.image.PlaceholderImageSample", | ||||||
|  |     "id": "202006182165504", | ||||||
|  |     "title": "Image with placeholder", | ||||||
|  |     "description": "", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "image" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.image.GifImageSample", | ||||||
|  |     "id": "202006182162214", | ||||||
|  |     "title": "GIF image", | ||||||
|  |     "description": "", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "GIF", | ||||||
|  |       "image" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.image.SvgImageSample", | ||||||
|  |     "id": "202006182161952", | ||||||
|  |     "title": "SVG image", | ||||||
|  |     "description": "", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "SVG", | ||||||
|  |       "image" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.image.ImageSample", | ||||||
|  |     "id": "202006182144659", | ||||||
|  |     "title": "Markdown image", | ||||||
|  |     "description": "", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "image" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.html.HtmlDetailsSample", | ||||||
|  |     "id": "202006182120752", | ||||||
|  |     "title": "Details HTML tag", | ||||||
|  |     "description": "Handling of `details` HTML tag", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "HTML", | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "image", | ||||||
|  |       "rendering" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.html.HtmlCenterTagSample", | ||||||
|  |     "id": "202006182120101", | ||||||
|  |     "title": "Center HTML tag", | ||||||
|  |     "description": "Handling of `center` HTML tag", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "HTML", | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "rendering" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.html.HtmlEmptyTagReplacementSample", | ||||||
|  |     "id": "202006182115725", | ||||||
|  |     "title": "HTML empty tag replacement", | ||||||
|  |     "description": "Render custom content when HTML tag contents is empty, in case of self-closed HTML tags or tags without content (closed right after opened)", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "HTML" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "rendering" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.html.HtmlIFrameSample", | ||||||
|  |     "id": "202006182115521", | ||||||
|  |     "title": "IFrame HTML tag", | ||||||
|  |     "description": "Handling of `iframe` HTML tag", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "HTML", | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "image", | ||||||
|  |       "rendering" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.html.HtmlImageSample", | ||||||
|  |     "id": "202006182115300", | ||||||
|  |     "title": "Html images", | ||||||
|  |     "description": "Usage of HTML images", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "HTML", | ||||||
|  |       "IMAGE" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "image", | ||||||
|  |       "rendering" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.html.HtmlEnhanceSample", | ||||||
|  |     "id": "202006182115103", | ||||||
|  |     "title": "Enhance custom HTML tag", | ||||||
|  |     "description": "Custom HTML tag implementation that _enhances_ a part of text given start and end indices", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "HTML" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "rendering", | ||||||
|  |       "span" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.html.HtmlRandomCharSize", | ||||||
|  |     "id": "202006182114923", | ||||||
|  |     "title": "Random char size HTML tag", | ||||||
|  |     "description": "Implementation of a custom HTML tag handler that assigns each character a random size", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "HTML" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "rendering", | ||||||
|  |       "span" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.html.HtmlAlignSample", | ||||||
|  |     "id": "202006182114630", | ||||||
|  |     "title": "Align HTML tag", | ||||||
|  |     "description": "Implement custom HTML tag handling", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "HTML" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "rendering", | ||||||
|  |       "span" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "javaClassName": "io.noties.markwon.app.samples.editor.EditorHeadingSample", | ||||||
|  |     "id": "202006182113954", | ||||||
|  |     "title": "Heading edit handler", | ||||||
|  |     "description": "Handling of heading node in editor", | ||||||
|  |     "artifacts": [ | ||||||
|  |       "EDITOR", | ||||||
|  |       "INLINE_PARSER" | ||||||
|  |     ], | ||||||
|  |     "tags": [ | ||||||
|  |       "editor" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     "javaClassName": "io.noties.markwon.app.samples.NoParsingSample", |     "javaClassName": "io.noties.markwon.app.samples.NoParsingSample", | ||||||
|     "id": "202006181171212", |     "id": "202006181171212", | ||||||
| @ -136,13 +317,12 @@ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample", |     "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample", | ||||||
|     "id": "202006181162024", |     "id": "202006181162024", | ||||||
|     "title": "User mention and issue (via text)", |     "title": "User mention and issue (via text)", | ||||||
|     "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", |     "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", | ||||||
|     "artifacts": [ |     "artifacts": [ | ||||||
|       "CORE", |       "CORE" | ||||||
|       "INLINE_PARSER" |  | ||||||
|     ], |     ], | ||||||
|     "tags": [ |     "tags": [ | ||||||
|       "parsing", |       "parsing", | ||||||
| @ -151,12 +331,13 @@ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample", |     "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample", | ||||||
|     "id": "202006181162024", |     "id": "202006181162024", | ||||||
|     "title": "User mention and issue (via text)", |     "title": "User mention and issue (via text)", | ||||||
|     "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", |     "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", | ||||||
|     "artifacts": [ |     "artifacts": [ | ||||||
|       "CORE" |       "CORE", | ||||||
|  |       "INLINE_PARSER" | ||||||
|     ], |     ], | ||||||
|     "tags": [ |     "tags": [ | ||||||
|       "parsing", |       "parsing", | ||||||
| @ -177,7 +358,7 @@ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     "javaClassName": "io.noties.markwon.app.samples.TableOfContentsSample", |     "javaClassName": "io.noties.markwon.app.samples.plugins.TableOfContentsSample", | ||||||
|     "id": "202006181161226", |     "id": "202006181161226", | ||||||
|     "title": "Table of contents", |     "title": "Table of contents", | ||||||
|     "description": "Sample plugin that adds a table of contents header", |     "description": "Sample plugin that adds a table of contents header", | ||||||
| @ -204,7 +385,7 @@ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     "javaClassName": "io.noties.markwon.app.samples.AnchorSample", |     "javaClassName": "io.noties.markwon.app.samples.plugins.AnchorSample", | ||||||
|     "id": "202006181130728", |     "id": "202006181130728", | ||||||
|     "title": "Anchor plugin", |     "title": "Anchor plugin", | ||||||
|     "description": "HTML-like anchor links plugin, which scrolls to clicked anchor", |     "description": "HTML-like anchor links plugin, which scrolls to clicked anchor", | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package io.noties.markwon.app.readme | |||||||
| 
 | 
 | ||||||
| import android.app.Activity | import android.app.Activity | ||||||
| import android.content.Context | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
| import android.net.Uri | import android.net.Uri | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.view.View | import android.view.View | ||||||
| @ -144,8 +145,12 @@ class ReadMeActivity : Activity() { | |||||||
|         data class Failure(val throwable: Throwable) : Result() |         data class Failure(val throwable: Throwable) : Result() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private companion object { |     companion object { | ||||||
|         fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try { |         fun makeIntent(context: Context): Intent { | ||||||
|  |             return Intent(context, ReadMeActivity::class.java) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try { | ||||||
| 
 | 
 | ||||||
|             if (data == null) { |             if (data == null) { | ||||||
|                 callback.invoke(Result.Success(loadReadMe(context))) |                 callback.invoke(Result.Success(loadReadMe(context))) | ||||||
|  | |||||||
| @ -8,10 +8,10 @@ import android.widget.TextView | |||||||
| import io.noties.adapt.Item | import io.noties.adapt.Item | ||||||
| import io.noties.markwon.Markwon | import io.noties.markwon.Markwon | ||||||
| import io.noties.markwon.app.R | import io.noties.markwon.app.R | ||||||
| import io.noties.markwon.app.widget.FlowLayout |  | ||||||
| import io.noties.markwon.app.utils.displayName | import io.noties.markwon.app.utils.displayName | ||||||
| import io.noties.markwon.app.utils.hidden | import io.noties.markwon.app.utils.hidden | ||||||
| import io.noties.markwon.app.utils.tagDisplayName | import io.noties.markwon.app.utils.tagDisplayName | ||||||
|  | import io.noties.markwon.app.widget.FlowLayout | ||||||
| import io.noties.markwon.sample.annotations.MarkwonArtifact | import io.noties.markwon.sample.annotations.MarkwonArtifact | ||||||
| import io.noties.markwon.utils.NoCopySpannableFactory | import io.noties.markwon.utils.NoCopySpannableFactory | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -27,4 +27,8 @@ object Tags { | |||||||
|     const val textAddedListener = "text-added-listener" |     const val textAddedListener = "text-added-listener" | ||||||
|     const val editor = "editor" |     const val editor = "editor" | ||||||
|     const val span = "span" |     const val span = "span" | ||||||
|  |     const val svg = "SVG" | ||||||
|  |     const val gif = "GIF" | ||||||
|  |     const val inline = "inline" | ||||||
|  |     const val html = "HTML" | ||||||
| } | } | ||||||
| @ -1,11 +1,19 @@ | |||||||
| package io.noties.markwon.app.sample.ui | package io.noties.markwon.app.sample.ui | ||||||
| 
 | 
 | ||||||
| import android.content.Context | import android.content.Context | ||||||
|  | import android.text.SpannableStringBuilder | ||||||
|  | import android.text.Spanned | ||||||
|  | import android.text.style.StrikethroughSpan | ||||||
| import android.view.View | import android.view.View | ||||||
|  | import android.widget.Button | ||||||
| import android.widget.EditText | import android.widget.EditText | ||||||
|  | import android.widget.TextView | ||||||
| import io.noties.markwon.app.R | import io.noties.markwon.app.R | ||||||
|  | import io.noties.markwon.core.spans.EmphasisSpan | ||||||
|  | import io.noties.markwon.core.spans.StrongEmphasisSpan | ||||||
|  | import java.util.ArrayList | ||||||
| 
 | 
 | ||||||
| abstract class MarkwonEditTextSample: MarkwonSample() { | abstract class MarkwonEditTextSample : MarkwonSample() { | ||||||
| 
 | 
 | ||||||
|     protected lateinit var context: Context |     protected lateinit var context: Context | ||||||
|     protected lateinit var editText: EditText |     protected lateinit var editText: EditText | ||||||
| @ -16,8 +24,80 @@ abstract class MarkwonEditTextSample: MarkwonSample() { | |||||||
|     override fun onViewCreated(view: View) { |     override fun onViewCreated(view: View) { | ||||||
|         context = view.context |         context = view.context | ||||||
|         editText = view.findViewById(R.id.edit_text) |         editText = view.findViewById(R.id.edit_text) | ||||||
|  |         initBottomBar(view) | ||||||
|         render() |         render() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     abstract fun render() |     abstract fun render() | ||||||
|  | 
 | ||||||
|  |     private fun initBottomBar(view: View) { | ||||||
|  |         // all except block-quote wraps if have selection, or inserts at current cursor position | ||||||
|  |         val bold: Button = view.findViewById(R.id.bold) | ||||||
|  |         val italic: Button = view.findViewById(R.id.italic) | ||||||
|  |         val strike: Button = view.findViewById(R.id.strike) | ||||||
|  |         val quote: Button = view.findViewById(R.id.quote) | ||||||
|  |         val code: Button = view.findViewById(R.id.code) | ||||||
|  | 
 | ||||||
|  |         addSpan(bold, StrongEmphasisSpan()) | ||||||
|  |         addSpan(italic, EmphasisSpan()) | ||||||
|  |         addSpan(strike, StrikethroughSpan()) | ||||||
|  | 
 | ||||||
|  |         bold.setOnClickListener(InsertOrWrapClickListener(editText, "**")) | ||||||
|  |         italic.setOnClickListener(InsertOrWrapClickListener(editText, "_")) | ||||||
|  |         strike.setOnClickListener(InsertOrWrapClickListener(editText, "~~")) | ||||||
|  |         code.setOnClickListener(InsertOrWrapClickListener(editText, "`")) | ||||||
|  |         quote.setOnClickListener { | ||||||
|  |             val start = editText.selectionStart | ||||||
|  |             val end = editText.selectionEnd | ||||||
|  |             if (start < 0) { | ||||||
|  |                 return@setOnClickListener | ||||||
|  |             } | ||||||
|  |             if (start == end) { | ||||||
|  |                 editText.text.insert(start, "> ") | ||||||
|  |             } else { | ||||||
|  |                 // wrap the whole selected area in a quote | ||||||
|  |                 val newLines: MutableList<Int> = ArrayList(3) | ||||||
|  |                 newLines.add(start) | ||||||
|  |                 val text = editText.text.subSequence(start, end).toString() | ||||||
|  |                 var index = text.indexOf('\n') | ||||||
|  |                 while (index != -1) { | ||||||
|  |                     newLines.add(start + index + 1) | ||||||
|  |                     index = text.indexOf('\n', index + 1) | ||||||
|  |                 } | ||||||
|  |                 for (i in newLines.indices.reversed()) { | ||||||
|  |                     editText.text.insert(newLines[i], "> ") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun addSpan(textView: TextView, vararg spans: Any) { | ||||||
|  |         val builder = SpannableStringBuilder(textView.text) | ||||||
|  |         val end = builder.length | ||||||
|  |         for (span in spans) { | ||||||
|  |             builder.setSpan(span, 0, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) | ||||||
|  |         } | ||||||
|  |         textView.text = builder | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private class InsertOrWrapClickListener( | ||||||
|  |             private val editText: EditText, | ||||||
|  |             private val text: String | ||||||
|  |     ) : View.OnClickListener { | ||||||
|  |         override fun onClick(v: View) { | ||||||
|  |             val start = editText.selectionStart | ||||||
|  |             val end = editText.selectionEnd | ||||||
|  |             if (start < 0) { | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             if (start == end) { | ||||||
|  |                 // insert at current position | ||||||
|  |                 editText.text.insert(start, text) | ||||||
|  |             } else { | ||||||
|  |                 editText.text.insert(end, text) | ||||||
|  |                 editText.text.insert(start, text) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
| } | } | ||||||
| @ -19,12 +19,14 @@ import io.noties.debug.Debug | |||||||
| import io.noties.markwon.Markwon | import io.noties.markwon.Markwon | ||||||
| import io.noties.markwon.app.App | import io.noties.markwon.app.App | ||||||
| import io.noties.markwon.app.R | import io.noties.markwon.app.R | ||||||
|  | import io.noties.markwon.app.readme.ReadMeActivity | ||||||
| import io.noties.markwon.app.sample.Sample | import io.noties.markwon.app.sample.Sample | ||||||
| import io.noties.markwon.app.sample.SampleItem | import io.noties.markwon.app.sample.SampleItem | ||||||
| import io.noties.markwon.app.sample.SampleManager | import io.noties.markwon.app.sample.SampleManager | ||||||
| import io.noties.markwon.app.sample.SampleSearch | import io.noties.markwon.app.sample.SampleSearch | ||||||
| import io.noties.markwon.app.utils.Cancellable | import io.noties.markwon.app.utils.Cancellable | ||||||
| import io.noties.markwon.app.utils.displayName | import io.noties.markwon.app.utils.displayName | ||||||
|  | import io.noties.markwon.app.utils.hidden | ||||||
| import io.noties.markwon.app.utils.onPreDraw | import io.noties.markwon.app.utils.onPreDraw | ||||||
| import io.noties.markwon.app.utils.recyclerView | import io.noties.markwon.app.utils.recyclerView | ||||||
| import io.noties.markwon.app.utils.tagDisplayName | import io.noties.markwon.app.utils.tagDisplayName | ||||||
| @ -83,20 +85,21 @@ class SampleListFragment : Fragment() { | |||||||
|         recyclerView.setHasFixedSize(true) |         recyclerView.setHasFixedSize(true) | ||||||
|         recyclerView.adapter = adapt |         recyclerView.adapter = adapt | ||||||
| 
 | 
 | ||||||
|         // additional padding for RecyclerView | //        // additional padding for RecyclerView | ||||||
|         searchBar.onPreDraw { |         // greatly complicates state restoration (items jump and a lot of times state cannot be | ||||||
|             recyclerView.setPadding( |         //  even restored (layout manager scrolls to top item and that's it) | ||||||
|                     recyclerView.paddingLeft, | //        searchBar.onPreDraw { | ||||||
|                     recyclerView.paddingTop + searchBar.height, | //            recyclerView.setPadding( | ||||||
|                     recyclerView.paddingRight, | //                    recyclerView.paddingLeft, | ||||||
|                     recyclerView.paddingBottom | //                    recyclerView.paddingTop + searchBar.height, | ||||||
|             ) | //                    recyclerView.paddingRight, | ||||||
|             recyclerView.post { | //                    recyclerView.paddingBottom | ||||||
|                 recyclerView.scrollToPosition(0) | //            ) | ||||||
|             } | //        } | ||||||
|         } | 
 | ||||||
|  |         val state: State? = arguments?.getParcelable(STATE) | ||||||
|  |         Debug.i(state) | ||||||
| 
 | 
 | ||||||
|         val state: State? = savedInstanceState?.getParcelable(STATE) |  | ||||||
|         pendingRecyclerScrollPosition = state?.recyclerScrollPosition |         pendingRecyclerScrollPosition = state?.recyclerScrollPosition | ||||||
|         if (state?.search != null) { |         if (state?.search != null) { | ||||||
|             searchBar.search(state.search) |             searchBar.search(state.search) | ||||||
| @ -106,6 +109,14 @@ class SampleListFragment : Fragment() { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onDestroyView() { |     override fun onDestroyView() { | ||||||
|  | 
 | ||||||
|  |         val state = State( | ||||||
|  |                 search, | ||||||
|  |                 adapt.recyclerView?.scrollPosition | ||||||
|  |         ) | ||||||
|  |         Debug.i(state) | ||||||
|  |         arguments?.putParcelable(STATE, state) | ||||||
|  | 
 | ||||||
|         val cancellable = this.cancellable |         val cancellable = this.cancellable | ||||||
|         if (cancellable != null && !cancellable.isCancelled) { |         if (cancellable != null && !cancellable.isCancelled) { | ||||||
|             cancellable.cancel() |             cancellable.cancel() | ||||||
| @ -114,24 +125,38 @@ class SampleListFragment : Fragment() { | |||||||
|         super.onDestroyView() |         super.onDestroyView() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onSaveInstanceState(outState: Bundle) { |     // not called? yeah, whatever | ||||||
|         super.onSaveInstanceState(outState) | //    override fun onSaveInstanceState(outState: Bundle) { | ||||||
| 
 | //        super.onSaveInstanceState(outState) | ||||||
|         val state = State( | // | ||||||
|                 search, | //        val state = State( | ||||||
|                 adapt.recyclerView?.scrollPosition | //                search, | ||||||
|         ) | //                adapt.recyclerView?.scrollPosition | ||||||
|         outState.putParcelable(STATE, state) | //        ) | ||||||
|     } | //        Debug.i(state) | ||||||
|  | //        outState.putParcelable(STATE, state) | ||||||
|  | //    } | ||||||
| 
 | 
 | ||||||
|     private fun initAppBar(view: View) { |     private fun initAppBar(view: View) { | ||||||
|         val appBar = view.findViewById<View>(R.id.app_bar) |         val appBar = view.findViewById<View>(R.id.app_bar) | ||||||
| 
 | 
 | ||||||
|         val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon) |         val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon) | ||||||
|         val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title) |         val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title) | ||||||
|  |         val appBarIconReadme: ImageView = appBar.findViewById(R.id.app_bar_icon_readme) | ||||||
|  | 
 | ||||||
|  |         val isInitialScreen = type is Type.All | ||||||
|  | 
 | ||||||
|  |         appBarIcon.hidden = isInitialScreen | ||||||
|  |         appBarIconReadme.hidden = !isInitialScreen | ||||||
| 
 | 
 | ||||||
|         val type = this.type |         val type = this.type | ||||||
|         if (type is Type.All) { |         if (type is Type.All) { | ||||||
|  |             appBarIconReadme.setOnClickListener { | ||||||
|  |                 context?.let { | ||||||
|  |                     val intent = ReadMeActivity.makeIntent(it) | ||||||
|  |                     it.startActivity(intent) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -162,14 +187,23 @@ class SampleListFragment : Fragment() { | |||||||
|         } |         } | ||||||
|         adapt.setItems(items) |         adapt.setItems(items) | ||||||
| 
 | 
 | ||||||
|  |         val recyclerView = adapt.recyclerView ?: return | ||||||
|  | 
 | ||||||
|         val scrollPosition = pendingRecyclerScrollPosition |         val scrollPosition = pendingRecyclerScrollPosition | ||||||
|  | 
 | ||||||
|  |         Debug.i(scrollPosition) | ||||||
|  |         Debug.trace() | ||||||
|  | 
 | ||||||
|         if (scrollPosition != null) { |         if (scrollPosition != null) { | ||||||
|             pendingRecyclerScrollPosition = null |             pendingRecyclerScrollPosition = null | ||||||
|             val recyclerView = adapt.recyclerView ?: return |  | ||||||
|             recyclerView.onPreDraw { |             recyclerView.onPreDraw { | ||||||
|                 (recyclerView.layoutManager as? LinearLayoutManager) |                 (recyclerView.layoutManager as? LinearLayoutManager) | ||||||
|                         ?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset) |                         ?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset) | ||||||
|             } |             } | ||||||
|  |         } else { | ||||||
|  |             recyclerView.onPreDraw { | ||||||
|  |                 recyclerView.scrollToPosition(0) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -207,6 +241,9 @@ class SampleListFragment : Fragment() { | |||||||
|             else -> SampleSearch.All(search) |             else -> SampleSearch.All(search) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         Debug.i(sampleSearch) | ||||||
|  |         Debug.trace() | ||||||
|  | 
 | ||||||
|         // clear current |         // clear current | ||||||
|         cancellable?.let { |         cancellable?.let { | ||||||
|             if (!it.isCancelled) { |             if (!it.isCancelled) { | ||||||
| @ -277,12 +314,21 @@ class SampleListFragment : Fragment() { | |||||||
| 
 | 
 | ||||||
|     private val RecyclerView.scrollPosition: RecyclerScrollPosition? |     private val RecyclerView.scrollPosition: RecyclerScrollPosition? | ||||||
|         get() { |         get() { | ||||||
|             val holder = findViewHolderForLayoutPosition(0) ?: return null |             val holder = findFirstVisibleViewHolder() ?: return null | ||||||
|             val position = holder.adapterPosition |             val position = holder.adapterPosition | ||||||
|             val offset = holder.itemView.top |             val offset = holder.itemView.top | ||||||
|             return RecyclerScrollPosition(position, offset) |             return RecyclerScrollPosition(position, offset) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |     // because findViewHolderForLayoutPosition doesn't work :'( | ||||||
|  |     private fun RecyclerView.findFirstVisibleViewHolder(): RecyclerView.ViewHolder? { | ||||||
|  |         if (childCount > 0) { | ||||||
|  |             val child = getChildAt(0) | ||||||
|  |             return findContainingViewHolder(child) | ||||||
|  |         } | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private sealed class Type { |     private sealed class Type { | ||||||
|         class Artifact(val artifact: MarkwonArtifact) : Type() |         class Artifact(val artifact: MarkwonArtifact) : Type() | ||||||
|         class Tag(val tag: String) : Type() |         class Tag(val tag: String) : Type() | ||||||
|  | |||||||
| @ -45,5 +45,7 @@ public class AdditionalSpacingSample extends MarkwonTextViewSample { | |||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|       .build(); |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -61,7 +61,7 @@ class ReadMorePlugin extends AbstractMarkwonPlugin { | |||||||
|     // establish connections with all _dynamic_ content that your markdown supports, |     // establish connections with all _dynamic_ content that your markdown supports, | ||||||
|     //  like images, tables, latex, etc |     //  like images, tables, latex, etc | ||||||
|     registry.require(ImagesPlugin.class); |     registry.require(ImagesPlugin.class); | ||||||
|     registry.require(TablePlugin.class); | //    registry.require(TablePlugin.class); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @Override |   @Override | ||||||
|  | |||||||
| @ -0,0 +1,32 @@ | |||||||
|  | package io.noties.markwon.app.samples.editor; | ||||||
|  | 
 | ||||||
|  | import java.util.concurrent.Executors; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonEditTextSample; | ||||||
|  | import io.noties.markwon.app.samples.editor.shared.HeadingEditHandler; | ||||||
|  | import io.noties.markwon.editor.MarkwonEditor; | ||||||
|  | import io.noties.markwon.editor.MarkwonEditorTextWatcher; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182113954", | ||||||
|  |   title = "Heading edit handler", | ||||||
|  |   description = "Handling of heading node in editor", | ||||||
|  |   artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER}, | ||||||
|  |   tags = {Tags.editor} | ||||||
|  | ) | ||||||
|  | public class EditorHeadingSample extends MarkwonEditTextSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final Markwon markwon = Markwon.create(context); | ||||||
|  |     final MarkwonEditor editor = MarkwonEditor.builder(markwon) | ||||||
|  |       .useEditHandler(new HeadingEditHandler()) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( | ||||||
|  |       editor, Executors.newSingleThreadExecutor(), editText)); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,82 @@ | |||||||
|  | package io.noties.markwon.app.samples.editor.shared; | ||||||
|  | 
 | ||||||
|  | import android.text.Editable; | ||||||
|  | import android.text.Spanned; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.core.MarkwonTheme; | ||||||
|  | import io.noties.markwon.core.spans.HeadingSpan; | ||||||
|  | import io.noties.markwon.editor.EditHandler; | ||||||
|  | import io.noties.markwon.editor.PersistedSpans; | ||||||
|  | 
 | ||||||
|  | public class HeadingEditHandler implements EditHandler<HeadingSpan> { | ||||||
|  | 
 | ||||||
|  |   private MarkwonTheme theme; | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void init(@NonNull Markwon markwon) { | ||||||
|  |     this.theme = markwon.configuration().theme(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||||
|  |     builder | ||||||
|  |       .persistSpan(Head1.class, () -> new Head1(theme)) | ||||||
|  |       .persistSpan(Head2.class, () -> new Head2(theme)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void handleMarkdownSpan( | ||||||
|  |     @NonNull PersistedSpans persistedSpans, | ||||||
|  |     @NonNull Editable editable, | ||||||
|  |     @NonNull String input, | ||||||
|  |     @NonNull HeadingSpan span, | ||||||
|  |     int spanStart, | ||||||
|  |     int spanTextLength | ||||||
|  |   ) { | ||||||
|  |     final Class<?> type; | ||||||
|  |     switch (span.getLevel()) { | ||||||
|  |       case 1: | ||||||
|  |         type = Head1.class; | ||||||
|  |         break; | ||||||
|  |       case 2: | ||||||
|  |         type = Head2.class; | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         type = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (type != null) { | ||||||
|  |       final int index = input.indexOf('\n', spanStart + spanTextLength); | ||||||
|  |       final int end = index < 0 | ||||||
|  |         ? input.length() | ||||||
|  |         : index; | ||||||
|  |       editable.setSpan( | ||||||
|  |         persistedSpans.get(type), | ||||||
|  |         spanStart, | ||||||
|  |         end, | ||||||
|  |         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @NonNull | ||||||
|  |   @Override | ||||||
|  |   public Class<HeadingSpan> markdownSpanType() { | ||||||
|  |     return HeadingSpan.class; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static class Head1 extends HeadingSpan { | ||||||
|  |     Head1(@NonNull MarkwonTheme theme) { | ||||||
|  |       super(theme, 1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static class Head2 extends HeadingSpan { | ||||||
|  |     Head2(@NonNull MarkwonTheme theme) { | ||||||
|  |       super(theme, 2); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,87 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import android.text.Layout; | ||||||
|  | import android.text.style.AlignmentSpan; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.MarkwonConfiguration; | ||||||
|  | import io.noties.markwon.RenderProps; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.tag.SimpleTagHandler; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182114630", | ||||||
|  |   title = "Align HTML tag", | ||||||
|  |   description = "Implement custom HTML tag handling", | ||||||
|  |   artifacts = MarkwonArtifact.HTML, | ||||||
|  |   tags = {Tags.rendering, Tags.span, Tags.html} | ||||||
|  | ) | ||||||
|  | public class HtmlAlignSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       "<align center>We are centered</align>\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "<align end>We are at the end</align>\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "<align>We should be at the start</align>\n" + | ||||||
|  |       "\n"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(HtmlPlugin.create()) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @Override | ||||||
|  |         public void configure(@NonNull Registry registry) { | ||||||
|  |           registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin | ||||||
|  |             .addHandler(new AlignTagHandler())); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class AlignTagHandler extends SimpleTagHandler { | ||||||
|  | 
 | ||||||
|  |   @Nullable | ||||||
|  |   @Override | ||||||
|  |   public Object getSpans( | ||||||
|  |     @NonNull MarkwonConfiguration configuration, | ||||||
|  |     @NonNull RenderProps renderProps, | ||||||
|  |     @NonNull HtmlTag tag) { | ||||||
|  | 
 | ||||||
|  |     final Layout.Alignment alignment; | ||||||
|  | 
 | ||||||
|  |     // html attribute without value, <align center></align> | ||||||
|  |     if (tag.attributes().containsKey("center")) { | ||||||
|  |       alignment = Layout.Alignment.ALIGN_CENTER; | ||||||
|  |     } else if (tag.attributes().containsKey("end")) { | ||||||
|  |       alignment = Layout.Alignment.ALIGN_OPPOSITE; | ||||||
|  |     } else { | ||||||
|  |       // empty value or any other will make regular alignment | ||||||
|  |       alignment = Layout.Alignment.ALIGN_NORMAL; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return new AlignmentSpan.Standard(alignment); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @NonNull | ||||||
|  |   @Override | ||||||
|  |   public Collection<String> supportedTags() { | ||||||
|  |     return Collections.singleton("align"); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,115 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import android.text.Layout; | ||||||
|  | import android.text.style.AlignmentSpan; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.noties.debug.Debug; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.MarkwonVisitor; | ||||||
|  | import io.noties.markwon.SpannableBuilder; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.app.samples.html.shared.IFrameHtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.MarkwonHtmlRenderer; | ||||||
|  | import io.noties.markwon.html.TagHandler; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182120101", | ||||||
|  |   title = "Center HTML tag", | ||||||
|  |   description = "Handling of `center` HTML tag", | ||||||
|  |   artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE}, | ||||||
|  |   tags = {Tags.rendering, Tags.html} | ||||||
|  | ) | ||||||
|  | public class HtmlCenterTagSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String html = "<html>\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "<head></head>\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "<body>\n" + | ||||||
|  |       "    <p></p>\n" + | ||||||
|  |       "    <h3>LiSA's Sword Art Online: Alicization OP Song \"ADAMAS\" Certified Platinum with 250,000 Downloads</h3>\n" + | ||||||
|  |       "    <p></p>\n" + | ||||||
|  |       "    <h5>The upper tune was already certified Gold one month after its digital release</h5>\n" + | ||||||
|  |       "    <p>According to The Recording Industry Association of Japan (RIAJ)'s monthly report for April 2020, one of the <span\n" + | ||||||
|  |       "            style=\"color: #ff9900;\"><strong><a href=\"http://www.lxixsxa.com/\" target=\"_blank\"><span\n" + | ||||||
|  |       "                        style=\"color: #ff9900;\">LiSA</span></a></strong></span>'s 14th single songs,\n" + | ||||||
|  |       "        <strong>\"ADAMAS\"</strong> (the first OP theme for the TV anime <a href=\"/sword-art-online\"\n" + | ||||||
|  |       "            target=\"_blank\"><span style=\"color: #ff9900;\"><strong><em>Sword Art Online:\n" + | ||||||
|  |       "                        Alicization</em></strong></span></a>) has been certified <strong>Platinum</strong> for\n" + | ||||||
|  |       "        surpassing 250,000 downloads.</p>\n" + | ||||||
|  |       "    <p> </p>\n" + | ||||||
|  |       "    <p>As a double A-side single with <strong>\"Akai Wana (who loves it?),\"</strong> <strong>\"ADAMAS\"</strong> was\n" + | ||||||
|  |       "        released from SACRA Music in Japan on December 12, 2018. Its CD single ranked second in Oricon's weekly single\n" + | ||||||
|  |       "        chart by selling 35,000 copies in its first week. Meanwhile, the song was released digitally two months prior to\n" + | ||||||
|  |       "        its CD release, October 8, then reached Gold (100,000 downloads) in the following month.</p>\n" + | ||||||
|  |       "    <p> </p>\n" + | ||||||
|  |       "    <p> </p>\n" + | ||||||
|  |       "    <center>\n" + | ||||||
|  |       "        <p><strong>\"ADAMAS\"</strong> MV YouTube EDIT ver.:</p>\n" + | ||||||
|  |       "        <p><iframe src=\"https://www.youtube.com/embed/UeEIl4JlE-g\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe>\n" + | ||||||
|  |       "        </p>\n" + | ||||||
|  |       "        <p> </p>\n" + | ||||||
|  |       "        <p>Standard edition CD jacket:</p>\n" + | ||||||
|  |       "        <p><img src=\"https://img1.ak.crunchyroll.com/i/spire2/d7b1d6bc7563224388ef5ffc04a967581589950464_full.jpg\"\n" + | ||||||
|  |       "                alt=\"\" width=\"640\" height=\"635\"></p>\n" + | ||||||
|  |       "    </center>\n" + | ||||||
|  |       "    <p>  </p>\n" + | ||||||
|  |       "    <hr>\n" + | ||||||
|  |       "    <p> </p>\n" + | ||||||
|  |       "    <p>Source: RIAJ press release</p>\n" + | ||||||
|  |       "    <p> </p>\n" + | ||||||
|  |       "    <p><em>©SACRA MUSIC</em></p>\n" + | ||||||
|  |       "    <p> </p>\n" + | ||||||
|  |       "    <p style=\"text-align: center;\"><a href=\"https://got.cr/PremiumTrial-NewsBanner4\"><em><img\n" + | ||||||
|  |       "                    src=\"https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1559091520_full.png\"\n" + | ||||||
|  |       "                    alt=\"\" width=\"640\" height=\"43\"></em></a></p>\n" + | ||||||
|  |       "</body>\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "</html>"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(HtmlPlugin.create(plugin -> | ||||||
|  |         plugin.addHandler(new CenterTagHandler()))) | ||||||
|  |       .usePlugin(new IFrameHtmlPlugin()) | ||||||
|  |       .usePlugin(ImagesPlugin.create()) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, html); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class CenterTagHandler extends TagHandler { | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { | ||||||
|  |     Debug.e("center, isBlock: %s", tag.isBlock()); | ||||||
|  |     if (tag.isBlock()) { | ||||||
|  |       visitChildren(visitor, renderer, tag.getAsBlock()); | ||||||
|  |     } | ||||||
|  |     SpannableBuilder.setSpans( | ||||||
|  |       visitor.builder(), | ||||||
|  |       new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), | ||||||
|  |       tag.start(), | ||||||
|  |       tag.end() | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @NonNull | ||||||
|  |   @Override | ||||||
|  |   public Collection<String> supportedTags() { | ||||||
|  |     return Collections.singleton("center"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @ -0,0 +1,426 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Color; | ||||||
|  | import android.graphics.Paint; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.text.Layout; | ||||||
|  | import android.text.Spanned; | ||||||
|  | import android.text.style.ClickableSpan; | ||||||
|  | import android.text.style.LeadingMarginSpan; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.Iterator; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.MarkwonVisitor; | ||||||
|  | import io.noties.markwon.SpannableBuilder; | ||||||
|  | import io.noties.markwon.app.R; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonSample; | ||||||
|  | import io.noties.markwon.core.MarkwonTheme; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.MarkwonHtmlRenderer; | ||||||
|  | import io.noties.markwon.html.TagHandler; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | import io.noties.markwon.utils.LeadingMarginUtils; | ||||||
|  | import io.noties.markwon.utils.NoCopySpannableFactory; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182120752", | ||||||
|  |   title = "Details HTML tag", | ||||||
|  |   description = "Handling of `details` HTML tag", | ||||||
|  |   artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE}, | ||||||
|  |   tags = {Tags.image, Tags.rendering, Tags.html} | ||||||
|  | ) | ||||||
|  | public class HtmlDetailsSample extends MarkwonSample { | ||||||
|  | 
 | ||||||
|  |   private Context context; | ||||||
|  |   private ViewGroup content; | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   protected int getLayoutResId() { | ||||||
|  |     return R.layout.activity_html_details; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void onViewCreated(@NotNull View view) { | ||||||
|  |     context = view.getContext(); | ||||||
|  |     content = view.findViewById(R.id.content); | ||||||
|  |     render(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private void render() { | ||||||
|  |     final String md = "# Hello\n\n<details>\n" + | ||||||
|  |       "  <summary>stuff with \n\n*mark* **down**\n\n</summary>\n" + | ||||||
|  |       "  <p>\n\n" + | ||||||
|  |       "<!-- the above p cannot start right at the beginning of the line and is mandatory for everything else to work -->\n" + | ||||||
|  |       "## *formatted* **heading** with [a](link)\n" + | ||||||
|  |       "```java\n" + | ||||||
|  |       "code block\n" + | ||||||
|  |       "```\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "  <details>\n" + | ||||||
|  |       "    <summary><small>nested</small> stuff</summary><p>\n" + | ||||||
|  |       "<!-- alternative placement of p shown above -->\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "* list\n" + | ||||||
|  |       "* with\n" + | ||||||
|  |       "\n\n" + | ||||||
|  |       "\n\n" + | ||||||
|  |       " 1. nested\n" + | ||||||
|  |       " 1. items\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "    ```java\n" + | ||||||
|  |       "    // including code\n" + | ||||||
|  |       "    ```\n" + | ||||||
|  |       " 1. blocks\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "<details><summary>The 3rd!</summary>\n\n" + | ||||||
|  |       "**bold** _em_\n</details>" + | ||||||
|  |       "  </p></details>\n" + | ||||||
|  |       "</p></details>\n\n" + | ||||||
|  |       "and **this** *is* how..."; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(HtmlPlugin.create(plugin -> | ||||||
|  |         plugin.addHandler(new DetailsTagHandler()))) | ||||||
|  |       .usePlugin(ImagesPlugin.create()) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     final Spanned spanned = markwon.toMarkdown(md); | ||||||
|  |     final DetailsParsingSpan[] spans = spanned.getSpans(0, spanned.length(), DetailsParsingSpan.class); | ||||||
|  | 
 | ||||||
|  |     // if we have no details, proceed as usual (single text-view) | ||||||
|  |     if (spans == null || spans.length == 0) { | ||||||
|  |       // no details | ||||||
|  |       final TextView textView = appendTextView(); | ||||||
|  |       markwon.setParsedMarkdown(textView, spanned); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     final List<DetailsElement> list = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  |     for (DetailsParsingSpan span : spans) { | ||||||
|  |       final DetailsElement e = settle(new DetailsElement(spanned.getSpanStart(span), spanned.getSpanEnd(span), span.summary), list); | ||||||
|  |       if (e != null) { | ||||||
|  |         list.add(e); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (DetailsElement element : list) { | ||||||
|  |       initDetails(element, spanned); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     sort(list); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     TextView textView; | ||||||
|  |     int start = 0; | ||||||
|  | 
 | ||||||
|  |     for (DetailsElement element : list) { | ||||||
|  | 
 | ||||||
|  |       if (element.start != start) { | ||||||
|  |         // subSequence and add new TextView | ||||||
|  |         textView = appendTextView(); | ||||||
|  |         textView.setText(subSequenceTrimmed(spanned, start, element.start)); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // now add details TextView | ||||||
|  |       textView = appendTextView(); | ||||||
|  |       initDetailsTextView(markwon, textView, element); | ||||||
|  | 
 | ||||||
|  |       start = element.end; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (start != spanned.length()) { | ||||||
|  |       // another textView with rest content | ||||||
|  |       textView = appendTextView(); | ||||||
|  |       textView.setText(subSequenceTrimmed(spanned, start, spanned.length())); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @NonNull | ||||||
|  |   private TextView appendTextView() { | ||||||
|  |     final View view = LayoutInflater.from(context) | ||||||
|  |       .inflate(R.layout.view_html_details_text_view, content, false); | ||||||
|  |     final TextView textView = view.findViewById(R.id.text); | ||||||
|  |     content.addView(view); | ||||||
|  |     return textView; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private void initDetailsTextView( | ||||||
|  |     @NonNull Markwon markwon, | ||||||
|  |     @NonNull TextView textView, | ||||||
|  |     @NonNull DetailsElement element) { | ||||||
|  | 
 | ||||||
|  |     // minor optimization | ||||||
|  |     textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); | ||||||
|  | 
 | ||||||
|  |     // so, each element with children is a details tag | ||||||
|  |     // there is a reason why we needed the SpannableBuilder in the first place -> we must revert spans | ||||||
|  | //        final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||||
|  |     final SpannableBuilder builder = new SpannableBuilder(); | ||||||
|  |     append(builder, markwon, textView, element, element); | ||||||
|  |     markwon.setParsedMarkdown(textView, builder.spannableStringBuilder()); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private void append( | ||||||
|  |     @NonNull SpannableBuilder builder, | ||||||
|  |     @NonNull Markwon markwon, | ||||||
|  |     @NonNull TextView textView, | ||||||
|  |     @NonNull DetailsElement root, | ||||||
|  |     @NonNull DetailsElement element) { | ||||||
|  |     if (!element.children.isEmpty()) { | ||||||
|  | 
 | ||||||
|  |       final int start = builder.length(); | ||||||
|  | 
 | ||||||
|  | //            builder.append(element.content); | ||||||
|  |       builder.append(subSequenceTrimmed(element.content, 0, element.content.length())); | ||||||
|  | 
 | ||||||
|  |       builder.setSpan(new ClickableSpan() { | ||||||
|  |         @Override | ||||||
|  |         public void onClick(@NonNull View widget) { | ||||||
|  |           element.expanded = !element.expanded; | ||||||
|  | 
 | ||||||
|  |           initDetailsTextView(markwon, textView, root); | ||||||
|  |         } | ||||||
|  |       }, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||||
|  | 
 | ||||||
|  |       if (element.expanded) { | ||||||
|  |         for (DetailsElement child : element.children) { | ||||||
|  |           append(builder, markwon, textView, root, child); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       builder.setSpan(new DetailsSpan(markwon.configuration().theme(), element), start); | ||||||
|  | 
 | ||||||
|  |     } else { | ||||||
|  |       builder.append(element.content); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // if null -> remove from where it was processed, | ||||||
|  |   // else replace from where it was processed with a new one (can become expandable) | ||||||
|  |   @Nullable | ||||||
|  |   private static DetailsElement settle( | ||||||
|  |     @NonNull DetailsElement element, | ||||||
|  |     @NonNull List<? extends DetailsElement> elements) { | ||||||
|  |     for (DetailsElement e : elements) { | ||||||
|  |       if (element.start > e.start && element.end <= e.end) { | ||||||
|  |         final DetailsElement settled = settle(element, e.children); | ||||||
|  |         if (settled != null) { | ||||||
|  | 
 | ||||||
|  |           // the thing is we must balance children if done like this | ||||||
|  |           // let's just create a tree actually, so we are easier to modify | ||||||
|  |           final Iterator<DetailsElement> iterator = e.children.iterator(); | ||||||
|  |           while (iterator.hasNext()) { | ||||||
|  |             final DetailsElement balanced = settle(iterator.next(), Collections.singletonList(element)); | ||||||
|  |             if (balanced == null) { | ||||||
|  |               iterator.remove(); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           // add to our children | ||||||
|  |           e.children.add(element); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return element; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static void initDetails(@NonNull DetailsElement element, @NonNull Spanned spanned) { | ||||||
|  |     int end = element.end; | ||||||
|  |     for (int i = element.children.size() - 1; i >= 0; i--) { | ||||||
|  |       final DetailsElement child = element.children.get(i); | ||||||
|  |       if (child.end < end) { | ||||||
|  |         element.children.add(new DetailsElement(child.end, end, spanned.subSequence(child.end, end))); | ||||||
|  |       } | ||||||
|  |       initDetails(child, spanned); | ||||||
|  |       end = child.start; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     final int start = (element.start + element.content.length()); | ||||||
|  |     if (end != start) { | ||||||
|  |       element.children.add(new DetailsElement(start, end, spanned.subSequence(start, end))); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static void sort(@NonNull List<DetailsElement> elements) { | ||||||
|  |     Collections.sort(elements, (o1, o2) -> Integer.compare(o1.start, o2.start)); | ||||||
|  |     for (DetailsElement element : elements) { | ||||||
|  |       sort(element.children); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @NonNull | ||||||
|  |   private static CharSequence subSequenceTrimmed(@NonNull CharSequence cs, int start, int end) { | ||||||
|  | 
 | ||||||
|  |     while (start < end) { | ||||||
|  | 
 | ||||||
|  |       final boolean isStartEmpty = Character.isWhitespace(cs.charAt(start)); | ||||||
|  |       final boolean isEndEmpty = Character.isWhitespace(cs.charAt(end - 1)); | ||||||
|  | 
 | ||||||
|  |       if (!isStartEmpty && !isEndEmpty) { | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (isStartEmpty) { | ||||||
|  |         start += 1; | ||||||
|  |       } | ||||||
|  |       if (isEndEmpty) { | ||||||
|  |         end -= 1; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return cs.subSequence(start, end); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static class DetailsElement { | ||||||
|  | 
 | ||||||
|  |     final int start; | ||||||
|  |     final int end; | ||||||
|  |     final CharSequence content; | ||||||
|  |     final List<DetailsElement> children = new ArrayList<>(0); | ||||||
|  | 
 | ||||||
|  |     boolean expanded; | ||||||
|  | 
 | ||||||
|  |     DetailsElement(int start, int end, @NonNull CharSequence content) { | ||||||
|  |       this.start = start; | ||||||
|  |       this.end = end; | ||||||
|  |       this.content = content; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     @NonNull | ||||||
|  |     public String toString() { | ||||||
|  |       return "DetailsElement{" + | ||||||
|  |         "start=" + start + | ||||||
|  |         ", end=" + end + | ||||||
|  |         ", content=" + toStringContent(content) + | ||||||
|  |         ", children=" + children + | ||||||
|  |         ", expanded=" + expanded + | ||||||
|  |         '}'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private static String toStringContent(@NonNull CharSequence cs) { | ||||||
|  |       return cs.toString().replaceAll("\n", "\\n"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static class DetailsTagHandler extends TagHandler { | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void handle( | ||||||
|  |       @NonNull MarkwonVisitor visitor, | ||||||
|  |       @NonNull MarkwonHtmlRenderer renderer, | ||||||
|  |       @NonNull HtmlTag tag) { | ||||||
|  | 
 | ||||||
|  |       int summaryEnd = -1; | ||||||
|  | 
 | ||||||
|  |       for (HtmlTag child : tag.getAsBlock().children()) { | ||||||
|  | 
 | ||||||
|  |         if (!child.isClosed()) { | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ("summary".equals(child.name())) { | ||||||
|  |           summaryEnd = child.end(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final TagHandler tagHandler = renderer.tagHandler(child.name()); | ||||||
|  |         if (tagHandler != null) { | ||||||
|  |           tagHandler.handle(visitor, renderer, child); | ||||||
|  |         } else if (child.isBlock()) { | ||||||
|  |           visitChildren(visitor, renderer, child.getAsBlock()); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (summaryEnd > -1) { | ||||||
|  |         visitor.builder().setSpan(new DetailsParsingSpan( | ||||||
|  |           subSequenceTrimmed(visitor.builder(), tag.start(), summaryEnd) | ||||||
|  |         ), tag.start(), tag.end()); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Collection<String> supportedTags() { | ||||||
|  |       return Collections.singleton("details"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static class DetailsParsingSpan { | ||||||
|  | 
 | ||||||
|  |     final CharSequence summary; | ||||||
|  | 
 | ||||||
|  |     DetailsParsingSpan(@NonNull CharSequence summary) { | ||||||
|  |       this.summary = summary; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static class DetailsSpan implements LeadingMarginSpan { | ||||||
|  | 
 | ||||||
|  |     private final DetailsElement element; | ||||||
|  |     private final int blockMargin; | ||||||
|  |     private final int blockQuoteWidth; | ||||||
|  | 
 | ||||||
|  |     private final Rect rect = new Rect(); | ||||||
|  |     private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); | ||||||
|  | 
 | ||||||
|  |     DetailsSpan(@NonNull MarkwonTheme theme, @NonNull DetailsElement element) { | ||||||
|  |       this.element = element; | ||||||
|  |       this.blockMargin = theme.getBlockMargin(); | ||||||
|  |       this.blockQuoteWidth = theme.getBlockQuoteWidth(); | ||||||
|  |       this.paint.setStyle(Paint.Style.FILL); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getLeadingMargin(boolean first) { | ||||||
|  |       return blockMargin; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { | ||||||
|  | 
 | ||||||
|  |       if (LeadingMarginUtils.selfStart(start, text, this)) { | ||||||
|  |         rect.set(x, top, x + blockMargin, bottom); | ||||||
|  |         if (element.expanded) { | ||||||
|  |           paint.setColor(Color.GREEN); | ||||||
|  |         } else { | ||||||
|  |           paint.setColor(Color.RED); | ||||||
|  |         } | ||||||
|  |         paint.setStyle(Paint.Style.FILL); | ||||||
|  |         c.drawRect(rect, paint); | ||||||
|  | 
 | ||||||
|  |       } else { | ||||||
|  | 
 | ||||||
|  |         if (element.expanded) { | ||||||
|  |           final int l = (blockMargin - blockQuoteWidth) / 2; | ||||||
|  |           rect.set(x + l, top, x + l + blockQuoteWidth, bottom); | ||||||
|  |           paint.setStyle(Paint.Style.FILL); | ||||||
|  |           paint.setColor(Color.GRAY); | ||||||
|  |           c.drawRect(rect, paint); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,42 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182171424", | ||||||
|  |   title = "Disable HTML", | ||||||
|  |   description = "Disable HTML via replacing special `<` and `>` symbols", | ||||||
|  |   artifacts = MarkwonArtifact.CORE, | ||||||
|  |   tags = {Tags.html, Tags.rendering, Tags.parsing, Tags.plugin} | ||||||
|  | ) | ||||||
|  | public class HtmlDisableSanitizeSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "# Html <b>disabled</b>\n\n" + | ||||||
|  |       "<em>emphasis <strong>strong</strong>\n\n" + | ||||||
|  |       "<p>paragraph <img src='hey.jpg' /></p>\n\n" + | ||||||
|  |       "<test></test>\n\n" + | ||||||
|  |       "<test>"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public String processMarkdown(@NonNull String markdown) { | ||||||
|  |           return markdown | ||||||
|  |             .replaceAll("<", "<") | ||||||
|  |             .replaceAll(">", ">"); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,47 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.html.HtmlEmptyTagReplacement; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182115725", | ||||||
|  |   title = "HTML empty tag replacement", | ||||||
|  |   description = "Render custom content when HTML tag contents is empty, " + | ||||||
|  |     "in case of self-closed HTML tags or tags without content (closed " + | ||||||
|  |     "right after opened)", | ||||||
|  |   artifacts = MarkwonArtifact.HTML, | ||||||
|  |   tags = {Tags.rendering, Tags.html} | ||||||
|  | ) | ||||||
|  | public class HtmlEmptyTagReplacementSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       "<empty></empty> the `<empty></empty>` is replaced?"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(HtmlPlugin.create(plugin -> { | ||||||
|  |         plugin.emptyTagReplacement(new HtmlEmptyTagReplacement() { | ||||||
|  |           @Nullable | ||||||
|  |           @Override | ||||||
|  |           public String replace(@NonNull HtmlTag tag) { | ||||||
|  |             if ("empty".equals(tag.name())) { | ||||||
|  |               return "REPLACED_EMPTY_WITH_IT"; | ||||||
|  |             } | ||||||
|  |             return super.replace(tag); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       })) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,102 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import android.text.TextUtils; | ||||||
|  | import android.text.style.AbsoluteSizeSpan; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.annotation.Px; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.MarkwonVisitor; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.MarkwonHtmlRenderer; | ||||||
|  | import io.noties.markwon.html.TagHandler; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182115103", | ||||||
|  |   title = "Enhance custom HTML tag", | ||||||
|  |   description = "Custom HTML tag implementation " + | ||||||
|  |     "that _enhances_ a part of text given start and end indices", | ||||||
|  |   artifacts = MarkwonArtifact.HTML, | ||||||
|  |   tags = {Tags.rendering, Tags.span, Tags.html} | ||||||
|  | ) | ||||||
|  | public class HtmlEnhanceSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       "<enhance start=\"5\" end=\"12\">This is text that must be enhanced, at least a part of it</enhance>"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(HtmlPlugin.create()) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @Override | ||||||
|  |         public void configure(@NonNull Registry registry) { | ||||||
|  |           registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin | ||||||
|  |             .addHandler(new EnhanceTagHandler((int) (textView.getTextSize() * 2 + .05F)))); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class EnhanceTagHandler extends TagHandler { | ||||||
|  | 
 | ||||||
|  |   private final int enhanceTextSize; | ||||||
|  | 
 | ||||||
|  |   EnhanceTagHandler(@Px int enhanceTextSize) { | ||||||
|  |     this.enhanceTextSize = enhanceTextSize; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void handle( | ||||||
|  |     @NonNull MarkwonVisitor visitor, | ||||||
|  |     @NonNull MarkwonHtmlRenderer renderer, | ||||||
|  |     @NonNull HtmlTag tag) { | ||||||
|  | 
 | ||||||
|  |     // we require start and end to be present | ||||||
|  |     final int start = parsePosition(tag.attributes().get("start")); | ||||||
|  |     final int end = parsePosition(tag.attributes().get("end")); | ||||||
|  | 
 | ||||||
|  |     if (start > -1 && end > -1) { | ||||||
|  |       visitor.builder().setSpan( | ||||||
|  |         new AbsoluteSizeSpan(enhanceTextSize), | ||||||
|  |         tag.start() + start, | ||||||
|  |         tag.start() + end | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @NonNull | ||||||
|  |   @Override | ||||||
|  |   public Collection<String> supportedTags() { | ||||||
|  |     return Collections.singleton("enhance"); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static int parsePosition(@Nullable String value) { | ||||||
|  |     int position; | ||||||
|  |     if (!TextUtils.isEmpty(value)) { | ||||||
|  |       try { | ||||||
|  |         position = Integer.parseInt(value); | ||||||
|  |       } catch (NumberFormatException e) { | ||||||
|  |         e.printStackTrace(); | ||||||
|  |         position = -1; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       position = -1; | ||||||
|  |     } | ||||||
|  |     return position; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,50 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.app.samples.html.shared.IFrameHtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182115521", | ||||||
|  |   title = "IFrame HTML tag", | ||||||
|  |   description = "Handling of `iframe` HTML tag", | ||||||
|  |   artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE}, | ||||||
|  |   tags = {Tags.image, Tags.rendering, Tags.html} | ||||||
|  | ) | ||||||
|  | public class HtmlIFrameSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       "# Hello iframe\n\n" + | ||||||
|  |       "<p class=\"p1\"><img title=\"JUMP FORCE\" src=\"https://img1.ak.crunchyroll.com/i/spire1/f0c009039dd9f8dff5907fff148adfca1587067000_full.jpg\" alt=\"JUMP FORCE\" width=\"640\" height=\"362\" /></p>\n" + | ||||||
|  |       "<p class=\"p2\"> </p>\n" + | ||||||
|  |       "<p class=\"p1\">Switch owners will soon get to take part in the ultimate <em>Shonen Jump </em>rumble. Bandai Namco announced plans to bring <strong><em>Jump Force </em></strong>to <strong>Switch</strong> as <strong><em>Jump Force Deluxe Edition</em></strong>, with a release set for sometime this year. This version will include all of the original playable characters and those from Character Pass 1, and <strong>Character Pass 2 is also in the works </strong>for all versions, starting with <strong>Shoto Todoroki from </strong><span style=\"color: #ff9900;\"><a href=\"/my-hero-academia?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><strong><em>My Hero Academia</em></strong></span></a></span>.</p>\n" + | ||||||
|  |       "<p class=\"p2\"> </p>\n" + | ||||||
|  |       "<p class=\"p1\">Other than Todoroki, Bandai Namco hinted that the four other Character Pass 2 characters will hail from <span style=\"color: #ff9900;\"><a href=\"/hunter-x-hunter?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>Hunter x Hunter</em></span></a></span>, <em>Yu Yu Hakusho</em>, <span style=\"color: #ff9900;\"><a href=\"/bleach?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>Bleach</em></span></a></span>, and <span style=\"color: #ff9900;\"><a href=\"/jojos-bizarre-adventure?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>JoJo's Bizarre Adventure</em></span></a></span>. Character Pass 2 will be priced at $17.99, and Todoroki launches this spring.<span class=\"Apple-converted-space\"> </span></p>\n" + | ||||||
|  |       "<p class=\"p2\"> </p>\n" + | ||||||
|  |       "<p class=\"p1\"><iframe style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://www.youtube.com/embed/At1qTj-LWCc\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe></p>\n" + | ||||||
|  |       "<p class=\"p2\"> </p>\n" + | ||||||
|  |       "<p class=\"p1\">Character Pass 2 promo:</p>\n" + | ||||||
|  |       "<p class=\"p2\"> </p>\n" + | ||||||
|  |       "<p class=\"p1\"><iframe style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://www.youtube.com/embed/CukwN6kV4R4\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe></p>\n" + | ||||||
|  |       "<p class=\"p2\"> </p>\n" + | ||||||
|  |       "<p class=\"p1\"><a href=\"https://got.cr/PremiumTrial-NewsBanner4\"><img style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1587067041_full.png\" alt=\"\" width=\"640\" height=\"43\" /></a></p>\n" + | ||||||
|  |       "<p class=\"p2\"> </p>\n" + | ||||||
|  |       "<p class=\"p1\">-------</p>\n" + | ||||||
|  |       "<p class=\"p1\"><em>Joseph Luster is the Games and Web editor at </em><a href=\"http://www.otakuusamagazine.com/ME2/Default.asp\"><em>Otaku USA Magazine</em></a><em>. You can read his webcomic, </em><a href=\"http://subhumanzoids.com/comics/big-dumb-fighting-idiots/\">BIG DUMB FIGHTING IDIOTS</a><em> at </em><a href=\"http://subhumanzoids.com/\"><em>subhumanzoids</em></a><em>. Follow him on Twitter </em><a href=\"https://twitter.com/Moldilox\"><em>@Moldilox</em></a><em>.</em><span class=\"Apple-converted-space\"> </span></p>"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(ImagesPlugin.create()) | ||||||
|  |       .usePlugin(HtmlPlugin.create()) | ||||||
|  |       .usePlugin(new IFrameHtmlPlugin()) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @ -0,0 +1,44 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182115300", | ||||||
|  |   title = "Html images", | ||||||
|  |   description = "Usage of HTML images", | ||||||
|  |   artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE}, | ||||||
|  |   tags = {Tags.image, Tags.rendering, Tags.html} | ||||||
|  | ) | ||||||
|  | public class HtmlImageSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     // treat unclosed/void `img` tag as HTML inline | ||||||
|  |     final String md = "" + | ||||||
|  |       "## Try CommonMark\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "Markwon IMG:\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "New lines...\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "HTML IMG:\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "<img src=\"https://upload.wikimedia.org/wikipedia/it/thumb/c/c5/GTA_2.JPG/220px-GTA_2.JPG\"></img>\n" + | ||||||
|  |       "\n" + | ||||||
|  |       "New lines\n\n"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(ImagesPlugin.create()) | ||||||
|  |       .usePlugin(HtmlPlugin.create()) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,86 @@ | |||||||
|  | package io.noties.markwon.app.samples.html; | ||||||
|  | 
 | ||||||
|  | import android.text.style.AbsoluteSizeSpan; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.Random; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.MarkwonVisitor; | ||||||
|  | import io.noties.markwon.SpannableBuilder; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.MarkwonHtmlRenderer; | ||||||
|  | import io.noties.markwon.html.TagHandler; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182114923", | ||||||
|  |   title = "Random char size HTML tag", | ||||||
|  |   description = "Implementation of a custom HTML tag handler " + | ||||||
|  |     "that assigns each character a random size", | ||||||
|  |   artifacts = MarkwonArtifact.HTML, | ||||||
|  |   tags = {Tags.rendering, Tags.span, Tags.html} | ||||||
|  | ) | ||||||
|  | public class HtmlRandomCharSize extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       "<random-char-size>\n" + | ||||||
|  |       "This message should have a jumpy feeling because of different sizes of characters\n" + | ||||||
|  |       "</random-char-size>\n\n"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(HtmlPlugin.create()) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @Override | ||||||
|  |         public void configure(@NonNull Registry registry) { | ||||||
|  |           registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin | ||||||
|  |             .addHandler(new RandomCharSize(new Random(42L), textView.getTextSize()))); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class RandomCharSize extends TagHandler { | ||||||
|  | 
 | ||||||
|  |   private final Random random; | ||||||
|  |   private final float base; | ||||||
|  | 
 | ||||||
|  |   RandomCharSize(@NonNull Random random, float base) { | ||||||
|  |     this.random = random; | ||||||
|  |     this.base = base; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void handle( | ||||||
|  |     @NonNull MarkwonVisitor visitor, | ||||||
|  |     @NonNull MarkwonHtmlRenderer renderer, | ||||||
|  |     @NonNull HtmlTag tag) { | ||||||
|  | 
 | ||||||
|  |     final SpannableBuilder builder = visitor.builder(); | ||||||
|  | 
 | ||||||
|  |     // text content is already added, we should only apply spans | ||||||
|  | 
 | ||||||
|  |     for (int i = tag.start(), end = tag.end(); i < end; i++) { | ||||||
|  |       final int size = (int) (base * (random.nextFloat() + 0.5F) + 0.5F); | ||||||
|  |       builder.setSpan(new AbsoluteSizeSpan(size, false), i, i + 1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @NonNull | ||||||
|  |   @Override | ||||||
|  |   public Collection<String> supportedTags() { | ||||||
|  |     return Collections.singleton("random-char-size"); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,52 @@ | |||||||
|  | package io.noties.markwon.app.samples.html.shared; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import org.commonmark.node.Image; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.MarkwonConfiguration; | ||||||
|  | import io.noties.markwon.RenderProps; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.tag.SimpleTagHandler; | ||||||
|  | import io.noties.markwon.image.ImageProps; | ||||||
|  | import io.noties.markwon.image.ImageSize; | ||||||
|  | 
 | ||||||
|  | public class IFrameHtmlPlugin extends AbstractMarkwonPlugin { | ||||||
|  |   @Override | ||||||
|  |   public void configure(@NonNull Registry registry) { | ||||||
|  |     registry.require(HtmlPlugin.class, htmlPlugin -> | ||||||
|  |       htmlPlugin.addHandler(new IFrameHtmlPlugin.EmbedTagHandler())); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static class EmbedTagHandler extends SimpleTagHandler { | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { | ||||||
|  |       final ImageSize imageSize = new ImageSize( | ||||||
|  |         new ImageSize.Dimension(640, "px"), | ||||||
|  |         new ImageSize.Dimension(480, "px") | ||||||
|  |       ); | ||||||
|  |       ImageProps.IMAGE_SIZE.set(renderProps, imageSize); | ||||||
|  | 
 | ||||||
|  |       ImageProps.DESTINATION.set( | ||||||
|  |         renderProps, | ||||||
|  |         "https://img1.ak.crunchyroll.com/i/spire2/d7b1d6bc7563224388ef5ffc04a967581589950464_full.jpg"); | ||||||
|  | 
 | ||||||
|  |       return configuration.spansFactory().require(Image.class) | ||||||
|  |         .getSpans(configuration, renderProps); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Collection<String> supportedTags() { | ||||||
|  |       return Collections.singleton("iframe"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,43 @@ | |||||||
|  | package io.noties.markwon.app.samples.image; | ||||||
|  | 
 | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.R; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182165828", | ||||||
|  |   title = "Image error handler", | ||||||
|  |   artifacts = MarkwonArtifact.IMAGE, | ||||||
|  |   tags = Tags.image | ||||||
|  | ) | ||||||
|  | public class ErrorImageSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       ""; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       // error handler additionally allows to log/inspect errors during image loading | ||||||
|  |       .usePlugin(ImagesPlugin.create(plugin -> | ||||||
|  |         plugin.errorHandler(new ImagesPlugin.ErrorHandler() { | ||||||
|  |           @Nullable | ||||||
|  |           @Override | ||||||
|  |           public Drawable handleError(@NonNull String url, @NonNull Throwable throwable) { | ||||||
|  |             return ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp); | ||||||
|  |           } | ||||||
|  |         }))) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,32 @@ | |||||||
|  | package io.noties.markwon.app.samples.image; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.image.gif.GifMediaDecoder; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182162214", | ||||||
|  |   title = "GIF image", | ||||||
|  |   artifacts = MarkwonArtifact.IMAGE, | ||||||
|  |   tags = {Tags.image, Tags.gif} | ||||||
|  | ) | ||||||
|  | public class GifImageSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       ""; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       // GIF is handled by default if library is used in the app | ||||||
|  | //      .usePlugin(ImagesPlugin.create()) | ||||||
|  |       .usePlugin(ImagesPlugin.create(plugin -> | ||||||
|  |         plugin.addMediaDecoder(GifMediaDecoder.create()))) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,27 @@ | |||||||
|  | package io.noties.markwon.app.samples.image; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.image.glide.GlideImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182170112", | ||||||
|  |   title = "Glide image", | ||||||
|  |   artifacts = MarkwonArtifact.IMAGE_GLIDE, | ||||||
|  |   tags = Tags.image | ||||||
|  | ) | ||||||
|  | public class GlideImageSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "[](https://www.youtube.com/watch?v=gs1I8_m4AOM)"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(GlideImagesPlugin.create(context)) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,58 @@ | |||||||
|  | package io.noties.markwon.app.samples.image; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import com.bumptech.glide.Glide; | ||||||
|  | import com.bumptech.glide.RequestBuilder; | ||||||
|  | import com.bumptech.glide.request.target.Target; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.R; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.image.AsyncDrawable; | ||||||
|  | import io.noties.markwon.image.glide.GlideImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182170241", | ||||||
|  |   title = "Glide image with placeholder", | ||||||
|  |   artifacts = MarkwonArtifact.IMAGE_GLIDE, | ||||||
|  |   tags = Tags.image | ||||||
|  | ) | ||||||
|  | public class GlidePlaceholderImageSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "[](https://www.youtube.com/watch?v=gs1I8_m4AOM)"; | ||||||
|  | 
 | ||||||
|  |     final Context context = this.context; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(GlideImagesPlugin.create(new GlideImagesPlugin.GlideStore() { | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) { | ||||||
|  | //          final Drawable placeholder = ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp); | ||||||
|  | //          placeholder.setBounds(0, 0, 100, 100); | ||||||
|  |           return Glide.with(context) | ||||||
|  |             .load(drawable.getDestination()) | ||||||
|  | //            .placeholder(ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp)); | ||||||
|  | //            .placeholder(placeholder); | ||||||
|  |             .placeholder(R.drawable.ic_home_black_36dp); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void cancel(@NonNull Target<?> target) { | ||||||
|  |           Glide.with(context) | ||||||
|  |             .clear(target); | ||||||
|  |         } | ||||||
|  |       })) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,28 @@ | |||||||
|  | package io.noties.markwon.app.samples.image; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182144659", | ||||||
|  |   title = "Markdown image", | ||||||
|  |   artifacts = MarkwonArtifact.IMAGE, | ||||||
|  |   tags = Tags.image | ||||||
|  | ) | ||||||
|  | public class ImageSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       ""; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(ImagesPlugin.create()) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,45 @@ | |||||||
|  | package io.noties.markwon.app.samples.image; | ||||||
|  | 
 | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.R; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.image.AsyncDrawable; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182165504", | ||||||
|  |   title = "Image with placeholder", | ||||||
|  |   artifacts = MarkwonArtifact.IMAGE, | ||||||
|  |   tags = Tags.image | ||||||
|  | ) | ||||||
|  | public class PlaceholderImageSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       ""; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(ImagesPlugin.create(plugin -> | ||||||
|  |         plugin.placeholderProvider(new ImagesPlugin.PlaceholderProvider() { | ||||||
|  |           @Nullable | ||||||
|  |           @Override | ||||||
|  |           public Drawable providePlaceholder(@NonNull AsyncDrawable drawable) { | ||||||
|  |             // by default drawable intrinsic size will be used | ||||||
|  |             //  otherwise bounds can be applied explicitly | ||||||
|  |             return ContextCompat.getDrawable(context, R.drawable.ic_android_black_24dp); | ||||||
|  |           } | ||||||
|  |         }))) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,37 @@ | |||||||
|  | package io.noties.markwon.app.samples.image; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.image.svg.SvgPictureMediaDecoder; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182161952", | ||||||
|  |   title = "SVG image", | ||||||
|  |   artifacts = MarkwonArtifact.IMAGE, | ||||||
|  |   tags = {Tags.image, Tags.svg} | ||||||
|  | ) | ||||||
|  | public class SvgImageSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "" + | ||||||
|  |       ""; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       // SVG and GIF are automatically handled if required | ||||||
|  |       //  libraries are in path (specified in dependencies block) | ||||||
|  | //      .usePlugin(ImagesPlugin.create()) | ||||||
|  |       // let's make it implicit | ||||||
|  |       .usePlugin(ImagesPlugin.create(plugin -> | ||||||
|  |         // there 2 svg media decoders: | ||||||
|  |         // - regular `SvgMediaDecoder` | ||||||
|  |         // - special one when SVG doesn't have width and height specified - `SvgPictureMediaDecoder` | ||||||
|  |         plugin.addMediaDecoder(SvgPictureMediaDecoder.create()))) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,77 @@ | |||||||
|  | package io.noties.markwon.app.samples.inlineparsing; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import org.commonmark.node.Block; | ||||||
|  | import org.commonmark.node.BlockQuote; | ||||||
|  | import org.commonmark.node.Heading; | ||||||
|  | import org.commonmark.node.HtmlBlock; | ||||||
|  | import org.commonmark.node.ListBlock; | ||||||
|  | import org.commonmark.node.ThematicBreak; | ||||||
|  | import org.commonmark.parser.InlineParserFactory; | ||||||
|  | import org.commonmark.parser.Parser; | ||||||
|  | 
 | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.Set; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.inlineparser.BackticksInlineProcessor; | ||||||
|  | import io.noties.markwon.inlineparser.MarkwonInlineParser; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182170607", | ||||||
|  |   title = "Disable code inline parsing", | ||||||
|  |   artifacts = MarkwonArtifact.INLINE_PARSER, | ||||||
|  |   tags = {Tags.inline, Tags.parsing} | ||||||
|  | ) | ||||||
|  | public class InlineParsingDisableCodeSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     // parses all as usual, but ignores code (inline and block) | ||||||
|  | 
 | ||||||
|  |     final String md = "# Head!\n\n" + | ||||||
|  |       "* one\n" + | ||||||
|  |       "+ two\n\n" + | ||||||
|  |       "and **bold** to `you`!\n\n" + | ||||||
|  |       "> a quote _em_\n\n" + | ||||||
|  |       "```java\n" + | ||||||
|  |       "final int i = 0;\n" + | ||||||
|  |       "```\n\n" + | ||||||
|  |       "**Good day!**"; | ||||||
|  | 
 | ||||||
|  |     final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() | ||||||
|  |       .excludeInlineProcessor(BackticksInlineProcessor.class) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     // unfortunately there is no _exclude_ method for parser-builder | ||||||
|  |     final Set<Class<? extends Block>> enabledBlocks = new HashSet<Class<? extends Block>>() {{ | ||||||
|  |       // IndentedCodeBlock.class and FencedCodeBlock.class are missing | ||||||
|  |       // this is full list (including above) that can be passed to `enabledBlockTypes` method | ||||||
|  |       addAll(Arrays.asList( | ||||||
|  |         BlockQuote.class, | ||||||
|  |         Heading.class, | ||||||
|  |         HtmlBlock.class, | ||||||
|  |         ThematicBreak.class, | ||||||
|  |         ListBlock.class)); | ||||||
|  |     }}; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @Override | ||||||
|  |         public void configureParser(@NonNull Parser.Builder builder) { | ||||||
|  |           builder | ||||||
|  |             .inlineParserFactory(inlineParserFactory) | ||||||
|  |             .enabledBlockTypes(enabledBlocks); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,55 @@ | |||||||
|  | package io.noties.markwon.app.samples.inlineparsing; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import org.commonmark.parser.InlineParserFactory; | ||||||
|  | import org.commonmark.parser.Parser; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.inlineparser.CloseBracketInlineProcessor; | ||||||
|  | import io.noties.markwon.inlineparser.MarkwonInlineParser; | ||||||
|  | import io.noties.markwon.inlineparser.OpenBracketInlineProcessor; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182170412", | ||||||
|  |   title = "Links only inline parsing", | ||||||
|  |   artifacts = MarkwonArtifact.INLINE_PARSER, | ||||||
|  |   tags = {Tags.parsing, Tags.inline} | ||||||
|  | ) | ||||||
|  | public class InlineParsingLinksOnlySample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     // note that image is considered a link now | ||||||
|  |     final String md = "**bold_bold-italic_** <u>html-u</u>, [link](#)  `code`"; | ||||||
|  | 
 | ||||||
|  |     // create an inline-parser-factory that will _ONLY_ parse links | ||||||
|  |     //  this would mean: | ||||||
|  |     //  * no emphasises (strong and regular aka bold and italics), | ||||||
|  |     //  * no images, | ||||||
|  |     //  * no code, | ||||||
|  |     //  * no HTML entities (&) | ||||||
|  |     //  * no HTML tags | ||||||
|  |     // markdown blocks are still parsed | ||||||
|  |     final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilderNoDefaults() | ||||||
|  |       .referencesEnabled(true) | ||||||
|  |       .addInlineProcessor(new OpenBracketInlineProcessor()) | ||||||
|  |       .addInlineProcessor(new CloseBracketInlineProcessor()) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @Override | ||||||
|  |         public void configureParser(@NonNull Parser.Builder builder) { | ||||||
|  |           builder.inlineParserFactory(inlineParserFactory); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,44 @@ | |||||||
|  | package io.noties.markwon.app.samples.inlineparsing; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.inlineparser.BackticksInlineProcessor; | ||||||
|  | import io.noties.markwon.inlineparser.MarkwonInlineParser; | ||||||
|  | import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182170823", | ||||||
|  |   title = "Inline parsing with defaults", | ||||||
|  |   artifacts = MarkwonArtifact.INLINE_PARSER, | ||||||
|  |   tags = {Tags.inline, Tags.parsing} | ||||||
|  | ) | ||||||
|  | public class InlineParsingNoDefaultsSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     // a plugin with NO defaults registered | ||||||
|  | 
 | ||||||
|  |     final String md = "no [links](#) for **you** `code`!"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       // pass `MarkwonInlineParser.factoryBuilderNoDefaults()` no disable all | ||||||
|  |       .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults())) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @Override | ||||||
|  |         public void configure(@NonNull Registry registry) { | ||||||
|  |           registry.require(MarkwonInlineParserPlugin.class, plugin -> { | ||||||
|  |             plugin.factoryBuilder() | ||||||
|  |               .addInlineProcessor(new BackticksInlineProcessor()); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,59 @@ | |||||||
|  | package io.noties.markwon.app.samples.inlineparsing; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import org.commonmark.node.Block; | ||||||
|  | import org.commonmark.node.HtmlBlock; | ||||||
|  | import org.commonmark.parser.Parser; | ||||||
|  | 
 | ||||||
|  | import java.util.Set; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.core.CorePlugin; | ||||||
|  | import io.noties.markwon.inlineparser.HtmlInlineProcessor; | ||||||
|  | import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182171239", | ||||||
|  |   title = "Inline parsing exclude HTML", | ||||||
|  |   artifacts = MarkwonArtifact.INLINE_PARSER, | ||||||
|  |   tags = {Tags.parsing, Tags.inline, Tags.block} | ||||||
|  | ) | ||||||
|  | public class InlineParsingNoHtmlSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     final String md = "# Html <b>disabled</b>\n\n" + | ||||||
|  |       "<em>emphasis <strong>strong</strong>\n\n" + | ||||||
|  |       "<p>paragraph <img src='hey.jpg' /></p>\n\n" + | ||||||
|  |       "<test></test>\n\n" + | ||||||
|  |       "<test>"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(MarkwonInlineParserPlugin.create()) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @Override | ||||||
|  |         public void configure(@NonNull Registry registry) { | ||||||
|  |           registry.require(MarkwonInlineParserPlugin.class, plugin -> { | ||||||
|  |             plugin.factoryBuilder() | ||||||
|  |               .excludeInlineProcessor(HtmlInlineProcessor.class); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void configureParser(@NonNull Parser.Builder builder) { | ||||||
|  |           final Set<Class<? extends Block>> blocks = CorePlugin.enabledBlockTypes(); | ||||||
|  |           blocks.remove(HtmlBlock.class); | ||||||
|  | 
 | ||||||
|  |           builder.enabledBlockTypes(blocks); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,44 @@ | |||||||
|  | package io.noties.markwon.app.samples.inlineparsing; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; | ||||||
|  | import io.noties.markwon.inlineparser.OpenBracketInlineProcessor; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006182170723", | ||||||
|  |   title = "Inline parsing with defaults", | ||||||
|  |   artifacts = MarkwonArtifact.INLINE_PARSER, | ||||||
|  |   tags = {Tags.inline, Tags.parsing} | ||||||
|  | ) | ||||||
|  | public class InlineParsingWithDefaultsSample extends MarkwonTextViewSample { | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  |     // a plugin with defaults registered | ||||||
|  | 
 | ||||||
|  |     final String md = "no [links](#) for **you** `code`!"; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(MarkwonInlineParserPlugin.create()) | ||||||
|  |       // the same as: | ||||||
|  | //       .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilder())) | ||||||
|  |       .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |         @Override | ||||||
|  |         public void configure(@NonNull Registry registry) { | ||||||
|  |           registry.require(MarkwonInlineParserPlugin.class, plugin -> { | ||||||
|  |             plugin.factoryBuilder() | ||||||
|  |               .excludeInlineProcessor(OpenBracketInlineProcessor.class); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,50 @@ | |||||||
|  | package io.noties.markwon.app.samples.plugins; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.ScrollView; | ||||||
|  | 
 | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.app.R; | ||||||
|  | import io.noties.markwon.app.sample.Tags; | ||||||
|  | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.app.samples.plugins.shared.AnchorHeadingPlugin; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
|  | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
|  | 
 | ||||||
|  | @MarkwonSampleInfo( | ||||||
|  |   id = "202006181130728", | ||||||
|  |   title = "Anchor plugin", | ||||||
|  |   description = "HTML-like anchor links plugin, which scrolls to clicked anchor", | ||||||
|  |   artifacts = MarkwonArtifact.CORE, | ||||||
|  |   tags = {Tags.links, Tags.anchor, Tags.plugin} | ||||||
|  | ) | ||||||
|  | public class AnchorSample extends MarkwonTextViewSample { | ||||||
|  | 
 | ||||||
|  |   private ScrollView scrollView; | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void onViewCreated(@NotNull View view) { | ||||||
|  |     scrollView = view.findViewById(R.id.scroll_view); | ||||||
|  |     super.onViewCreated(view); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void render() { | ||||||
|  | 
 | ||||||
|  |     final String lorem = context.getString(R.string.lorem); | ||||||
|  |     final String md = "" + | ||||||
|  |       "Hello [there](#there)!\n\n\n" + | ||||||
|  |       lorem + "\n\n" + | ||||||
|  |       "# There!\n\n" + | ||||||
|  |       lorem; | ||||||
|  | 
 | ||||||
|  |     final Markwon markwon = Markwon.builder(context) | ||||||
|  |       .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top))) | ||||||
|  |       .build(); | ||||||
|  | 
 | ||||||
|  |     markwon.setMarkdown(textView, md); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package io.noties.markwon.app.samples; | package io.noties.markwon.app.samples.plugins; | ||||||
| 
 | 
 | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.ScrollView; | import android.widget.ScrollView; | ||||||
| @ -21,6 +21,7 @@ import io.noties.markwon.MarkwonVisitor; | |||||||
| import io.noties.markwon.app.R; | import io.noties.markwon.app.R; | ||||||
| import io.noties.markwon.app.sample.Tags; | import io.noties.markwon.app.sample.Tags; | ||||||
| import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; | ||||||
|  | import io.noties.markwon.app.samples.plugins.shared.AnchorHeadingPlugin; | ||||||
| import io.noties.markwon.core.SimpleBlockNodeVisitor; | import io.noties.markwon.core.SimpleBlockNodeVisitor; | ||||||
| import io.noties.markwon.sample.annotations.MarkwonArtifact; | import io.noties.markwon.sample.annotations.MarkwonArtifact; | ||||||
| import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | import io.noties.markwon.sample.annotations.MarkwonSampleInfo; | ||||||
| @ -1,76 +1,32 @@ | |||||||
| package io.noties.markwon.app.samples; | package io.noties.markwon.app.samples.plugins.shared; | ||||||
| 
 | 
 | ||||||
| import android.text.Spannable; | import android.text.Spannable; | ||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.ScrollView; |  | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| 
 | 
 | ||||||
| import org.jetbrains.annotations.NotNull; |  | ||||||
| 
 |  | ||||||
| import io.noties.markwon.AbstractMarkwonPlugin; | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
| import io.noties.markwon.LinkResolverDef; | import io.noties.markwon.LinkResolverDef; | ||||||
| import io.noties.markwon.Markwon; |  | ||||||
| import io.noties.markwon.MarkwonConfiguration; | import io.noties.markwon.MarkwonConfiguration; | ||||||
| import io.noties.markwon.app.R; |  | ||||||
| import io.noties.markwon.app.sample.Tags; |  | ||||||
| import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; |  | ||||||
| import io.noties.markwon.core.spans.HeadingSpan; | import io.noties.markwon.core.spans.HeadingSpan; | ||||||
| import io.noties.markwon.sample.annotations.MarkwonArtifact; |  | ||||||
| import io.noties.markwon.sample.annotations.MarkwonSampleInfo; |  | ||||||
| 
 | 
 | ||||||
| @MarkwonSampleInfo( | public class AnchorHeadingPlugin extends AbstractMarkwonPlugin { | ||||||
|   id = "202006181130728", |  | ||||||
|   title = "Anchor plugin", |  | ||||||
|   description = "HTML-like anchor links plugin, which scrolls to clicked anchor", |  | ||||||
|   artifacts = MarkwonArtifact.CORE, |  | ||||||
|   tags = {Tags.links, Tags.anchor, Tags.plugin} |  | ||||||
| ) |  | ||||||
| public class AnchorSample extends MarkwonTextViewSample { |  | ||||||
| 
 |  | ||||||
|   private ScrollView scrollView; |  | ||||||
| 
 |  | ||||||
|   @Override |  | ||||||
|   public void onViewCreated(@NotNull View view) { |  | ||||||
|     scrollView = view.findViewById(R.id.scroll_view); |  | ||||||
|     super.onViewCreated(view); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @Override |  | ||||||
|   public void render() { |  | ||||||
| 
 |  | ||||||
|     final String lorem = context.getString(R.string.lorem); |  | ||||||
|     final String md = "" + |  | ||||||
|       "Hello [there](#there)!\n\n\n" + |  | ||||||
|       lorem + "\n\n" + |  | ||||||
|       "# There!\n\n" + |  | ||||||
|       lorem; |  | ||||||
| 
 |  | ||||||
|     final Markwon markwon = Markwon.builder(context) |  | ||||||
|       .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top))) |  | ||||||
|       .build(); |  | ||||||
| 
 |  | ||||||
|     markwon.setMarkdown(textView, md); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class AnchorHeadingPlugin extends AbstractMarkwonPlugin { |  | ||||||
| 
 | 
 | ||||||
|   public interface ScrollTo { |   public interface ScrollTo { | ||||||
|     void scrollTo(@NonNull TextView view, int top); |     void scrollTo(@NonNull TextView view, int top); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private final ScrollTo scrollTo; |   private final AnchorHeadingPlugin.ScrollTo scrollTo; | ||||||
| 
 | 
 | ||||||
|   AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) { |   public AnchorHeadingPlugin(@NonNull AnchorHeadingPlugin.ScrollTo scrollTo) { | ||||||
|     this.scrollTo = scrollTo; |     this.scrollTo = scrollTo; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @Override |   @Override | ||||||
|   public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { |   public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||||
|     builder.linkResolver(new AnchorLinkResolver(scrollTo)); |     builder.linkResolver(new AnchorHeadingPlugin.AnchorLinkResolver(scrollTo)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @Override |   @Override | ||||||
| @ -84,7 +40,7 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin { | |||||||
|         final int end = spannable.getSpanEnd(span); |         final int end = spannable.getSpanEnd(span); | ||||||
|         final int flags = spannable.getSpanFlags(span); |         final int flags = spannable.getSpanFlags(span); | ||||||
|         spannable.setSpan( |         spannable.setSpan( | ||||||
|           new AnchorSpan(createAnchor(spannable.subSequence(start, end))), |           new AnchorHeadingPlugin.AnchorSpan(createAnchor(spannable.subSequence(start, end))), | ||||||
|           start, |           start, | ||||||
|           end, |           end, | ||||||
|           flags |           flags | ||||||
| @ -95,9 +51,9 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin { | |||||||
| 
 | 
 | ||||||
|   private static class AnchorLinkResolver extends LinkResolverDef { |   private static class AnchorLinkResolver extends LinkResolverDef { | ||||||
| 
 | 
 | ||||||
|     private final ScrollTo scrollTo; |     private final AnchorHeadingPlugin.ScrollTo scrollTo; | ||||||
| 
 | 
 | ||||||
|     AnchorLinkResolver(@NonNull ScrollTo scrollTo) { |     AnchorLinkResolver(@NonNull AnchorHeadingPlugin.ScrollTo scrollTo) { | ||||||
|       this.scrollTo = scrollTo; |       this.scrollTo = scrollTo; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -106,10 +62,10 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin { | |||||||
|       if (link.startsWith("#")) { |       if (link.startsWith("#")) { | ||||||
|         final TextView textView = (TextView) view; |         final TextView textView = (TextView) view; | ||||||
|         final Spanned spanned = (Spannable) textView.getText(); |         final Spanned spanned = (Spannable) textView.getText(); | ||||||
|         final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class); |         final AnchorHeadingPlugin.AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorHeadingPlugin.AnchorSpan.class); | ||||||
|         if (spans != null) { |         if (spans != null) { | ||||||
|           final String anchor = link.substring(1); |           final String anchor = link.substring(1); | ||||||
|           for (AnchorSpan span : spans) { |           for (AnchorHeadingPlugin.AnchorSpan span : spans) { | ||||||
|             if (anchor.equals(span.anchor)) { |             if (anchor.equals(span.anchor)) { | ||||||
|               final int start = spanned.getSpanStart(span); |               final int start = spanned.getSpanStart(span); | ||||||
|               final int line = textView.getLayout().getLineForOffset(start); |               final int line = textView.getLayout().getLineForOffset(start); | ||||||
| @ -139,4 +95,3 @@ class AnchorHeadingPlugin extends AbstractMarkwonPlugin { | |||||||
|       .toLowerCase(); |       .toLowerCase(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| @ -4,6 +4,7 @@ import android.content.Context | |||||||
| import android.util.AttributeSet | import android.util.AttributeSet | ||||||
| import android.view.ViewGroup | import android.view.ViewGroup | ||||||
| import io.noties.markwon.app.R | import io.noties.markwon.app.R | ||||||
|  | import io.noties.markwon.app.utils.hidden | ||||||
| import kotlin.math.max | import kotlin.math.max | ||||||
| 
 | 
 | ||||||
| class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) { | class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) { | ||||||
| @ -61,6 +62,9 @@ class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, att | |||||||
| 
 | 
 | ||||||
|         for (i in 0 until childCount) { |         for (i in 0 until childCount) { | ||||||
|             val child = getChildAt(i) |             val child = getChildAt(i) | ||||||
|  |             if (child.hidden) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             // measure |             // measure | ||||||
|             child.measure(childWidthSpec, childHeightSpec) |             child.measure(childWidthSpec, childHeightSpec) | ||||||
|  | |||||||
| @ -73,6 +73,9 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context, | |||||||
|             textField.setText("") |             textField.setText("") | ||||||
|             looseFocus() |             looseFocus() | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         isSaveEnabled = false | ||||||
|  |         textField.isSaveEnabled = false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun search(text: String) { |     fun search(text: String) { | ||||||
|  | |||||||
| @ -0,0 +1,9 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |         android:width="24dp" | ||||||
|  |         android:height="24dp" | ||||||
|  |         android:viewportWidth="24.0" | ||||||
|  |         android:viewportHeight="24.0"> | ||||||
|  |     <path | ||||||
|  |         android:fillColor="#FF000000" | ||||||
|  |         android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"/> | ||||||
|  | </vector> | ||||||
							
								
								
									
										4
									
								
								app-sample/src/main/res/drawable/ic_home_black_36dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app-sample/src/main/res/drawable/ic_home_black_36dp.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | <vector android:height="36dp" android:viewportHeight="24.0" | ||||||
|  |     android:viewportWidth="24.0" android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <path android:fillColor="#FF000000" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/> | ||||||
|  | </vector> | ||||||
| @ -0,0 +1,4 @@ | |||||||
|  | <vector android:height="48dp" android:viewportHeight="24.0" | ||||||
|  |     android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <path android:fillColor="#FF000000" android:pathData="M15,9L9,9v6h6L15,9zM13,13h-2v-2h2v2zM21,11L21,9h-2L19,7c0,-1.1 -0.9,-2 -2,-2h-2L15,3h-2v2h-2L11,3L9,3v2L7,5c-1.1,0 -2,0.9 -2,2v2L3,9v2h2v2L3,13v2h2v2c0,1.1 0.9,2 2,2h2v2h2v-2h2v2h2v-2h2c1.1,0 2,-0.9 2,-2v-2h2v-2h-2v-2h2zM17,17L7,17L7,7h10v10z"/> | ||||||
|  | </vector> | ||||||
| @ -0,0 +1,6 @@ | |||||||
|  | <vector android:height="64dp" android:viewportHeight="24.0" | ||||||
|  |     android:viewportWidth="24.0" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <path android:fillColor="#FFFF0000" android:pathData="M15.5,9.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/> | ||||||
|  |     <path android:fillColor="#FFFF0000" android:pathData="M8.5,9.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/> | ||||||
|  |     <path android:fillColor="#FFFF0000" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12,16c-1.48,0 -2.75,-0.81 -3.45,-2L6.88,14c0.8,2.05 2.79,3.5 5.12,3.5s4.32,-1.45 5.12,-3.5h-1.67c-0.7,1.19 -1.97,2 -3.45,2z"/> | ||||||
|  | </vector> | ||||||
							
								
								
									
										12
									
								
								app-sample/src/main/res/layout/activity_html_details.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app-sample/src/main/res/layout/activity_html_details.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  | 
 | ||||||
|  |     <LinearLayout | ||||||
|  |         android:id="@+id/content" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:orientation="vertical" /> | ||||||
|  | 
 | ||||||
|  | </ScrollView> | ||||||
| @ -10,15 +10,22 @@ | |||||||
|         style="@style/AppBarContainer" |         style="@style/AppBarContainer" | ||||||
|         android:orientation="horizontal"> |         android:orientation="horizontal"> | ||||||
| 
 | 
 | ||||||
|         <ImageView |         <FrameLayout | ||||||
|             style="@style/AppBarIcon" |             android:layout_width="@dimen/app_bar_height" | ||||||
|             android:src="@mipmap/ic_launcher" |             android:layout_height="@dimen/app_bar_height"> | ||||||
|             tools:ignore="ContentDescription" /> | 
 | ||||||
|  |             <ImageView | ||||||
|  |                 style="@style/AppBarIcon" | ||||||
|  |                 android:src="@drawable/ic_arrow_back_white_24dp" | ||||||
|  |                 android:visibility="gone" | ||||||
|  |                 tools:ignore="ContentDescription" /> | ||||||
|  | 
 | ||||||
|  |         </FrameLayout> | ||||||
| 
 | 
 | ||||||
|         <FrameLayout |         <FrameLayout | ||||||
|             android:layout_width="match_parent" |             android:layout_width="0px" | ||||||
|             android:layout_height="match_parent" |             android:layout_height="match_parent" | ||||||
|             android:layout_marginEnd="@dimen/app_bar_height"> |             android:layout_weight="1"> | ||||||
| 
 | 
 | ||||||
|             <TextView |             <TextView | ||||||
|                 style="@style/AppBarTitle" |                 style="@style/AppBarTitle" | ||||||
| @ -26,11 +33,32 @@ | |||||||
| 
 | 
 | ||||||
|         </FrameLayout> |         </FrameLayout> | ||||||
| 
 | 
 | ||||||
|  |         <FrameLayout | ||||||
|  |             android:layout_width="@dimen/app_bar_height" | ||||||
|  |             android:layout_height="@dimen/app_bar_height"> | ||||||
|  | 
 | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/app_bar_icon_readme" | ||||||
|  |                 style="@style/AppBarIcon" | ||||||
|  |                 android:src="@mipmap/ic_launcher" | ||||||
|  |                 android:visibility="visible" | ||||||
|  |                 tools:ignore="ContentDescription" /> | ||||||
|  | 
 | ||||||
|  |         </FrameLayout> | ||||||
|  | 
 | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
| 
 | 
 | ||||||
|     <FrameLayout |     <LinearLayout | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="match_parent"> |         android:layout_height="match_parent" | ||||||
|  |         android:orientation="vertical"> | ||||||
|  | 
 | ||||||
|  |         <io.noties.markwon.app.widget.SearchBar | ||||||
|  |             android:id="@+id/search_bar" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:animateLayoutChanges="true" | ||||||
|  |             android:padding="@dimen/content_padding" /> | ||||||
| 
 | 
 | ||||||
|         <androidx.recyclerview.widget.RecyclerView |         <androidx.recyclerview.widget.RecyclerView | ||||||
|             android:id="@+id/recycler_view" |             android:id="@+id/recycler_view" | ||||||
| @ -39,17 +67,8 @@ | |||||||
|             android:clipChildren="false" |             android:clipChildren="false" | ||||||
|             android:clipToPadding="false" |             android:clipToPadding="false" | ||||||
|             android:overScrollMode="never" |             android:overScrollMode="never" | ||||||
|             android:paddingBottom="36dip" |             android:paddingBottom="36dip" /> | ||||||
|             tools:layout_marginTop="56dip" /> |  | ||||||
| 
 | 
 | ||||||
|         <io.noties.markwon.app.widget.SearchBar |     </LinearLayout> | ||||||
|             android:id="@+id/search_bar" |  | ||||||
|             android:layout_width="match_parent" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:animateLayoutChanges="true" |  | ||||||
|             android:background="@color/search_bar_background_full" |  | ||||||
|             android:padding="@dimen/content_padding" /> |  | ||||||
| 
 |  | ||||||
|     </FrameLayout> |  | ||||||
| 
 | 
 | ||||||
| </LinearLayout> | </LinearLayout> | ||||||
| @ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <TextView xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:id="@+id/text" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:padding="8dip" | ||||||
|  |     android:textAppearance="?android:attr/textAppearanceMedium" | ||||||
|  |     android:textColor="#000" /> | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov