diff --git a/app-sample/samples.json b/app-sample/samples.json index 7e2e2b0a..6e27d5bb 100644 --- a/app-sample/samples.json +++ b/app-sample/samples.json @@ -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", diff --git a/app-sample/src/main/java/io/noties/markwon/app/readme/ReadMeActivity.kt b/app-sample/src/main/java/io/noties/markwon/app/readme/ReadMeActivity.kt index 45143557..77d5cd59 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/readme/ReadMeActivity.kt +++ b/app-sample/src/main/java/io/noties/markwon/app/readme/ReadMeActivity.kt @@ -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))) diff --git a/app-sample/src/main/java/io/noties/markwon/app/sample/SampleItem.kt b/app-sample/src/main/java/io/noties/markwon/app/sample/SampleItem.kt index 56576f11..e06f7705 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/sample/SampleItem.kt +++ b/app-sample/src/main/java/io/noties/markwon/app/sample/SampleItem.kt @@ -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 diff --git a/app-sample/src/main/java/io/noties/markwon/app/sample/Tags.kt b/app-sample/src/main/java/io/noties/markwon/app/sample/Tags.kt index a5481ce5..35208d2a 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/sample/Tags.kt +++ b/app-sample/src/main/java/io/noties/markwon/app/sample/Tags.kt @@ -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" } \ No newline at end of file diff --git a/app-sample/src/main/java/io/noties/markwon/app/sample/ui/MarkwonEditTextSample.kt b/app-sample/src/main/java/io/noties/markwon/app/sample/ui/MarkwonEditTextSample.kt index c7fd2602..0ea55776 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/sample/ui/MarkwonEditTextSample.kt +++ b/app-sample/src/main/java/io/noties/markwon/app/sample/ui/MarkwonEditTextSample.kt @@ -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 = 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) + } + } + + } } \ No newline at end of file diff --git a/app-sample/src/main/java/io/noties/markwon/app/sample/ui/SampleListFragment.kt b/app-sample/src/main/java/io/noties/markwon/app/sample/ui/SampleListFragment.kt index 86ea2bc6..5ad6e69a 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/sample/ui/SampleListFragment.kt +++ b/app-sample/src/main/java/io/noties/markwon/app/sample/ui/SampleListFragment.kt @@ -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(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() diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/AdditionalSpacingSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/AdditionalSpacingSample.java index 80e0d7ba..6a791859 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/samples/AdditionalSpacingSample.java +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/AdditionalSpacingSample.java @@ -45,5 +45,7 @@ public class AdditionalSpacingSample extends MarkwonTextViewSample { } }) .build(); + + markwon.setMarkdown(textView, md); } } diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/ReadMorePluginSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/ReadMorePluginSample.java index acc23895..00340f84 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/samples/ReadMorePluginSample.java +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/ReadMorePluginSample.java @@ -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 diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/editor/EditorHeadingSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/editor/EditorHeadingSample.java new file mode 100644 index 00000000..ed9c34b3 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/editor/EditorHeadingSample.java @@ -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)); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/editor/shared/HeadingEditHandler.java b/app-sample/src/main/java/io/noties/markwon/app/samples/editor/shared/HeadingEditHandler.java new file mode 100644 index 00000000..990d3995 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/editor/shared/HeadingEditHandler.java @@ -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 { + + 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 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); + } + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlAlignSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlAlignSample.java new file mode 100644 index 00000000..f9e66816 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlAlignSample.java @@ -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 = "" + + "We are centered\n" + + "\n" + + "We are at the end\n" + + "\n" + + "We should be at the start\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, + 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 supportedTags() { + return Collections.singleton("align"); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlCenterTagSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlCenterTagSample.java new file mode 100644 index 00000000..0fa80c3f --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlCenterTagSample.java @@ -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 = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "

\n" + + "

LiSA's Sword Art Online: Alicization OP Song \"ADAMAS\" Certified Platinum with 250,000 Downloads

\n" + + "

\n" + + "
The upper tune was already certified Gold one month after its digital release
\n" + + "

According to The Recording Industry Association of Japan (RIAJ)'s monthly report for April 2020, one of the LiSA's 14th single songs,\n" + + " \"ADAMAS\" (the first OP theme for the TV anime Sword Art Online:\n" + + " Alicization) has been certified Platinum for\n" + + " surpassing 250,000 downloads.

\n" + + "

 

\n" + + "

As a double A-side single with \"Akai Wana (who loves it?),\" \"ADAMAS\" 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.

\n" + + "

 

\n" + + "

 

\n" + + "
\n" + + "

\"ADAMAS\" MV YouTube EDIT ver.:

\n" + + "

\n" + + "

\n" + + "

 

\n" + + "

Standard edition CD jacket:

\n" + + "

\"\"

\n" + + "
\n" + + "

  

\n" + + "
\n" + + "

 

\n" + + "

Source: RIAJ press release

\n" + + "

 

\n" + + "

©SACRA MUSIC

\n" + + "

 

\n" + + "

\n" + + "\n" + + "\n" + + ""; + + 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 supportedTags() { + return Collections.singleton("center"); + } +} + diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlDetailsSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlDetailsSample.java new file mode 100644 index 00000000..a63f44d9 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlDetailsSample.java @@ -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
\n" + + " stuff with \n\n*mark* **down**\n\n\n" + + "

\n\n" + + "\n" + + "## *formatted* **heading** with [a](link)\n" + + "```java\n" + + "code block\n" + + "```\n" + + "\n" + + "

\n" + + " nested stuff

\n" + + "\n" + + "\n" + + "* list\n" + + "* with\n" + + "\n\n" + + "![img](https://raw.githubusercontent.com/noties/Markwon/master/art/markwon_logo.png)\n\n" + + " 1. nested\n" + + " 1. items\n" + + "\n" + + " ```java\n" + + " // including code\n" + + " ```\n" + + " 1. blocks\n" + + "\n" + + "

The 3rd!\n\n" + + "**bold** _em_\n
" + + "

\n" + + "

\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 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 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 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 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 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 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); + } + } + } + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlDisableSanitizeSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlDisableSanitizeSample.java new file mode 100644 index 00000000..fe6f873f --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlDisableSanitizeSample.java @@ -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 disabled\n\n" + + "emphasis strong\n\n" + + "

paragraph

\n\n" + + "\n\n" + + ""; + + 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); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlEmptyTagReplacementSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlEmptyTagReplacementSample.java new file mode 100644 index 00000000..89262277 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlEmptyTagReplacementSample.java @@ -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 = "" + + " the `` 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); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlEnhanceSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlEnhanceSample.java new file mode 100644 index 00000000..4a12a0da --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlEnhanceSample.java @@ -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 = "" + + "This is text that must be enhanced, at least a part of it"; + + + 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 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; + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlIFrameSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlIFrameSample.java new file mode 100644 index 00000000..008681ac --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlIFrameSample.java @@ -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" + + "

\"JUMP

\n" + + "

 

\n" + + "

Switch owners will soon get to take part in the ultimate Shonen Jump rumble. Bandai Namco announced plans to bring Jump Force to Switch as Jump Force Deluxe Edition, 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 Character Pass 2 is also in the works for all versions, starting with Shoto Todoroki from My Hero Academia.

\n" + + "

 

\n" + + "

Other than Todoroki, Bandai Namco hinted that the four other Character Pass 2 characters will hail from Hunter x Hunter, Yu Yu Hakusho, Bleach, and JoJo's Bizarre Adventure. Character Pass 2 will be priced at $17.99, and Todoroki launches this spring. 

\n" + + "

 

\n" + + "

\n" + + "

 

\n" + + "

Character Pass 2 promo:

\n" + + "

 

\n" + + "

\n" + + "

 

\n" + + "

\"\"

\n" + + "

 

\n" + + "

-------

\n" + + "

Joseph Luster is the Games and Web editor at Otaku USA Magazine. You can read his webcomic, BIG DUMB FIGHTING IDIOTS at subhumanzoids. Follow him on Twitter @Moldilox. 

"; + + final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create()) + .usePlugin(HtmlPlugin.create()) + .usePlugin(new IFrameHtmlPlugin()) + .build(); + + markwon.setMarkdown(textView, md); + } +} + diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlImageSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlImageSample.java new file mode 100644 index 00000000..6bb5c286 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlImageSample.java @@ -0,0 +1,44 @@ +package io.noties.markwon.app.samples.html; + +import io.noties.markwon.Markwon; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; +import io.noties.markwon.html.HtmlPlugin; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "202006182115300", + title = "Html images", + description = "Usage of HTML images", + artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE}, + tags = {Tags.image, Tags.rendering, Tags.html} +) +public class HtmlImageSample extends MarkwonTextViewSample { + @Override + public void render() { + // treat unclosed/void `img` tag as HTML inline + final String md = "" + + "## Try CommonMark\n" + + "\n" + + "Markwon IMG:\n" + + "\n" + + "![](https://upload.wikimedia.org/wikipedia/it/thumb/c/c5/GTA_2.JPG/220px-GTA_2.JPG)\n" + + "\n" + + "New lines...\n" + + "\n" + + "HTML IMG:\n" + + "\n" + + "\n" + + "\n" + + "New lines\n\n"; + + final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create()) + .usePlugin(HtmlPlugin.create()) + .build(); + + markwon.setMarkdown(textView, md); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlRandomCharSize.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlRandomCharSize.java new file mode 100644 index 00000000..5d66efa3 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/HtmlRandomCharSize.java @@ -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 = "" + + "\n" + + "This message should have a jumpy feeling because of different sizes of characters\n" + + "\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 supportedTags() { + return Collections.singleton("random-char-size"); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/html/shared/IFrameHtmlPlugin.java b/app-sample/src/main/java/io/noties/markwon/app/samples/html/shared/IFrameHtmlPlugin.java new file mode 100644 index 00000000..146287f0 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/html/shared/IFrameHtmlPlugin.java @@ -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 supportedTags() { + return Collections.singleton("iframe"); + } + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/image/ErrorImageSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/image/ErrorImageSample.java new file mode 100644 index 00000000..2c798ed2 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/image/ErrorImageSample.java @@ -0,0 +1,43 @@ +package io.noties.markwon.app.samples.image; + +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import io.noties.markwon.Markwon; +import io.noties.markwon.app.R; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "202006182165828", + title = "Image error handler", + artifacts = MarkwonArtifact.IMAGE, + tags = Tags.image +) +public class ErrorImageSample extends MarkwonTextViewSample { + @Override + public void render() { + final String md = "" + + "![error](https://github.com/dcurtis/markdown-mark/raw/master/png/______1664x1024-solid.png)"; + + final Markwon markwon = Markwon.builder(context) + // error handler additionally allows to log/inspect errors during image loading + .usePlugin(ImagesPlugin.create(plugin -> + plugin.errorHandler(new ImagesPlugin.ErrorHandler() { + @Nullable + @Override + public Drawable handleError(@NonNull String url, @NonNull Throwable throwable) { + return ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp); + } + }))) + .build(); + + markwon.setMarkdown(textView, md); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/image/GifImageSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/image/GifImageSample.java new file mode 100644 index 00000000..7a8cad95 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/image/GifImageSample.java @@ -0,0 +1,32 @@ +package io.noties.markwon.app.samples.image; + +import io.noties.markwon.Markwon; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.image.gif.GifMediaDecoder; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "202006182162214", + title = "GIF image", + artifacts = MarkwonArtifact.IMAGE, + tags = {Tags.image, Tags.gif} +) +public class GifImageSample extends MarkwonTextViewSample { + @Override + public void render() { + final String md = "" + + "![gif-image](https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif)"; + + final Markwon markwon = Markwon.builder(context) + // GIF is handled by default if library is used in the app +// .usePlugin(ImagesPlugin.create()) + .usePlugin(ImagesPlugin.create(plugin -> + plugin.addMediaDecoder(GifMediaDecoder.create()))) + .build(); + + markwon.setMarkdown(textView, md); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/image/GlideImageSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/image/GlideImageSample.java new file mode 100644 index 00000000..c46b01af --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/image/GlideImageSample.java @@ -0,0 +1,27 @@ +package io.noties.markwon.app.samples.image; + +import io.noties.markwon.Markwon; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; +import io.noties.markwon.image.glide.GlideImagesPlugin; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "202006182170112", + title = "Glide image", + artifacts = MarkwonArtifact.IMAGE_GLIDE, + tags = Tags.image +) +public class GlideImageSample extends MarkwonTextViewSample { + @Override + public void render() { + final String md = "[![undefined](https://img.youtube.com/vi/gs1I8_m4AOM/0.jpg)](https://www.youtube.com/watch?v=gs1I8_m4AOM)"; + + final Markwon markwon = Markwon.builder(context) + .usePlugin(GlideImagesPlugin.create(context)) + .build(); + + markwon.setMarkdown(textView, md); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/image/GlidePlaceholderImageSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/image/GlidePlaceholderImageSample.java new file mode 100644 index 00000000..843e1920 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/image/GlidePlaceholderImageSample.java @@ -0,0 +1,58 @@ +package io.noties.markwon.app.samples.image; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.request.target.Target; + +import io.noties.markwon.Markwon; +import io.noties.markwon.app.R; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; +import io.noties.markwon.image.AsyncDrawable; +import io.noties.markwon.image.glide.GlideImagesPlugin; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "202006182170241", + title = "Glide image with placeholder", + artifacts = MarkwonArtifact.IMAGE_GLIDE, + tags = Tags.image +) +public class GlidePlaceholderImageSample extends MarkwonTextViewSample { + @Override + public void render() { + final String md = "[![undefined](https://img.youtube.com/vi/gs1I8_m4AOM/0.jpg)](https://www.youtube.com/watch?v=gs1I8_m4AOM)"; + + final Context context = this.context; + + final Markwon markwon = Markwon.builder(context) + .usePlugin(GlideImagesPlugin.create(new GlideImagesPlugin.GlideStore() { + @NonNull + @Override + public RequestBuilder 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); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/image/ImageSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/image/ImageSample.java new file mode 100644 index 00000000..9ff22cca --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/image/ImageSample.java @@ -0,0 +1,28 @@ +package io.noties.markwon.app.samples.image; + +import io.noties.markwon.Markwon; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "202006182144659", + title = "Markdown image", + artifacts = MarkwonArtifact.IMAGE, + tags = Tags.image +) +public class ImageSample extends MarkwonTextViewSample { + @Override + public void render() { + final String md = "" + + "![image](https://github.com/dcurtis/markdown-mark/raw/master/png/208x128-solid.png)"; + + final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create()) + .build(); + + markwon.setMarkdown(textView, md); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/image/PlaceholderImageSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/image/PlaceholderImageSample.java new file mode 100644 index 00000000..c9195799 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/image/PlaceholderImageSample.java @@ -0,0 +1,45 @@ +package io.noties.markwon.app.samples.image; + +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import io.noties.markwon.Markwon; +import io.noties.markwon.app.R; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; +import io.noties.markwon.image.AsyncDrawable; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "202006182165504", + title = "Image with placeholder", + artifacts = MarkwonArtifact.IMAGE, + tags = Tags.image +) +public class PlaceholderImageSample extends MarkwonTextViewSample { + @Override + public void render() { + final String md = "" + + "![image](https://github.com/dcurtis/markdown-mark/raw/master/png/1664x1024-solid.png)"; + + final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create(plugin -> + plugin.placeholderProvider(new ImagesPlugin.PlaceholderProvider() { + @Nullable + @Override + public Drawable providePlaceholder(@NonNull AsyncDrawable drawable) { + // by default drawable intrinsic size will be used + // otherwise bounds can be applied explicitly + return ContextCompat.getDrawable(context, R.drawable.ic_android_black_24dp); + } + }))) + .build(); + + markwon.setMarkdown(textView, md); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/image/SvgImageSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/image/SvgImageSample.java new file mode 100644 index 00000000..c18709d7 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/image/SvgImageSample.java @@ -0,0 +1,37 @@ +package io.noties.markwon.app.samples.image; + +import io.noties.markwon.Markwon; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.image.svg.SvgPictureMediaDecoder; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "202006182161952", + title = "SVG image", + artifacts = MarkwonArtifact.IMAGE, + tags = {Tags.image, Tags.svg} +) +public class SvgImageSample extends MarkwonTextViewSample { + @Override + public void render() { + final String md = "" + + "![svg-image](https://github.com/dcurtis/markdown-mark/raw/master/svg/markdown-mark-solid.svg)"; + + final Markwon markwon = Markwon.builder(context) + // SVG and GIF are automatically handled if required + // libraries are in path (specified in dependencies block) +// .usePlugin(ImagesPlugin.create()) + // let's make it implicit + .usePlugin(ImagesPlugin.create(plugin -> + // there 2 svg media decoders: + // - regular `SvgMediaDecoder` + // - special one when SVG doesn't have width and height specified - `SvgPictureMediaDecoder` + plugin.addMediaDecoder(SvgPictureMediaDecoder.create()))) + .build(); + + markwon.setMarkdown(textView, md); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingDisableCodeSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingDisableCodeSample.java new file mode 100644 index 00000000..725e594f --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingDisableCodeSample.java @@ -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> enabledBlocks = new HashSet>() {{ + // 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); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingLinksOnlySample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingLinksOnlySample.java new file mode 100644 index 00000000..24f0cc95 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingLinksOnlySample.java @@ -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_** html-u, [link](#) ![alt](#image) `code`"; + + // create an inline-parser-factory that will _ONLY_ parse links + // this would mean: + // * no emphasises (strong and regular aka bold and italics), + // * no images, + // * no code, + // * no HTML entities (&) + // * 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); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingNoDefaultsSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingNoDefaultsSample.java new file mode 100644 index 00000000..7347c130 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingNoDefaultsSample.java @@ -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); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingNoHtmlSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingNoHtmlSample.java new file mode 100644 index 00000000..fdcf7885 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingNoHtmlSample.java @@ -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 disabled\n\n" + + "emphasis strong\n\n" + + "

paragraph

\n\n" + + "\n\n" + + ""; + + 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> blocks = CorePlugin.enabledBlockTypes(); + blocks.remove(HtmlBlock.class); + + builder.enabledBlockTypes(blocks); + } + }) + .build(); + + markwon.setMarkdown(textView, md); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingWithDefaultsSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingWithDefaultsSample.java new file mode 100644 index 00000000..a9fabd73 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/inlineparsing/InlineParsingWithDefaultsSample.java @@ -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); + } +} diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/plugins/AnchorSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/plugins/AnchorSample.java new file mode 100644 index 00000000..5b0d555a --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/plugins/AnchorSample.java @@ -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); + } +} + diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/TableOfContentsSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/plugins/TableOfContentsSample.java similarity index 97% rename from app-sample/src/main/java/io/noties/markwon/app/samples/TableOfContentsSample.java rename to app-sample/src/main/java/io/noties/markwon/app/samples/plugins/TableOfContentsSample.java index 04b2db63..35f5ee66 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/samples/TableOfContentsSample.java +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/plugins/TableOfContentsSample.java @@ -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; diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/AnchorSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/plugins/shared/AnchorHeadingPlugin.java similarity index 55% rename from app-sample/src/main/java/io/noties/markwon/app/samples/AnchorSample.java rename to app-sample/src/main/java/io/noties/markwon/app/samples/plugins/shared/AnchorHeadingPlugin.java index a58b5840..4663b4cb 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/samples/AnchorSample.java +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/plugins/shared/AnchorHeadingPlugin.java @@ -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(); } } - diff --git a/app-sample/src/main/java/io/noties/markwon/app/widget/FlowLayout.kt b/app-sample/src/main/java/io/noties/markwon/app/widget/FlowLayout.kt index a975f453..aa82b137 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/widget/FlowLayout.kt +++ b/app-sample/src/main/java/io/noties/markwon/app/widget/FlowLayout.kt @@ -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) diff --git a/app-sample/src/main/java/io/noties/markwon/app/widget/SearchBar.kt b/app-sample/src/main/java/io/noties/markwon/app/widget/SearchBar.kt index 5c7508a1..7999fac4 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/widget/SearchBar.kt +++ b/app-sample/src/main/java/io/noties/markwon/app/widget/SearchBar.kt @@ -73,6 +73,9 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context, textField.setText("") looseFocus() } + + isSaveEnabled = false + textField.isSaveEnabled = false } fun search(text: String) { diff --git a/app-sample/src/main/res/drawable/ic_android_black_24dp.xml b/app-sample/src/main/res/drawable/ic_android_black_24dp.xml new file mode 100644 index 00000000..401cbf63 --- /dev/null +++ b/app-sample/src/main/res/drawable/ic_android_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app-sample/src/main/res/drawable/ic_home_black_36dp.xml b/app-sample/src/main/res/drawable/ic_home_black_36dp.xml new file mode 100644 index 00000000..c3b7e150 --- /dev/null +++ b/app-sample/src/main/res/drawable/ic_home_black_36dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app-sample/src/main/res/drawable/ic_memory_black_48dp.xml b/app-sample/src/main/res/drawable/ic_memory_black_48dp.xml new file mode 100644 index 00000000..88ac2954 --- /dev/null +++ b/app-sample/src/main/res/drawable/ic_memory_black_48dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app-sample/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml b/app-sample/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml new file mode 100644 index 00000000..f9c60705 --- /dev/null +++ b/app-sample/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app-sample/src/main/res/layout/activity_html_details.xml b/app-sample/src/main/res/layout/activity_html_details.xml new file mode 100644 index 00000000..7a61f5d8 --- /dev/null +++ b/app-sample/src/main/res/layout/activity_html_details.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app-sample/src/main/res/layout/fragment_sample_list.xml b/app-sample/src/main/res/layout/fragment_sample_list.xml index 18dacbac..fe2cf16d 100644 --- a/app-sample/src/main/res/layout/fragment_sample_list.xml +++ b/app-sample/src/main/res/layout/fragment_sample_list.xml @@ -10,15 +10,22 @@ style="@style/AppBarContainer" android:orientation="horizontal"> - + + + + + + android:layout_weight="1"> + + + + + + - + android:layout_height="match_parent" + android:orientation="vertical"> + + + android:paddingBottom="36dip" /> - - - + \ No newline at end of file diff --git a/app-sample/src/main/res/layout/view_html_details_text_view.xml b/app-sample/src/main/res/layout/view_html_details_text_view.xml new file mode 100644 index 00000000..36775ea9 --- /dev/null +++ b/app-sample/src/main/res/layout/view_html_details_text_view.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file