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