Sample app, adding moving samples
This commit is contained in:
parent
25740d7389
commit
bc58790704
@ -1,4 +1,401 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.NoParsingSample",
|
||||||
|
"id": "202006181171212",
|
||||||
|
"title": "No parsing",
|
||||||
|
"description": "All commonmark parsing is disabled (both inlines and blocks)",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"parsing",
|
||||||
|
"rendering"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.InlinePluginNoDefaultsSample",
|
||||||
|
"id": "202006181170857",
|
||||||
|
"title": "Inline parsing without defaults",
|
||||||
|
"description": "Configure inline parser plugin to **not** have any **inline** parsing",
|
||||||
|
"artifacts": [
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"parsing"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.editor.EditorNewLineContinuationSample",
|
||||||
|
"id": "202006181170348",
|
||||||
|
"title": "Editor new line continuation",
|
||||||
|
"description": "Sample of how new line character can be handled in order to add a _continuation_, for example adding a new bullet list item if current line starts with one",
|
||||||
|
"artifacts": [
|
||||||
|
"EDITOR",
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"editor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.editor.EditorMultipleEditSpansSample",
|
||||||
|
"id": "202006181165920",
|
||||||
|
"title": "Multiple edit spans",
|
||||||
|
"description": "Additional multiple edit spans for editor",
|
||||||
|
"artifacts": [
|
||||||
|
"EDITOR",
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"editor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.editor.EditorAdditionalPluginSample",
|
||||||
|
"id": "202006181165347",
|
||||||
|
"title": "Additional plugin",
|
||||||
|
"description": "Additional plugin for editor",
|
||||||
|
"artifacts": [
|
||||||
|
"EDITOR",
|
||||||
|
"EXT_STRIKETHROUGH",
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"editor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.editor.EditorAdditionalEditSpan",
|
||||||
|
"id": "202006181165136",
|
||||||
|
"title": "Additional edit span",
|
||||||
|
"description": "Additional _edit_ span (span that is present in `EditText` along with punctuation",
|
||||||
|
"artifacts": [
|
||||||
|
"EDITOR",
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"editor",
|
||||||
|
"span"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.editor.EditorCustomPunctuationSample",
|
||||||
|
"id": "202006181164627",
|
||||||
|
"title": "Custom punctuation span",
|
||||||
|
"description": "Custom span for punctuation in editor",
|
||||||
|
"artifacts": [
|
||||||
|
"EDITOR",
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"editor",
|
||||||
|
"span"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.editor.EditorPreRenderSample",
|
||||||
|
"id": "202006181164422",
|
||||||
|
"title": "Editor with pre-render (async)",
|
||||||
|
"description": "Editor functionality with highlight taking place in another thread",
|
||||||
|
"artifacts": [
|
||||||
|
"EDITOR",
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"editor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.editor.EditorSimpleSample",
|
||||||
|
"id": "202006181164227",
|
||||||
|
"title": "Simple editor",
|
||||||
|
"description": "Simple usage of editor with markdown highlight",
|
||||||
|
"artifacts": [
|
||||||
|
"EDITOR",
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"editor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.CustomExtensionSample",
|
||||||
|
"id": "202006181163248",
|
||||||
|
"title": "Custom extension",
|
||||||
|
"description": "Custom extension that adds an icon from resources and renders it as image with `@ic-name` syntax",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"extension",
|
||||||
|
"image",
|
||||||
|
"parsing",
|
||||||
|
"plugin",
|
||||||
|
"rendering",
|
||||||
|
"span"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"INLINE_PARSER"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"parsing",
|
||||||
|
"rendering",
|
||||||
|
"text-added-listener"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"parsing",
|
||||||
|
"rendering",
|
||||||
|
"text-added-listener"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.ReadMorePluginSample",
|
||||||
|
"id": "202006181161505",
|
||||||
|
"title": "Read more plugin",
|
||||||
|
"description": "Plugin that adds expand/collapse (\"show all\"/\"show less\")",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.TableOfContentsSample",
|
||||||
|
"id": "202006181161226",
|
||||||
|
"title": "Table of contents",
|
||||||
|
"description": "Sample plugin that adds a table of contents header",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin",
|
||||||
|
"rendering"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.LetterOrderedListSample",
|
||||||
|
"id": "202006181130954",
|
||||||
|
"title": "Letter ordered list",
|
||||||
|
"description": "Render bullet list inside an ordered list with letters instead of bullets",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"lists",
|
||||||
|
"plugin",
|
||||||
|
"rendering"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.AnchorSample",
|
||||||
|
"id": "202006181130728",
|
||||||
|
"title": "Anchor plugin",
|
||||||
|
"description": "HTML-like anchor links plugin, which scrolls to clicked anchor",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"anchor",
|
||||||
|
"links",
|
||||||
|
"plugin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.AllBlocksNoForcedNewLineSample",
|
||||||
|
"id": "202006181130227",
|
||||||
|
"title": "All blocks no padding",
|
||||||
|
"description": "Do not render new lines (padding) after all blocks",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"block",
|
||||||
|
"padding",
|
||||||
|
"rendering",
|
||||||
|
"spacing"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.HeadingNoSpaceBlockHandlerSample",
|
||||||
|
"id": "202006181125924",
|
||||||
|
"title": "Heading no padding (block handler)",
|
||||||
|
"description": "Process padding (spacing) after heading with a `BlockHandler`",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"block",
|
||||||
|
"heading",
|
||||||
|
"padding",
|
||||||
|
"rendering",
|
||||||
|
"spacing"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.HeadingNoSpaceSample",
|
||||||
|
"id": "202006181125622",
|
||||||
|
"title": "Heading no padding",
|
||||||
|
"description": "Do not add a new line after heading node",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"padding",
|
||||||
|
"rendering",
|
||||||
|
"spacing"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.AdditionalSpacingSample",
|
||||||
|
"id": "202006181125321",
|
||||||
|
"title": "Additional spacing after block",
|
||||||
|
"description": "Add additional spacing (padding) after last line of a block",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"padding",
|
||||||
|
"spacing",
|
||||||
|
"span"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.SoftBreakAddsNewLineSample",
|
||||||
|
"id": "202006181125040",
|
||||||
|
"title": "Soft break new line",
|
||||||
|
"description": "Add a new line for a markdown soft-break node",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"new-line",
|
||||||
|
"soft-break"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.SoftBreakAddsSpace",
|
||||||
|
"id": "202006181124706",
|
||||||
|
"title": "Soft break adds space",
|
||||||
|
"description": "By default a soft break (`\n`) will add a space character instead of new line",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"defaults",
|
||||||
|
"new-line",
|
||||||
|
"soft-break"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.ImagesCustomSchemeSample",
|
||||||
|
"id": "202006181124201",
|
||||||
|
"title": "Image destination custom scheme",
|
||||||
|
"description": "Example of handling custom scheme (`https`, `ftp`, `whatever`, etc.) for images destination URLs with `ImagesPlugin`",
|
||||||
|
"artifacts": [
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.LinkWithoutSchemeSample",
|
||||||
|
"id": "202006181124005",
|
||||||
|
"title": "Links without scheme",
|
||||||
|
"description": "Links without scheme are considered to be `https`",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"defaults",
|
||||||
|
"links"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.CustomizeThemeSample",
|
||||||
|
"id": "202006181123617",
|
||||||
|
"title": "Customize theme",
|
||||||
|
"description": "Customize `MarkwonTheme` styling",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin",
|
||||||
|
"style",
|
||||||
|
"theme"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.DisableNodeSample",
|
||||||
|
"id": "202006181123308",
|
||||||
|
"title": "Disable node from rendering",
|
||||||
|
"description": "Disable _parsed_ node from being rendered (markdown syntax is still consumed)",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"parsing",
|
||||||
|
"rendering"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.ParagraphSpanStyle",
|
||||||
|
"id": "202006181122647",
|
||||||
|
"title": "Paragraph style",
|
||||||
|
"description": "Apply a style (via span) to a paragraph",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"paragraph",
|
||||||
|
"span",
|
||||||
|
"style"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.LinkTitleSample",
|
||||||
|
"id": "202006181122230",
|
||||||
|
"title": "Obtain link title",
|
||||||
|
"description": "Obtain title (text) of clicked link, `[title](#destination)`",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"links",
|
||||||
|
"span"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.movementmethod.DisableImplicitMovementMethodPluginSample",
|
||||||
|
"id": "202006181121803",
|
||||||
|
"title": "Disable implicit movement method via plugin",
|
||||||
|
"description": "Disable implicit movement method via `MovementMethodPlugin`",
|
||||||
|
"artifacts": [
|
||||||
|
"CORE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"links",
|
||||||
|
"movement-method",
|
||||||
|
"recycler-view"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"javaClassName": "io.noties.markwon.app.samples.movementmethod.MovementMethodPluginSample",
|
"javaClassName": "io.noties.markwon.app.samples.movementmethod.MovementMethodPluginSample",
|
||||||
"id": "202006179081631",
|
"id": "202006179081631",
|
||||||
@ -17,7 +414,7 @@
|
|||||||
"javaClassName": "io.noties.markwon.app.samples.movementmethod.DisableImplicitMovementMethodSample",
|
"javaClassName": "io.noties.markwon.app.samples.movementmethod.DisableImplicitMovementMethodSample",
|
||||||
"id": "202006179081256",
|
"id": "202006179081256",
|
||||||
"title": "Disable implicit movement method",
|
"title": "Disable implicit movement method",
|
||||||
"description": "Configure `Markwon` to **not** apply implicit movement method",
|
"description": "Configure `Markwon` to **not** apply implicit movement method, which consumes touch events when used in a `RecyclerView` even when markdown does not contain links",
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
"CORE"
|
"CORE"
|
||||||
],
|
],
|
||||||
@ -50,6 +447,7 @@
|
|||||||
"CORE"
|
"CORE"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
|
"defaults",
|
||||||
"links",
|
"links",
|
||||||
"movement-method"
|
"movement-method"
|
||||||
]
|
]
|
||||||
|
@ -10,4 +10,21 @@ object Tags {
|
|||||||
const val links = "links"
|
const val links = "links"
|
||||||
const val plugin = "plugin"
|
const val plugin = "plugin"
|
||||||
const val recyclerView = "recycler-view"
|
const val recyclerView = "recycler-view"
|
||||||
|
const val paragraph = "paragraph"
|
||||||
|
const val rendering = "rendering"
|
||||||
|
const val style = "style"
|
||||||
|
const val theme = "theme"
|
||||||
|
const val image = "image"
|
||||||
|
const val newLine = "new-line"
|
||||||
|
const val softBreak = "soft-break"
|
||||||
|
const val defaults = "defaults"
|
||||||
|
const val spacing = "spacing"
|
||||||
|
const val padding = "padding"
|
||||||
|
const val heading = "heading"
|
||||||
|
const val anchor = "anchor"
|
||||||
|
const val lists = "lists"
|
||||||
|
const val extension = "extension"
|
||||||
|
const val textAddedListener = "text-added-listener"
|
||||||
|
const val editor = "editor"
|
||||||
|
const val span = "span"
|
||||||
}
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package io.noties.markwon.app.sample.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import io.noties.markwon.app.R
|
||||||
|
|
||||||
|
abstract class MarkwonEditTextSample: MarkwonSample() {
|
||||||
|
|
||||||
|
protected lateinit var context: Context
|
||||||
|
protected lateinit var editText: EditText
|
||||||
|
|
||||||
|
override val layoutResId: Int
|
||||||
|
get() = R.layout.activity_edit_text
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
context = view.context
|
||||||
|
editText = view.findViewById(R.id.edit_text)
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun render()
|
||||||
|
}
|
@ -13,7 +13,7 @@ abstract class MarkwonTextViewSample : MarkwonSample() {
|
|||||||
|
|
||||||
override val layoutResId: Int = R.layout.sample_text_view
|
override val layoutResId: Int = R.layout.sample_text_view
|
||||||
|
|
||||||
final override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
context = view.context
|
context = view.context
|
||||||
textView = view.findViewById(R.id.text_view)
|
textView = view.findViewById(R.id.text_view)
|
||||||
render()
|
render()
|
||||||
|
@ -20,15 +20,15 @@ import io.noties.markwon.Markwon
|
|||||||
import io.noties.markwon.app.App
|
import io.noties.markwon.app.App
|
||||||
import io.noties.markwon.app.R
|
import io.noties.markwon.app.R
|
||||||
import io.noties.markwon.app.sample.Sample
|
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.SampleManager
|
||||||
import io.noties.markwon.app.sample.SampleSearch
|
import io.noties.markwon.app.sample.SampleSearch
|
||||||
import io.noties.markwon.app.sample.SampleItem
|
|
||||||
import io.noties.markwon.app.widget.SearchBar
|
|
||||||
import io.noties.markwon.app.utils.Cancellable
|
import io.noties.markwon.app.utils.Cancellable
|
||||||
import io.noties.markwon.app.utils.displayName
|
import io.noties.markwon.app.utils.displayName
|
||||||
import io.noties.markwon.app.utils.onPreDraw
|
import io.noties.markwon.app.utils.onPreDraw
|
||||||
import io.noties.markwon.app.utils.recyclerView
|
import io.noties.markwon.app.utils.recyclerView
|
||||||
import io.noties.markwon.app.utils.tagDisplayName
|
import io.noties.markwon.app.utils.tagDisplayName
|
||||||
|
import io.noties.markwon.app.widget.SearchBar
|
||||||
import io.noties.markwon.movement.MovementMethodPlugin
|
import io.noties.markwon.movement.MovementMethodPlugin
|
||||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
@ -91,6 +91,9 @@ class SampleListFragment : Fragment() {
|
|||||||
recyclerView.paddingRight,
|
recyclerView.paddingRight,
|
||||||
recyclerView.paddingBottom
|
recyclerView.paddingBottom
|
||||||
)
|
)
|
||||||
|
recyclerView.post {
|
||||||
|
recyclerView.scrollToPosition(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val state: State? = savedInstanceState?.getParcelable(STATE)
|
val state: State? = savedInstanceState?.getParcelable(STATE)
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.Heading;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonSpansFactory;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||||
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
|
import io.noties.markwon.core.spans.LastLineSpacingSpan;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181125321",
|
||||||
|
title = "Additional spacing after block",
|
||||||
|
description = "Add additional spacing (padding) after last line of a block",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.spacing, Tags.padding, Tags.span}
|
||||||
|
)
|
||||||
|
public class AdditionalSpacingSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Title title title title title title title title title title \n\ntext text text text";
|
||||||
|
|
||||||
|
// please note that bottom line (after 1 & 2 levels) will be drawn _AFTER_ padding
|
||||||
|
final int spacing = (int) (128 * context.getResources().getDisplayMetrics().density + .5F);
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||||
|
builder.headingBreakHeight(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||||
|
builder.appendFactory(
|
||||||
|
Heading.class,
|
||||||
|
(configuration, props) -> new LastLineSpacingSpan(spacing));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.BlockHandlerDef;
|
||||||
|
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.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181130227",
|
||||||
|
title = "All blocks no padding",
|
||||||
|
description = "Do not render new lines (padding) after all blocks",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.block, Tags.spacing, Tags.padding, Tags.rendering}
|
||||||
|
)
|
||||||
|
public class AllBlocksNoForcedNewLineSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Hello there!\n\n" +
|
||||||
|
"* a first\n" +
|
||||||
|
"* second\n" +
|
||||||
|
"- third\n" +
|
||||||
|
"* * nested one\n\n" +
|
||||||
|
"> block quote\n\n" +
|
||||||
|
"> > and nested one\n\n" +
|
||||||
|
"```java\n" +
|
||||||
|
"final int i = 0;\n" +
|
||||||
|
"```\n\n";
|
||||||
|
|
||||||
|
// extend default block handler
|
||||||
|
final MarkwonVisitor.BlockHandler blockHandler = new BlockHandlerDef() {
|
||||||
|
@Override
|
||||||
|
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||||
|
if (visitor.hasNext(node)) {
|
||||||
|
visitor.ensureNewLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
|
builder.blockHandler(blockHandler);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
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 interface ScrollTo {
|
||||||
|
void scrollTo(@NonNull TextView view, int top);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ScrollTo scrollTo;
|
||||||
|
|
||||||
|
AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) {
|
||||||
|
this.scrollTo = scrollTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||||
|
builder.linkResolver(new AnchorLinkResolver(scrollTo));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterSetText(@NonNull TextView textView) {
|
||||||
|
final Spannable spannable = (Spannable) textView.getText();
|
||||||
|
// obtain heading spans
|
||||||
|
final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class);
|
||||||
|
if (spans != null) {
|
||||||
|
for (HeadingSpan span : spans) {
|
||||||
|
final int start = spannable.getSpanStart(span);
|
||||||
|
final int end = spannable.getSpanEnd(span);
|
||||||
|
final int flags = spannable.getSpanFlags(span);
|
||||||
|
spannable.setSpan(
|
||||||
|
new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
flags
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AnchorLinkResolver extends LinkResolverDef {
|
||||||
|
|
||||||
|
private final ScrollTo scrollTo;
|
||||||
|
|
||||||
|
AnchorLinkResolver(@NonNull ScrollTo scrollTo) {
|
||||||
|
this.scrollTo = scrollTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resolve(@NonNull View view, @NonNull String link) {
|
||||||
|
if (link.startsWith("#")) {
|
||||||
|
final TextView textView = (TextView) view;
|
||||||
|
final Spanned spanned = (Spannable) textView.getText();
|
||||||
|
final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class);
|
||||||
|
if (spans != null) {
|
||||||
|
final String anchor = link.substring(1);
|
||||||
|
for (AnchorSpan span : spans) {
|
||||||
|
if (anchor.equals(span.anchor)) {
|
||||||
|
final int start = spanned.getSpanStart(span);
|
||||||
|
final int line = textView.getLayout().getLineForOffset(start);
|
||||||
|
final int top = textView.getLayout().getLineTop(line);
|
||||||
|
scrollTo.scrollTo(textView, top);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.resolve(view, link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AnchorSpan {
|
||||||
|
final String anchor;
|
||||||
|
|
||||||
|
AnchorSpan(@NonNull String anchor) {
|
||||||
|
this.anchor = anchor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String createAnchor(@NonNull CharSequence content) {
|
||||||
|
return String.valueOf(content)
|
||||||
|
.replaceAll("[^\\w]", "")
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,436 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.ReplacementSpan;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.node.CustomNode;
|
||||||
|
import org.commonmark.node.Delimited;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.node.Text;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
||||||
|
import org.commonmark.parser.delimiter.DelimiterRun;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
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.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181163248",
|
||||||
|
title = "Custom extension",
|
||||||
|
description = "Custom extension that adds an " +
|
||||||
|
"icon from resources and renders it as image with " +
|
||||||
|
"`@ic-name` syntax",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.parsing, Tags.rendering, Tags.plugin, Tags.image, Tags.extension, Tags.span}
|
||||||
|
)
|
||||||
|
public class CustomExtensionSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Hello! @ic-android-black-24\n\n" +
|
||||||
|
"" +
|
||||||
|
"Home 36 black: @ic-home-black-36\n\n" +
|
||||||
|
"" +
|
||||||
|
"Memory 48 black: @ic-memory-black-48\n\n" +
|
||||||
|
"" +
|
||||||
|
"### I AM ANOTHER HEADER\n\n" +
|
||||||
|
"" +
|
||||||
|
"Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
// note that we haven't registered CorePlugin, as it's the only one that can be
|
||||||
|
// implicitly deducted and added automatically. All other plugins require explicit
|
||||||
|
// `usePlugin` call
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(IconPlugin.create(IconSpanProvider.create(context, 0)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IconPlugin extends AbstractMarkwonPlugin {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static IconPlugin create(@NonNull IconSpanProvider iconSpanProvider) {
|
||||||
|
return new IconPlugin(iconSpanProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final IconSpanProvider iconSpanProvider;
|
||||||
|
|
||||||
|
IconPlugin(@NonNull IconSpanProvider iconSpanProvider) {
|
||||||
|
this.iconSpanProvider = iconSpanProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
builder.customDelimiterProcessor(IconProcessor.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
|
builder.on(IconNode.class, (visitor, iconNode) -> {
|
||||||
|
|
||||||
|
final String name = iconNode.name();
|
||||||
|
final String color = iconNode.color();
|
||||||
|
final String size = iconNode.size();
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(name)
|
||||||
|
&& !TextUtils.isEmpty(color)
|
||||||
|
&& !TextUtils.isEmpty(size)) {
|
||||||
|
|
||||||
|
final int length = visitor.length();
|
||||||
|
|
||||||
|
visitor.builder().append(name);
|
||||||
|
visitor.setSpans(length, iconSpanProvider.provide(name, color, size));
|
||||||
|
visitor.builder().append(' ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String processMarkdown(@NonNull String markdown) {
|
||||||
|
return IconProcessor.prepare(markdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class IconSpanProvider {
|
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
@NonNull
|
||||||
|
public static IconSpanProvider create(@NonNull Context context, @DrawableRes int fallBack) {
|
||||||
|
return new Impl(context, fallBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public abstract IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size);
|
||||||
|
|
||||||
|
|
||||||
|
private static class Impl extends IconSpanProvider {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final Resources resources;
|
||||||
|
private final int fallBack;
|
||||||
|
|
||||||
|
Impl(@NonNull Context context, @DrawableRes int fallBack) {
|
||||||
|
this.context = context;
|
||||||
|
this.resources = context.getResources();
|
||||||
|
this.fallBack = fallBack;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size) {
|
||||||
|
final String resName = iconName(name, color, size);
|
||||||
|
int resId = resources.getIdentifier(resName, "drawable", context.getPackageName());
|
||||||
|
if (resId == 0) {
|
||||||
|
resId = fallBack;
|
||||||
|
}
|
||||||
|
return new IconSpan(getDrawable(resId), IconSpan.ALIGN_CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String iconName(@NonNull String name, @NonNull String color, @NonNull String size) {
|
||||||
|
return "ic_" + name + "_" + color + "_" + size + "dp";
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Drawable getDrawable(int resId) {
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
return context.getDrawable(resId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IconSpan extends ReplacementSpan {
|
||||||
|
|
||||||
|
@IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER})
|
||||||
|
@Retention(RetentionPolicy.CLASS)
|
||||||
|
@interface Alignment {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int ALIGN_BOTTOM = 0;
|
||||||
|
public static final int ALIGN_BASELINE = 1;
|
||||||
|
public static final int ALIGN_CENTER = 2; // will only center if drawable height is less than text line height
|
||||||
|
|
||||||
|
|
||||||
|
private final Drawable drawable;
|
||||||
|
|
||||||
|
private final int alignment;
|
||||||
|
|
||||||
|
public IconSpan(@NonNull Drawable drawable, @Alignment int alignment) {
|
||||||
|
this.drawable = drawable;
|
||||||
|
this.alignment = alignment;
|
||||||
|
if (drawable.getBounds().isEmpty()) {
|
||||||
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
|
||||||
|
|
||||||
|
final Rect rect = drawable.getBounds();
|
||||||
|
|
||||||
|
if (fm != null) {
|
||||||
|
fm.ascent = -rect.bottom;
|
||||||
|
fm.descent = 0;
|
||||||
|
|
||||||
|
fm.top = fm.ascent;
|
||||||
|
fm.bottom = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rect.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
|
||||||
|
|
||||||
|
final int b = bottom - drawable.getBounds().bottom;
|
||||||
|
|
||||||
|
final int save = canvas.save();
|
||||||
|
try {
|
||||||
|
final int translationY;
|
||||||
|
if (ALIGN_CENTER == alignment) {
|
||||||
|
translationY = b - ((bottom - top - drawable.getBounds().height()) / 2);
|
||||||
|
} else if (ALIGN_BASELINE == alignment) {
|
||||||
|
translationY = b - paint.getFontMetricsInt().descent;
|
||||||
|
} else {
|
||||||
|
translationY = b;
|
||||||
|
}
|
||||||
|
canvas.translate(x, translationY);
|
||||||
|
drawable.draw(canvas);
|
||||||
|
} finally {
|
||||||
|
canvas.restoreToCount(save);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IconProcessor implements DelimiterProcessor {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static IconProcessor create() {
|
||||||
|
return new IconProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ic-home-black-24
|
||||||
|
private static final Pattern PATTERN = Pattern.compile("ic-(\\w+)-(\\w+)-(\\d+)");
|
||||||
|
|
||||||
|
private static final String TO_FIND = IconNode.DELIMITER_STRING + "ic-";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be used when input string does not wrap icon definition with `@` from both ends.
|
||||||
|
* So, `@ic-home-white-24` would become `@ic-home-white-24@`. This way parsing is easier
|
||||||
|
* and more predictable (cannot specify multiple ending delimiters, as we would require them:
|
||||||
|
* space, newline, end of a document, and a lot of more)
|
||||||
|
*
|
||||||
|
* @param input to process
|
||||||
|
* @return processed string
|
||||||
|
* @see #prepare(StringBuilder)
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static String prepare(@NonNull String input) {
|
||||||
|
final StringBuilder builder = new StringBuilder(input);
|
||||||
|
prepare(builder);
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void prepare(@NonNull StringBuilder builder) {
|
||||||
|
|
||||||
|
int start = builder.indexOf(TO_FIND);
|
||||||
|
int end;
|
||||||
|
|
||||||
|
while (start > -1) {
|
||||||
|
|
||||||
|
end = iconDefinitionEnd(start + TO_FIND.length(), builder);
|
||||||
|
|
||||||
|
// if we match our pattern, append `@` else ignore
|
||||||
|
if (iconDefinitionValid(builder.subSequence(start + 1, end))) {
|
||||||
|
builder.insert(end, '@');
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to next
|
||||||
|
start = builder.indexOf(TO_FIND, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char getOpeningCharacter() {
|
||||||
|
return IconNode.DELIMITER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char getClosingCharacter() {
|
||||||
|
return IconNode.DELIMITER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMinLength() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
|
||||||
|
return opener.length() >= 1 && closer.length() >= 1 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process(Text opener, Text closer, int delimiterUse) {
|
||||||
|
|
||||||
|
final IconGroupNode iconGroupNode = new IconGroupNode();
|
||||||
|
|
||||||
|
final Node next = opener.getNext();
|
||||||
|
|
||||||
|
boolean handled = false;
|
||||||
|
|
||||||
|
// process only if we have exactly one Text node
|
||||||
|
if (next instanceof Text && next.getNext() == closer) {
|
||||||
|
|
||||||
|
final String text = ((Text) next).getLiteral();
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(text)) {
|
||||||
|
|
||||||
|
// attempt to match
|
||||||
|
final Matcher matcher = PATTERN.matcher(text);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
final IconNode iconNode = new IconNode(
|
||||||
|
matcher.group(1),
|
||||||
|
matcher.group(2),
|
||||||
|
matcher.group(3)
|
||||||
|
);
|
||||||
|
iconGroupNode.appendChild(iconNode);
|
||||||
|
next.unlink();
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handled) {
|
||||||
|
|
||||||
|
// restore delimiters if we didn't match
|
||||||
|
|
||||||
|
iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING));
|
||||||
|
|
||||||
|
Node node;
|
||||||
|
for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) {
|
||||||
|
node = tmp.getNext();
|
||||||
|
// append a child anyway
|
||||||
|
iconGroupNode.appendChild(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING));
|
||||||
|
}
|
||||||
|
|
||||||
|
opener.insertBefore(iconGroupNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int iconDefinitionEnd(int index, @NonNull StringBuilder builder) {
|
||||||
|
|
||||||
|
// all spaces, new lines, non-words or digits,
|
||||||
|
|
||||||
|
char c;
|
||||||
|
|
||||||
|
int end = -1;
|
||||||
|
for (int i = index; i < builder.length(); i++) {
|
||||||
|
c = builder.charAt(i);
|
||||||
|
if (Character.isWhitespace(c)
|
||||||
|
|| !(Character.isLetterOrDigit(c) || c == '-' || c == '_')) {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end == -1) {
|
||||||
|
end = builder.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean iconDefinitionValid(@NonNull CharSequence cs) {
|
||||||
|
final Matcher matcher = PATTERN.matcher(cs);
|
||||||
|
return matcher.matches();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IconNode extends CustomNode implements Delimited {
|
||||||
|
|
||||||
|
public static final char DELIMITER = '@';
|
||||||
|
|
||||||
|
public static final String DELIMITER_STRING = "" + DELIMITER;
|
||||||
|
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final String color;
|
||||||
|
|
||||||
|
private final String size;
|
||||||
|
|
||||||
|
public IconNode(@NonNull String name, @NonNull String color, @NonNull String size) {
|
||||||
|
this.name = name;
|
||||||
|
this.color = color;
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String color() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOpeningDelimiter() {
|
||||||
|
return DELIMITER_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClosingDelimiter() {
|
||||||
|
return DELIMITER_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public String toString() {
|
||||||
|
return "IconNode{" +
|
||||||
|
"name='" + name + '\'' +
|
||||||
|
", color='" + color + '\'' +
|
||||||
|
", size='" + size + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IconGroupNode extends CustomNode {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
|
||||||
|
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.core.MarkwonTheme;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181123617",
|
||||||
|
title = "Customize theme",
|
||||||
|
description = "Customize `MarkwonTheme` styling",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.style, Tags.theme, Tags.plugin}
|
||||||
|
)
|
||||||
|
public class CustomizeThemeSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
|
||||||
|
final String md = "`A code` that is rendered differently\n\n```\nHello!\n```";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||||
|
builder
|
||||||
|
.codeBackgroundColor(Color.BLACK)
|
||||||
|
.codeTextColor(Color.RED);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.Heading;
|
||||||
|
|
||||||
|
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.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181123308",
|
||||||
|
title = "Disable node from rendering",
|
||||||
|
description = "Disable _parsed_ node from being rendered (markdown syntax is still consumed)",
|
||||||
|
artifacts = {MarkwonArtifact.CORE},
|
||||||
|
tags = {Tags.parsing, Tags.rendering}
|
||||||
|
)
|
||||||
|
public class DisableNodeSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
|
|
||||||
|
// for example to disable rendering of heading:
|
||||||
|
// try commenting this out to see that otherwise headings will be rendered
|
||||||
|
builder.on(Heading.class, null);
|
||||||
|
|
||||||
|
// same method can be used to override existing visitor by specifying
|
||||||
|
// a new NodeVisitor instance
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.Link;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
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.InlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181162024",
|
||||||
|
title = "User mention and issue (via text)",
|
||||||
|
description = "Github-like user mention and issue " +
|
||||||
|
"rendering via `CorePlugin.OnTextAddedListener`",
|
||||||
|
artifacts = {MarkwonArtifact.CORE, MarkwonArtifact.INLINE_PARSER},
|
||||||
|
tags = {Tags.parsing, Tags.textAddedListener, Tags.rendering}
|
||||||
|
)
|
||||||
|
public class GithubUserIssueInlineParsingSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Custom Extension 2\n" +
|
||||||
|
"\n" +
|
||||||
|
"This is an issue #1\n" +
|
||||||
|
"Done by @noties";
|
||||||
|
|
||||||
|
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
// include all current defaults (otherwise will be empty - contain only our inline-processors)
|
||||||
|
// included by default, to create factory-builder without defaults call `factoryBuilderNoDefaults`
|
||||||
|
// .includeDefaults()
|
||||||
|
.addInlineProcessor(new IssueInlineProcessor())
|
||||||
|
.addInlineProcessor(new UserInlineProcessor())
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IssueInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final Pattern RE = Pattern.compile("\\d+");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Node parse() {
|
||||||
|
final String id = match(RE);
|
||||||
|
if (id != null) {
|
||||||
|
final Link link = new Link(createIssueOrPullRequestLinkDestination(id), null);
|
||||||
|
link.appendChild(text("#" + id));
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String createIssueOrPullRequestLinkDestination(@NonNull String id) {
|
||||||
|
return "https://github.com/noties/Markwon/issues/" + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final Pattern RE = Pattern.compile("\\w+");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '@';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Node parse() {
|
||||||
|
final String user = match(RE);
|
||||||
|
if (user != null) {
|
||||||
|
final Link link = new Link(createUserLinkDestination(user), null);
|
||||||
|
link.appendChild(text("@" + user));
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String createUserLinkDestination(@NonNull String user) {
|
||||||
|
return "https://github.com/" + user;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.Link;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonConfiguration;
|
||||||
|
import io.noties.markwon.MarkwonVisitor;
|
||||||
|
import io.noties.markwon.RenderProps;
|
||||||
|
import io.noties.markwon.SpannableBuilder;
|
||||||
|
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.core.CoreProps;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181162024",
|
||||||
|
title = "User mention and issue (via text)",
|
||||||
|
description = "Github-like user mention and issue " +
|
||||||
|
"rendering via `CorePlugin.OnTextAddedListener`",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.parsing, Tags.textAddedListener, Tags.rendering}
|
||||||
|
)
|
||||||
|
public class GithubUserIssueOnTextAddedSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Custom Extension 2\n" +
|
||||||
|
"\n" +
|
||||||
|
"This is an issue #1\n" +
|
||||||
|
"Done by @noties";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configure(@NonNull Registry registry) {
|
||||||
|
registry.require(CorePlugin.class, corePlugin ->
|
||||||
|
corePlugin.addOnTextAddedListener(new GithubLinkifyRegexTextAddedListener()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GithubLinkifyRegexTextAddedListener implements CorePlugin.OnTextAddedListener {
|
||||||
|
|
||||||
|
private static final Pattern PATTERN = Pattern.compile("((#\\d+)|(@\\w+))", Pattern.MULTILINE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextAdded(@NonNull MarkwonVisitor visitor, @NonNull String text, int start) {
|
||||||
|
|
||||||
|
final Matcher matcher = PATTERN.matcher(text);
|
||||||
|
|
||||||
|
String value;
|
||||||
|
String url;
|
||||||
|
int index;
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
|
||||||
|
value = matcher.group(1);
|
||||||
|
|
||||||
|
// detect which one it is
|
||||||
|
if ('#' == value.charAt(0)) {
|
||||||
|
url = createIssueOrPullRequestLink(value.substring(1));
|
||||||
|
} else {
|
||||||
|
url = createUserLink(value.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's important to use `start` value (represents start-index of `text` in the visitor)
|
||||||
|
index = start + matcher.start();
|
||||||
|
|
||||||
|
setLink(visitor, url, index, index + value.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String createIssueOrPullRequestLink(@NonNull String number) {
|
||||||
|
// issues and pull-requests on github follow the same pattern and we
|
||||||
|
// cannot know for sure which one it is, but if we use issues for all types,
|
||||||
|
// github will automatically redirect to pull-request if it's the one which is opened
|
||||||
|
return "https://github.com/noties/Markwon/issues/" + number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String createUserLink(@NonNull String user) {
|
||||||
|
return "https://github.com/" + user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLink(@NonNull MarkwonVisitor visitor, @NonNull String destination, int start, int end) {
|
||||||
|
|
||||||
|
// might a simpler one, but it doesn't respect possible changes to links
|
||||||
|
// visitor.builder().setSpan(
|
||||||
|
// new LinkSpan(visitor.configuration().theme(), destination, visitor.configuration().linkResolver()),
|
||||||
|
// start,
|
||||||
|
// end
|
||||||
|
// );
|
||||||
|
|
||||||
|
// use default handlers for links
|
||||||
|
final MarkwonConfiguration configuration = visitor.configuration();
|
||||||
|
final RenderProps renderProps = visitor.renderProps();
|
||||||
|
|
||||||
|
CoreProps.LINK_DESTINATION.set(renderProps, destination);
|
||||||
|
|
||||||
|
SpannableBuilder.setSpans(
|
||||||
|
visitor.builder(),
|
||||||
|
configuration.spansFactory().require(Link.class).getSpans(configuration, renderProps),
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.Heading;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.BlockHandlerDef;
|
||||||
|
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.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181125924",
|
||||||
|
title = "Heading no padding (block handler)",
|
||||||
|
description = "Process padding (spacing) after heading with a " +
|
||||||
|
"`BlockHandler`",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.block, Tags.spacing, Tags.padding, Tags.heading, Tags.rendering}
|
||||||
|
)
|
||||||
|
public class HeadingNoSpaceBlockHandlerSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Title title title title title title title title title title\n\n" +
|
||||||
|
"text text text text" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
|
builder.blockHandler(new BlockHandlerDef() {
|
||||||
|
@Override
|
||||||
|
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||||
|
if (node instanceof Heading) {
|
||||||
|
if (visitor.hasNext(node)) {
|
||||||
|
visitor.ensureNewLine();
|
||||||
|
// ensure new line but do not force insert one
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.blockEnd(visitor, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.Heading;
|
||||||
|
|
||||||
|
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.core.CoreProps;
|
||||||
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181125622",
|
||||||
|
title = "Heading no padding",
|
||||||
|
description = "Do not add a new line after heading node",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.spacing, Tags.padding, Tags.spacing, Tags.rendering}
|
||||||
|
)
|
||||||
|
class HeadingNoSpaceSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Title title title title title title title title title title" +
|
||||||
|
"\n\ntext text text text" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||||
|
builder.headingBreakHeight(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
|
builder.on(Heading.class, (visitor, heading) -> {
|
||||||
|
|
||||||
|
visitor.ensureNewLine();
|
||||||
|
|
||||||
|
final int length = visitor.length();
|
||||||
|
visitor.visitChildren(heading);
|
||||||
|
|
||||||
|
CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
|
||||||
|
|
||||||
|
visitor.setSpansForNodeOptional(heading, length);
|
||||||
|
|
||||||
|
if (visitor.hasNext(heading)) {
|
||||||
|
visitor.ensureNewLine();
|
||||||
|
// by default Markwon adds a new line here
|
||||||
|
// visitor.forceNewLine();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
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.image.ImageItem;
|
||||||
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
import io.noties.markwon.image.SchemeHandler;
|
||||||
|
import io.noties.markwon.image.network.NetworkSchemeHandler;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181124201",
|
||||||
|
title = "Image destination custom scheme",
|
||||||
|
description = "Example of handling custom scheme " +
|
||||||
|
"(`https`, `ftp`, `whatever`, etc.) for images destination URLs " +
|
||||||
|
"with `ImagesPlugin`",
|
||||||
|
artifacts = {MarkwonArtifact.IMAGE},
|
||||||
|
tags = {Tags.image}
|
||||||
|
)
|
||||||
|
public class ImagesCustomSchemeSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configure(@NonNull Registry registry) {
|
||||||
|
|
||||||
|
// use registry.require to obtain a plugin, does also
|
||||||
|
// a runtime validation if this plugin is registered
|
||||||
|
registry.require(ImagesPlugin.class, plugin -> plugin.addSchemeHandler(new SchemeHandler() {
|
||||||
|
|
||||||
|
// it's a sample only, most likely you won't need to
|
||||||
|
// use existing scheme-handler, this for demonstration purposes only
|
||||||
|
final NetworkSchemeHandler handler = NetworkSchemeHandler.create();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
|
||||||
|
// just replace it with https for the sack of sample
|
||||||
|
final String url = raw.replace("myownscheme", "https");
|
||||||
|
return handler.handle(url, Uri.parse(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Collection<String> supportedSchemes() {
|
||||||
|
return Collections.singleton("myownscheme");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// or we can init plugin with this factory method
|
||||||
|
// .usePlugin(ImagesPlugin.create(plugin -> {
|
||||||
|
// plugin.addSchemeHandler(/**/)
|
||||||
|
// }))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
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.MarkwonInlineParser;
|
||||||
|
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181170857",
|
||||||
|
title = "Inline parsing without defaults",
|
||||||
|
description = "Configure inline parser plugin to **not** have any **inline** parsing",
|
||||||
|
artifacts = {MarkwonArtifact.INLINE_PARSER},
|
||||||
|
tags = {Tags.parsing}
|
||||||
|
)
|
||||||
|
public class InlinePluginNoDefaultsSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Heading\n" +
|
||||||
|
"`code` inlined and **bold** here";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
|
||||||
|
// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults(), factoryBuilder -> {
|
||||||
|
// // if anything, they can be included here
|
||||||
|
//// factoryBuilder.includeDefaults()
|
||||||
|
// }))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.SparseIntArray;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.BulletList;
|
||||||
|
import org.commonmark.node.ListItem;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.node.OrderedList;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonSpansFactory;
|
||||||
|
import io.noties.markwon.MarkwonVisitor;
|
||||||
|
import io.noties.markwon.Prop;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||||
|
import io.noties.markwon.core.CoreProps;
|
||||||
|
import io.noties.markwon.core.spans.BulletListItemSpan;
|
||||||
|
import io.noties.markwon.core.spans.OrderedListItemSpan;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181130954",
|
||||||
|
title = "Letter ordered list",
|
||||||
|
description = "Render bullet list inside an ordered list with letters instead of bullets",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.rendering, Tags.plugin, Tags.lists}
|
||||||
|
)
|
||||||
|
public class LetterOrderedListSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
// bullet list nested in ordered list renders letters instead of bullets
|
||||||
|
final String md = "" +
|
||||||
|
"1. Hello there!\n" +
|
||||||
|
"1. And here is how:\n" +
|
||||||
|
" - First\n" +
|
||||||
|
" - Second\n" +
|
||||||
|
" - Third\n" +
|
||||||
|
" 1. And first here\n\n";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BulletListIsOrderedWithLettersWhenNestedPlugin extends AbstractMarkwonPlugin {
|
||||||
|
|
||||||
|
private static final Prop<String> BULLET_LETTER = Prop.of("my-bullet-letter");
|
||||||
|
|
||||||
|
// or introduce some kind of synchronization if planning to use from multiple threads,
|
||||||
|
// for example via ThreadLocal
|
||||||
|
private final SparseIntArray bulletCounter = new SparseIntArray();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
|
||||||
|
// clear counter after render
|
||||||
|
bulletCounter.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
|
// NB that both ordered and bullet lists are represented
|
||||||
|
// by ListItem (must inspect parent to detect the type)
|
||||||
|
builder.on(ListItem.class, (visitor, listItem) -> {
|
||||||
|
// mimic original behaviour (copy-pasta from CorePlugin)
|
||||||
|
|
||||||
|
final int length = visitor.length();
|
||||||
|
|
||||||
|
visitor.visitChildren(listItem);
|
||||||
|
|
||||||
|
final Node parent = listItem.getParent();
|
||||||
|
if (parent instanceof OrderedList) {
|
||||||
|
|
||||||
|
final int start = ((OrderedList) parent).getStartNumber();
|
||||||
|
|
||||||
|
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED);
|
||||||
|
CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start);
|
||||||
|
|
||||||
|
// after we have visited the children increment start number
|
||||||
|
final OrderedList orderedList = (OrderedList) parent;
|
||||||
|
orderedList.setStartNumber(orderedList.getStartNumber() + 1);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET);
|
||||||
|
|
||||||
|
if (isBulletOrdered(parent)) {
|
||||||
|
// obtain current count value
|
||||||
|
final int count = currentBulletCountIn(parent);
|
||||||
|
BULLET_LETTER.set(visitor.renderProps(), createBulletLetter(count));
|
||||||
|
// update current count value
|
||||||
|
setCurrentBulletCountIn(parent, count + 1);
|
||||||
|
} else {
|
||||||
|
CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
|
||||||
|
// clear letter info when regular bullet list is used
|
||||||
|
BULLET_LETTER.clear(visitor.renderProps());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor.setSpansForNodeOptional(listItem, length);
|
||||||
|
|
||||||
|
if (visitor.hasNext(listItem)) {
|
||||||
|
visitor.ensureNewLine();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||||
|
builder.setFactory(ListItem.class, (configuration, props) -> {
|
||||||
|
final Object spans;
|
||||||
|
|
||||||
|
if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) {
|
||||||
|
final String letter = BULLET_LETTER.get(props);
|
||||||
|
if (!TextUtils.isEmpty(letter)) {
|
||||||
|
// NB, we are using OrderedListItemSpan here!
|
||||||
|
spans = new OrderedListItemSpan(
|
||||||
|
configuration.theme(),
|
||||||
|
letter
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
spans = new BulletListItemSpan(
|
||||||
|
configuration.theme(),
|
||||||
|
CoreProps.BULLET_LIST_ITEM_LEVEL.require(props)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props))
|
||||||
|
+ "." + '\u00a0';
|
||||||
|
|
||||||
|
spans = new OrderedListItemSpan(
|
||||||
|
configuration.theme(),
|
||||||
|
number
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spans;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private int currentBulletCountIn(@NonNull Node parent) {
|
||||||
|
return bulletCounter.get(parent.hashCode(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCurrentBulletCountIn(@NonNull Node parent, int count) {
|
||||||
|
bulletCounter.put(parent.hashCode(), count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String createBulletLetter(int count) {
|
||||||
|
// or lower `a`
|
||||||
|
// `'u00a0` is non-breakable space char
|
||||||
|
return ((char) ('A' + count)) + ".\u00a0";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int listLevel(@NonNull Node node) {
|
||||||
|
int level = 0;
|
||||||
|
Node parent = node.getParent();
|
||||||
|
while (parent != null) {
|
||||||
|
if (parent instanceof ListItem) {
|
||||||
|
level += 1;
|
||||||
|
}
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBulletOrdered(@NonNull Node node) {
|
||||||
|
node = node.getParent();
|
||||||
|
while (node != null) {
|
||||||
|
if (node instanceof OrderedList) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (node instanceof BulletList) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
node = node.getParent();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.node.Link;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.LinkResolver;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonSpansFactory;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||||
|
import io.noties.markwon.core.CoreProps;
|
||||||
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
|
import io.noties.markwon.core.spans.LinkSpan;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181122230",
|
||||||
|
title = "Obtain link title",
|
||||||
|
description = "Obtain title (text) of clicked link, `[title](#destination)`",
|
||||||
|
artifacts = {MarkwonArtifact.CORE},
|
||||||
|
tags = {Tags.links, Tags.span}
|
||||||
|
)
|
||||||
|
public class LinkTitleSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Links\n\n" +
|
||||||
|
"[link title](#)";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||||
|
builder.setFactory(Link.class, (configuration, props) ->
|
||||||
|
// create a subclass of markwon LinkSpan
|
||||||
|
new ClickSelfSpan(
|
||||||
|
configuration.theme(),
|
||||||
|
CoreProps.LINK_DESTINATION.require(props),
|
||||||
|
configuration.linkResolver()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClickSelfSpan extends LinkSpan {
|
||||||
|
|
||||||
|
ClickSelfSpan(
|
||||||
|
@NonNull MarkwonTheme theme,
|
||||||
|
@NonNull String link,
|
||||||
|
@NonNull LinkResolver resolver) {
|
||||||
|
super(theme, link, resolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View widget) {
|
||||||
|
Toast.makeText(
|
||||||
|
widget.getContext(),
|
||||||
|
String.format(Locale.ROOT, "clicked link title: '%s'", linkTitle(widget)),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show();
|
||||||
|
super.onClick(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private CharSequence linkTitle(@NonNull View widget) {
|
||||||
|
|
||||||
|
if (!(widget instanceof TextView)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Spanned spanned = (Spanned) ((TextView) widget).getText();
|
||||||
|
final int start = spanned.getSpanStart(this);
|
||||||
|
final int end = spanned.getSpanEnd(this);
|
||||||
|
|
||||||
|
if (start < 0 || end < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return spanned.subSequence(start, end);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
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 = "202006181124005",
|
||||||
|
title = "Links without scheme",
|
||||||
|
description = "Links without scheme are considered to be `https`",
|
||||||
|
artifacts = {MarkwonArtifact.CORE},
|
||||||
|
tags = {Tags.links, Tags.defaults}
|
||||||
|
)
|
||||||
|
public class LinkWithoutSchemeSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Links without scheme\n" +
|
||||||
|
"[a link without scheme](github.com) is considered to be `https`.\n" +
|
||||||
|
"Override `LinkResolverDef` to change this functionality" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.create(context);
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
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.MarkwonInlineParser;
|
||||||
|
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181171212",
|
||||||
|
title = "No parsing",
|
||||||
|
description = "All commonmark parsing is disabled (both inlines and blocks)",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.parsing, Tags.rendering}
|
||||||
|
)
|
||||||
|
public class NoParsingSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Heading\n" +
|
||||||
|
"[link](#) was _here_ and `then` and it was:\n" +
|
||||||
|
"> a quote\n" +
|
||||||
|
"```java\n" +
|
||||||
|
"final int someJavaCode = 0;\n" +
|
||||||
|
"```\n";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
// disable inline parsing
|
||||||
|
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
builder.enabledBlockTypes(Collections.emptySet());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.Paragraph;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonSpansFactory;
|
||||||
|
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 = "202006181122647",
|
||||||
|
title = "Paragraph style",
|
||||||
|
description = "Apply a style (via span) to a paragraph",
|
||||||
|
artifacts = {MarkwonArtifact.CORE},
|
||||||
|
tags = {Tags.paragraph, Tags.style, Tags.span}
|
||||||
|
)
|
||||||
|
public class ParagraphSpanStyle extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "# Hello!\n\nA paragraph?\n\nIt should be!";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||||
|
// apply a span to a Paragraph
|
||||||
|
builder.setFactory(Paragraph.class, (configuration, props) ->
|
||||||
|
new ForegroundColorSpan(Color.GREEN));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
|
import android.text.style.ReplacementSpan;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
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.ext.tables.TablePlugin;
|
||||||
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181161505",
|
||||||
|
title = "Read more plugin",
|
||||||
|
description = "Plugin that adds expand/collapse (\"show all\"/\"show less\")",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.plugin}
|
||||||
|
)
|
||||||
|
public class ReadMorePluginSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"Lorem **ipsum**  sit amet, consectetur adipiscing elit. Morbi vitae enim ut sem aliquet ultrices. Nunc a accumsan orci. Suspendisse tortor ante, lacinia ac scelerisque sed, dictum eget metus. Morbi ante augue, tristique eget quam in, vestibulum rutrum lacus. Nulla aliquam auctor cursus. Nulla at lacus condimentum, viverra lacus eget, sollicitudin ex. Cras efficitur leo dui, sit amet rutrum tellus venenatis et. Sed in facilisis libero. Etiam ultricies, nulla ut venenatis tincidunt, tortor erat tristique ante, non aliquet massa arcu eget nisl. Etiam gravida erat ante, sit amet lobortis mauris commodo nec. Praesent vitae sodales quam. Vivamus condimentum porta suscipit. Donec posuere id felis ac scelerisque. Vestibulum lacinia et leo id lobortis. Sed vitae dolor nec ligula dapibus finibus vel eu libero. Nam tincidunt maximus elit, sit amet tincidunt lacus laoreet malesuada.\n\n" +
|
||||||
|
"here we ";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.usePlugin(new ReadMorePlugin())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read more plugin based on text length. It is easier to implement than lines (need to adjust
|
||||||
|
* last line to include expand/collapse text).
|
||||||
|
*/
|
||||||
|
class ReadMorePlugin extends AbstractMarkwonPlugin {
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private final int maxLength = 150;
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private final String labelMore = "Show more...";
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private final String labelLess = "...Show less";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(@NonNull Registry registry) {
|
||||||
|
// establish connections with all _dynamic_ content that your markdown supports,
|
||||||
|
// like images, tables, latex, etc
|
||||||
|
registry.require(ImagesPlugin.class);
|
||||||
|
registry.require(TablePlugin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterSetText(@NonNull TextView textView) {
|
||||||
|
final CharSequence text = textView.getText();
|
||||||
|
if (text.length() < maxLength) {
|
||||||
|
// everything is OK, no need to ellipsize)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int breakAt = breakTextAt(text, 0, maxLength);
|
||||||
|
final CharSequence cs = createCollapsedString(text, 0, breakAt);
|
||||||
|
textView.setText(cs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
@NonNull
|
||||||
|
private CharSequence createCollapsedString(@NonNull CharSequence text, int start, int end) {
|
||||||
|
final SpannableStringBuilder builder = new SpannableStringBuilder(text, start, end);
|
||||||
|
|
||||||
|
// NB! each table row is represented as a space character and new-line (so length=2) no
|
||||||
|
// matter how many characters are inside table cells
|
||||||
|
|
||||||
|
// we can _clean_ this builder, for example remove all dynamic content (like images and tables,
|
||||||
|
// but keep them in full/expanded version)
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (true) {
|
||||||
|
// it is an implementation detail but _mostly_ dynamic content is implemented as
|
||||||
|
// ReplacementSpans
|
||||||
|
final ReplacementSpan[] spans = builder.getSpans(0, builder.length(), ReplacementSpan.class);
|
||||||
|
if (spans != null) {
|
||||||
|
for (ReplacementSpan span : spans) {
|
||||||
|
builder.removeSpan(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB! if there will be a table in _preview_ (collapsed) then each row will be represented as a
|
||||||
|
// space and new-line
|
||||||
|
trim(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CharSequence fullText = createFullText(text, builder);
|
||||||
|
|
||||||
|
builder.append(' ');
|
||||||
|
|
||||||
|
final int length = builder.length();
|
||||||
|
builder.append(labelMore);
|
||||||
|
builder.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View widget) {
|
||||||
|
((TextView) widget).setText(fullText);
|
||||||
|
}
|
||||||
|
}, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private CharSequence createFullText(@NonNull CharSequence text, @NonNull CharSequence collapsedText) {
|
||||||
|
// full/expanded text can also be different,
|
||||||
|
// for example it can be kept as-is and have no `collapse` functionality (once expanded cannot collapse)
|
||||||
|
// or can contain collapse feature
|
||||||
|
final CharSequence fullText;
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (true) {
|
||||||
|
// for example let's allow collapsing
|
||||||
|
final SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||||
|
builder.append(' ');
|
||||||
|
|
||||||
|
final int length = builder.length();
|
||||||
|
builder.append(labelLess);
|
||||||
|
builder.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View widget) {
|
||||||
|
((TextView) widget).setText(collapsedText);
|
||||||
|
}
|
||||||
|
}, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
fullText = builder;
|
||||||
|
} else {
|
||||||
|
fullText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void trim(@NonNull SpannableStringBuilder builder) {
|
||||||
|
|
||||||
|
// NB! tables use `\u00a0` (non breaking space) which is not reported as white-space
|
||||||
|
|
||||||
|
char c;
|
||||||
|
|
||||||
|
for (int i = 0, length = builder.length(); i < length; i++) {
|
||||||
|
c = builder.charAt(i);
|
||||||
|
if (!Character.isWhitespace(c) && c != '\u00a0') {
|
||||||
|
if (i > 0) {
|
||||||
|
builder.replace(0, i, "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = builder.length() - 1; i >= 0; i--) {
|
||||||
|
c = builder.charAt(i);
|
||||||
|
if (!Character.isWhitespace(c) && c != '\u00a0') {
|
||||||
|
if (i < builder.length() - 1) {
|
||||||
|
builder.replace(i, builder.length(), "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// depending on your locale these can be different
|
||||||
|
// There is a BreakIterator in Android, but it is not reliable, still theoretically
|
||||||
|
// it should work better than hand-written and hardcoded rules
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
private static int breakTextAt(@NonNull CharSequence text, int start, int max) {
|
||||||
|
|
||||||
|
int last = start;
|
||||||
|
|
||||||
|
// no need to check for _start_ (anyway will be ignored)
|
||||||
|
for (int i = start + max - 1; i > start; i--) {
|
||||||
|
final char c = text.charAt(i);
|
||||||
|
if (Character.isWhitespace(c)
|
||||||
|
|| c == '.'
|
||||||
|
|| c == ','
|
||||||
|
|| c == '!'
|
||||||
|
|| c == '?') {
|
||||||
|
// include this special character
|
||||||
|
last = i - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last <= start) {
|
||||||
|
// when used in subSequence last index is exclusive,
|
||||||
|
// so given max=150 would result in 0-149 subSequence
|
||||||
|
return start + max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||||
|
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 = "202006181125040",
|
||||||
|
title = "Soft break new line",
|
||||||
|
description = "Add a new line for a markdown soft-break node",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.newLine, Tags.softBreak}
|
||||||
|
)
|
||||||
|
class SoftBreakAddsNewLineSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"Hello there ->(line)\n(break)<- going on and on";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
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 = "202006181124706",
|
||||||
|
title = "Soft break adds space",
|
||||||
|
description = "By default a soft break (`\n`) will " +
|
||||||
|
"add a space character instead of new line",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.newLine, Tags.softBreak, Tags.defaults}
|
||||||
|
)
|
||||||
|
public class SoftBreakAddsSpace extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final String md = "" +
|
||||||
|
"Hello there ->(line)\n(break)<- going on and on";
|
||||||
|
|
||||||
|
// by default a soft break will add a space (instead of line break)
|
||||||
|
final Markwon markwon = Markwon.create(context);
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
package io.noties.markwon.app.samples;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.node.AbstractVisitor;
|
||||||
|
import org.commonmark.node.BulletList;
|
||||||
|
import org.commonmark.node.CustomBlock;
|
||||||
|
import org.commonmark.node.Heading;
|
||||||
|
import org.commonmark.node.Link;
|
||||||
|
import org.commonmark.node.ListItem;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.node.Text;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
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.core.SimpleBlockNodeVisitor;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181161226",
|
||||||
|
title = "Table of contents",
|
||||||
|
description = "Sample plugin that adds a table of contents header",
|
||||||
|
artifacts = MarkwonArtifact.CORE,
|
||||||
|
tags = {Tags.rendering, Tags.plugin}
|
||||||
|
)
|
||||||
|
public class TableOfContentsSample 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 = "" +
|
||||||
|
"# First\n" +
|
||||||
|
"" + lorem + "\n\n" +
|
||||||
|
"# Second\n" +
|
||||||
|
"" + lorem + "\n\n" +
|
||||||
|
"## Second level\n\n" +
|
||||||
|
"" + lorem + "\n\n" +
|
||||||
|
"### Level 3\n\n" +
|
||||||
|
"" + lorem + "\n\n" +
|
||||||
|
"# First again\n" +
|
||||||
|
"" + lorem + "\n\n";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(new TableOfContentsPlugin())
|
||||||
|
// NB! plugin is defined in `AnchorSample` file
|
||||||
|
.usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TableOfContentsPlugin extends AbstractMarkwonPlugin {
|
||||||
|
@Override
|
||||||
|
public void configure(@NonNull Registry registry) {
|
||||||
|
// just to make it explicit
|
||||||
|
registry.require(AnchorHeadingPlugin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
|
builder.on(TableOfContentsBlock.class, new SimpleBlockNodeVisitor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeRender(@NonNull Node node) {
|
||||||
|
|
||||||
|
// custom block to hold TOC
|
||||||
|
final TableOfContentsBlock block = new TableOfContentsBlock();
|
||||||
|
|
||||||
|
// create TOC title
|
||||||
|
{
|
||||||
|
final Text text = new Text("Table of contents");
|
||||||
|
final Heading heading = new Heading();
|
||||||
|
// important one - set TOC heading level
|
||||||
|
heading.setLevel(1);
|
||||||
|
heading.appendChild(text);
|
||||||
|
block.appendChild(heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
final HeadingVisitor visitor = new HeadingVisitor(block);
|
||||||
|
node.accept(visitor);
|
||||||
|
|
||||||
|
// make it the very first node in rendered markdown
|
||||||
|
node.prependChild(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HeadingVisitor extends AbstractVisitor {
|
||||||
|
|
||||||
|
private final BulletList bulletList = new BulletList();
|
||||||
|
private final StringBuilder builder = new StringBuilder();
|
||||||
|
private boolean isInsideHeading;
|
||||||
|
|
||||||
|
HeadingVisitor(@NonNull Node node) {
|
||||||
|
node.appendChild(bulletList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Heading heading) {
|
||||||
|
this.isInsideHeading = true;
|
||||||
|
try {
|
||||||
|
// reset build from previous content
|
||||||
|
builder.setLength(0);
|
||||||
|
|
||||||
|
// obtain level (can additionally filter by level, to skip lower ones)
|
||||||
|
final int level = heading.getLevel();
|
||||||
|
|
||||||
|
// build heading title
|
||||||
|
visitChildren(heading);
|
||||||
|
|
||||||
|
// initial list item
|
||||||
|
final ListItem listItem = new ListItem();
|
||||||
|
|
||||||
|
Node parent = listItem;
|
||||||
|
Node node = listItem;
|
||||||
|
|
||||||
|
for (int i = 1; i < level; i++) {
|
||||||
|
final ListItem li = new ListItem();
|
||||||
|
final BulletList bulletList = new BulletList();
|
||||||
|
bulletList.appendChild(li);
|
||||||
|
parent.appendChild(bulletList);
|
||||||
|
parent = li;
|
||||||
|
node = li;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String content = builder.toString();
|
||||||
|
final Link link = new Link("#" + AnchorHeadingPlugin.createAnchor(content), null);
|
||||||
|
final Text text = new Text(content);
|
||||||
|
link.appendChild(text);
|
||||||
|
node.appendChild(link);
|
||||||
|
bulletList.appendChild(listItem);
|
||||||
|
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
isInsideHeading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Text text) {
|
||||||
|
// can additionally check if we are building heading (to skip all other texts)
|
||||||
|
if (isInsideHeading) {
|
||||||
|
builder.append(text.getLiteral());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TableOfContentsBlock extends CustomBlock {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
package io.noties.markwon.app.samples.editor;
|
||||||
|
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
|
||||||
|
import io.noties.markwon.core.spans.StrongEmphasisSpan;
|
||||||
|
import io.noties.markwon.editor.AbstractEditHandler;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditor;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||||
|
import io.noties.markwon.editor.PersistedSpans;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181165136",
|
||||||
|
title = "Additional edit span",
|
||||||
|
description = "Additional _edit_ span (span that is present in " +
|
||||||
|
"`EditText` along with punctuation",
|
||||||
|
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||||
|
tags = {Tags.editor, Tags.span}
|
||||||
|
)
|
||||||
|
public class EditorAdditionalEditSpan extends MarkwonEditTextSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
// An additional span is used to highlight strong-emphasis
|
||||||
|
|
||||||
|
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(context))
|
||||||
|
.useEditHandler(new BoldEditHandler())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BoldEditHandler extends AbstractEditHandler<StrongEmphasisSpan> {
|
||||||
|
@Override
|
||||||
|
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||||
|
// Here we define which span is _persisted_ in EditText, it is not removed
|
||||||
|
// from EditText between text changes, but instead - reused (by changing
|
||||||
|
// position). Consider it as a cache for spans. We could use `StrongEmphasisSpan`
|
||||||
|
// here also, but I chose Bold to indicate that this span is not the same
|
||||||
|
// as in off-screen rendered markdown
|
||||||
|
builder.persistSpan(Bold.class, Bold::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMarkdownSpan(
|
||||||
|
@NonNull PersistedSpans persistedSpans,
|
||||||
|
@NonNull Editable editable,
|
||||||
|
@NonNull String input,
|
||||||
|
@NonNull StrongEmphasisSpan span,
|
||||||
|
int spanStart,
|
||||||
|
int spanTextLength) {
|
||||||
|
// Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4)
|
||||||
|
// because multiple inline markdown nodes can refer to the same text.
|
||||||
|
// For example, `**_~~hey~~_**` - we will receive `**_~~` in this method,
|
||||||
|
// and thus will have to manually find actual position in raw user input
|
||||||
|
final MarkwonEditorUtils.Match match =
|
||||||
|
MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__");
|
||||||
|
if (match != null) {
|
||||||
|
editable.setSpan(
|
||||||
|
// we handle StrongEmphasisSpan and represent it with Bold in EditText
|
||||||
|
// we still could use StrongEmphasisSpan, but it must be accessed
|
||||||
|
// via persistedSpans
|
||||||
|
persistedSpans.get(Bold.class),
|
||||||
|
match.start(),
|
||||||
|
match.end(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<StrongEmphasisSpan> markdownSpanType() {
|
||||||
|
return StrongEmphasisSpan.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bold extends MetricAffectingSpan {
|
||||||
|
public Bold() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(TextPaint tp) {
|
||||||
|
update(tp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMeasureState(@NonNull TextPaint textPaint) {
|
||||||
|
update(textPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(@NonNull TextPaint paint) {
|
||||||
|
paint.setFakeBoldText(true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package io.noties.markwon.app.samples.editor;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditor;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||||
|
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181165347",
|
||||||
|
title = "Additional plugin",
|
||||||
|
description = "Additional plugin for editor",
|
||||||
|
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER, MarkwonArtifact.EXT_STRIKETHROUGH},
|
||||||
|
tags = {Tags.editor}
|
||||||
|
)
|
||||||
|
public class EditorAdditionalPluginSample extends MarkwonEditTextSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
// As highlight works based on text-diff, everything that is present in input,
|
||||||
|
// but missing in resulting markdown is considered to be punctuation, this is why
|
||||||
|
// additional plugins do not need special handling
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(StrikethroughPlugin.create())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||||
|
|
||||||
|
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package io.noties.markwon.app.samples.editor;
|
||||||
|
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
|
||||||
|
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 = "202006181164627",
|
||||||
|
title = "Custom punctuation span",
|
||||||
|
description = "Custom span for punctuation in editor",
|
||||||
|
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||||
|
tags = {Tags.editor, Tags.span}
|
||||||
|
)
|
||||||
|
public class EditorCustomPunctuationSample extends MarkwonEditTextSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
// Use own punctuation span
|
||||||
|
|
||||||
|
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(context))
|
||||||
|
.punctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomPunctuationSpan extends ForegroundColorSpan {
|
||||||
|
CustomPunctuationSpan() {
|
||||||
|
super(0xFFFF0000); // RED
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package io.noties.markwon.app.samples.editor;
|
||||||
|
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
|
||||||
|
import io.noties.markwon.app.samples.editor.shared.BlockQuoteEditHandler;
|
||||||
|
import io.noties.markwon.app.samples.editor.shared.CodeEditHandler;
|
||||||
|
import io.noties.markwon.app.samples.editor.shared.LinkEditHandler;
|
||||||
|
import io.noties.markwon.app.samples.editor.shared.StrikethroughEditHandler;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditor;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||||
|
import io.noties.markwon.editor.handler.EmphasisEditHandler;
|
||||||
|
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
|
||||||
|
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||||
|
import io.noties.markwon.inlineparser.BangInlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.EntityInlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||||
|
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181165920",
|
||||||
|
title = "Multiple edit spans",
|
||||||
|
description = "Additional multiple edit spans for editor",
|
||||||
|
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||||
|
tags = {Tags.editor}
|
||||||
|
)
|
||||||
|
public class EditorMultipleEditSpansSample extends MarkwonEditTextSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
|
||||||
|
// for links to be clickable
|
||||||
|
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
|
||||||
|
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
// no inline images will be parsed
|
||||||
|
.excludeInlineProcessor(BangInlineProcessor.class)
|
||||||
|
// no html tags will be parsed
|
||||||
|
.excludeInlineProcessor(HtmlInlineProcessor.class)
|
||||||
|
// no entities will be parsed (aka `&` etc)
|
||||||
|
.excludeInlineProcessor(EntityInlineProcessor.class)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(StrikethroughPlugin.create())
|
||||||
|
.usePlugin(LinkifyPlugin.create())
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
|
||||||
|
// disable all commonmark-java blocks, only inlines will be parsed
|
||||||
|
// builder.enabledBlockTypes(Collections.emptySet());
|
||||||
|
|
||||||
|
builder.inlineParserFactory(inlineParserFactory);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
|
||||||
|
|
||||||
|
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
||||||
|
.useEditHandler(new EmphasisEditHandler())
|
||||||
|
.useEditHandler(new StrongEmphasisEditHandler())
|
||||||
|
.useEditHandler(new StrikethroughEditHandler())
|
||||||
|
.useEditHandler(new CodeEditHandler())
|
||||||
|
.useEditHandler(new BlockQuoteEditHandler())
|
||||||
|
.useEditHandler(new LinkEditHandler(onClick))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||||
|
editor, Executors.newSingleThreadExecutor(), editText));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
package io.noties.markwon.app.samples.editor;
|
||||||
|
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import io.noties.debug.Debug;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
|
||||||
|
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 = "202006181170348",
|
||||||
|
title = "Editor new line continuation",
|
||||||
|
description = "Sample of how new line character can be handled " +
|
||||||
|
"in order to add a _continuation_, for example adding a new " +
|
||||||
|
"bullet list item if current line starts with one",
|
||||||
|
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||||
|
tags = {Tags.editor}
|
||||||
|
)
|
||||||
|
public class EditorNewLineContinuationSample extends MarkwonEditTextSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
final Markwon markwon = Markwon.create(context);
|
||||||
|
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||||
|
|
||||||
|
final TextWatcher textWatcher = MarkdownNewLine
|
||||||
|
.wrap(MarkwonEditorTextWatcher.withProcess(editor));
|
||||||
|
|
||||||
|
editText.addTextChangedListener(textWatcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarkdownNewLine {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
static TextWatcher wrap(@NonNull TextWatcher textWatcher) {
|
||||||
|
return new NewLineTextWatcher(textWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarkdownNewLine() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NewLineTextWatcher implements TextWatcher {
|
||||||
|
|
||||||
|
// NB! matches only bullet lists
|
||||||
|
private final Pattern RE = Pattern.compile("^( {0,3}[\\-+* ]+)(.+)*$");
|
||||||
|
|
||||||
|
private final TextWatcher wrapped;
|
||||||
|
|
||||||
|
private boolean selfChange;
|
||||||
|
|
||||||
|
// this content is pending to be inserted at the beginning
|
||||||
|
private String pendingNewLineContent;
|
||||||
|
private int pendingNewLineIndex;
|
||||||
|
|
||||||
|
// mark current edited line for removal (range start/end)
|
||||||
|
private int clearLineStart;
|
||||||
|
private int clearLineEnd;
|
||||||
|
|
||||||
|
NewLineTextWatcher(@NonNull TextWatcher wrapped) {
|
||||||
|
this.wrapped = wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
if (selfChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just one new character added
|
||||||
|
if (before == 0
|
||||||
|
&& count == 1
|
||||||
|
&& '\n' == s.charAt(start)) {
|
||||||
|
int end = -1;
|
||||||
|
for (int i = start - 1; i >= 0; i--) {
|
||||||
|
if ('\n' == s.charAt(i)) {
|
||||||
|
end = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start at the very beginning
|
||||||
|
if (end < 0) {
|
||||||
|
end = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String pendingNewLineContent;
|
||||||
|
|
||||||
|
final int clearLineStart;
|
||||||
|
final int clearLineEnd;
|
||||||
|
|
||||||
|
final Matcher matcher = RE.matcher(s.subSequence(end, start));
|
||||||
|
if (matcher.matches()) {
|
||||||
|
// if second group is empty -> remove new line
|
||||||
|
final String content = matcher.group(2);
|
||||||
|
Debug.e("new line, content: '%s'", content);
|
||||||
|
if (TextUtils.isEmpty(content)) {
|
||||||
|
// another empty new line, remove this start
|
||||||
|
clearLineStart = end;
|
||||||
|
clearLineEnd = start;
|
||||||
|
pendingNewLineContent = null;
|
||||||
|
} else {
|
||||||
|
pendingNewLineContent = matcher.group(1);
|
||||||
|
clearLineStart = clearLineEnd = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pendingNewLineContent = null;
|
||||||
|
clearLineStart = clearLineEnd = 0;
|
||||||
|
}
|
||||||
|
this.pendingNewLineContent = pendingNewLineContent;
|
||||||
|
this.pendingNewLineIndex = start + 1;
|
||||||
|
this.clearLineStart = clearLineStart;
|
||||||
|
this.clearLineEnd = clearLineEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (selfChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingNewLineContent != null || clearLineStart < clearLineEnd) {
|
||||||
|
selfChange = true;
|
||||||
|
try {
|
||||||
|
if (pendingNewLineContent != null) {
|
||||||
|
s.insert(pendingNewLineIndex, pendingNewLineContent);
|
||||||
|
pendingNewLineContent = null;
|
||||||
|
} else {
|
||||||
|
s.replace(clearLineStart, clearLineEnd, "");
|
||||||
|
clearLineStart = clearLineEnd = 0;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selfChange = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB, we assume MarkdownEditor text watcher that only listens for this event,
|
||||||
|
// other text-watchers must be interested in other events also
|
||||||
|
wrapped.afterTextChanged(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
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.editor.MarkwonEditor;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181164422",
|
||||||
|
title = "Editor with pre-render (async)",
|
||||||
|
description = "Editor functionality with highlight " +
|
||||||
|
"taking place in another thread",
|
||||||
|
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||||
|
tags = {Tags.editor}
|
||||||
|
)
|
||||||
|
public class EditorPreRenderSample extends MarkwonEditTextSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
// Process highlight in background thread
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.create(context);
|
||||||
|
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||||
|
|
||||||
|
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||||
|
editor,
|
||||||
|
Executors.newCachedThreadPool(),
|
||||||
|
editText));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package io.noties.markwon.app.samples.editor;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonEditTextSample;
|
||||||
|
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 = "202006181164227",
|
||||||
|
title = "Simple editor",
|
||||||
|
description = "Simple usage of editor with markdown highlight",
|
||||||
|
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||||
|
tags = {Tags.editor}
|
||||||
|
)
|
||||||
|
public class EditorSimpleSample extends MarkwonEditTextSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
// Process highlight in-place (right after text has changed)
|
||||||
|
|
||||||
|
// obtain Markwon instance
|
||||||
|
final Markwon markwon = Markwon.create(context);
|
||||||
|
|
||||||
|
// create editor
|
||||||
|
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||||
|
|
||||||
|
// set edit listener
|
||||||
|
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
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.BlockQuoteSpan;
|
||||||
|
import io.noties.markwon.editor.EditHandler;
|
||||||
|
import io.noties.markwon.editor.PersistedSpans;
|
||||||
|
|
||||||
|
public class BlockQuoteEditHandler implements EditHandler<BlockQuoteSpan> {
|
||||||
|
|
||||||
|
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(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMarkdownSpan(
|
||||||
|
@NonNull PersistedSpans persistedSpans,
|
||||||
|
@NonNull Editable editable,
|
||||||
|
@NonNull String input,
|
||||||
|
@NonNull BlockQuoteSpan span,
|
||||||
|
int spanStart,
|
||||||
|
int spanTextLength) {
|
||||||
|
// todo: here we should actually find a proper ending of a block quote...
|
||||||
|
editable.setSpan(
|
||||||
|
persistedSpans.get(BlockQuoteSpan.class),
|
||||||
|
spanStart,
|
||||||
|
spanStart + spanTextLength,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<BlockQuoteSpan> markdownSpanType() {
|
||||||
|
return BlockQuoteSpan.class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
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.CodeSpan;
|
||||||
|
import io.noties.markwon.editor.EditHandler;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||||
|
import io.noties.markwon.editor.PersistedSpans;
|
||||||
|
|
||||||
|
public class CodeEditHandler implements EditHandler<CodeSpan> {
|
||||||
|
|
||||||
|
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(CodeSpan.class, () -> new CodeSpan(theme));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMarkdownSpan(
|
||||||
|
@NonNull PersistedSpans persistedSpans,
|
||||||
|
@NonNull Editable editable,
|
||||||
|
@NonNull String input,
|
||||||
|
@NonNull CodeSpan span,
|
||||||
|
int spanStart,
|
||||||
|
int spanTextLength) {
|
||||||
|
final MarkwonEditorUtils.Match match =
|
||||||
|
MarkwonEditorUtils.findDelimited(input, spanStart, "`");
|
||||||
|
if (match != null) {
|
||||||
|
editable.setSpan(
|
||||||
|
persistedSpans.get(CodeSpan.class),
|
||||||
|
match.start(),
|
||||||
|
match.end(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<CodeSpan> markdownSpanType() {
|
||||||
|
return CodeSpan.class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package io.noties.markwon.app.samples.editor.shared;
|
||||||
|
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import io.noties.markwon.core.spans.LinkSpan;
|
||||||
|
import io.noties.markwon.editor.AbstractEditHandler;
|
||||||
|
import io.noties.markwon.editor.PersistedSpans;
|
||||||
|
|
||||||
|
public class LinkEditHandler extends AbstractEditHandler<LinkSpan> {
|
||||||
|
|
||||||
|
public interface OnClick {
|
||||||
|
void onClick(@NonNull View widget, @NonNull String link);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final OnClick onClick;
|
||||||
|
|
||||||
|
public LinkEditHandler(@NonNull OnClick onClick) {
|
||||||
|
this.onClick = onClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||||
|
builder.persistSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMarkdownSpan(
|
||||||
|
@NonNull PersistedSpans persistedSpans,
|
||||||
|
@NonNull Editable editable,
|
||||||
|
@NonNull String input,
|
||||||
|
@NonNull LinkSpan span,
|
||||||
|
int spanStart,
|
||||||
|
int spanTextLength) {
|
||||||
|
|
||||||
|
final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class);
|
||||||
|
editLinkSpan.link = span.getLink();
|
||||||
|
|
||||||
|
// First first __letter__ to find link content (scheme start in URL, receiver in email address)
|
||||||
|
// NB! do not use phone number auto-link (via LinkifyPlugin) as we cannot guarantee proper link
|
||||||
|
// display. For example, we _could_ also look for a digit, but:
|
||||||
|
// * if phone number start with special symbol, we won't have it (`+`, `(`)
|
||||||
|
// * it might interfere with an ordered-list
|
||||||
|
int start = -1;
|
||||||
|
|
||||||
|
for (int i = spanStart, length = input.length(); i < length; i++) {
|
||||||
|
if (Character.isLetter(input.charAt(i))) {
|
||||||
|
start = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start > -1) {
|
||||||
|
editable.setSpan(
|
||||||
|
editLinkSpan,
|
||||||
|
start,
|
||||||
|
start + spanTextLength,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<LinkSpan> markdownSpanType() {
|
||||||
|
return LinkSpan.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class EditLinkSpan extends ClickableSpan {
|
||||||
|
|
||||||
|
private final OnClick onClick;
|
||||||
|
|
||||||
|
String link;
|
||||||
|
|
||||||
|
EditLinkSpan(@NonNull OnClick onClick) {
|
||||||
|
this.onClick = onClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View widget) {
|
||||||
|
if (link != null) {
|
||||||
|
onClick.onClick(widget, link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package io.noties.markwon.app.samples.editor.shared;
|
||||||
|
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.StrikethroughSpan;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import io.noties.markwon.editor.AbstractEditHandler;
|
||||||
|
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||||
|
import io.noties.markwon.editor.PersistedSpans;
|
||||||
|
|
||||||
|
public class StrikethroughEditHandler extends AbstractEditHandler<StrikethroughSpan> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||||
|
builder.persistSpan(StrikethroughSpan.class, StrikethroughSpan::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMarkdownSpan(
|
||||||
|
@NonNull PersistedSpans persistedSpans,
|
||||||
|
@NonNull Editable editable,
|
||||||
|
@NonNull String input,
|
||||||
|
@NonNull StrikethroughSpan span,
|
||||||
|
int spanStart,
|
||||||
|
int spanTextLength) {
|
||||||
|
final MarkwonEditorUtils.Match match =
|
||||||
|
MarkwonEditorUtils.findDelimited(input, spanStart, "~~");
|
||||||
|
if (match != null) {
|
||||||
|
editable.setSpan(
|
||||||
|
persistedSpans.get(StrikethroughSpan.class),
|
||||||
|
match.start(),
|
||||||
|
match.end(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<StrikethroughSpan> markdownSpanType() {
|
||||||
|
return StrikethroughSpan.class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package io.noties.markwon.app.samples.movementmethod
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import io.noties.markwon.app.sample.Tags
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||||
|
import io.noties.markwon.movement.MovementMethodPlugin
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "202006181121803",
|
||||||
|
title = "Disable implicit movement method via plugin",
|
||||||
|
description = "Disable implicit movement method via `MovementMethodPlugin`",
|
||||||
|
artifacts = [MarkwonArtifact.CORE],
|
||||||
|
tags = [Tags.links, Tags.movementMethod, Tags.recyclerView]
|
||||||
|
)
|
||||||
|
class DisableImplicitMovementMethodPluginSample : MarkwonTextViewSample() {
|
||||||
|
override fun render() {
|
||||||
|
val md = """
|
||||||
|
# Disable implicit movement method via plugin
|
||||||
|
We can disable implicit movement method via `MovementMethodPlugin` —
|
||||||
|
[link-that-is-not-clickable](https://noties.io)
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(MovementMethodPlugin.none())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md)
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,9 @@ import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
|||||||
@MarkwonSampleInfo(
|
@MarkwonSampleInfo(
|
||||||
id = "202006179081256",
|
id = "202006179081256",
|
||||||
title = "Disable implicit movement method",
|
title = "Disable implicit movement method",
|
||||||
description = "Configure `Markwon` to **not** apply implicit movement method",
|
description = "Configure `Markwon` to **not** apply implicit movement method, " +
|
||||||
|
"which consumes touch events when used in a `RecyclerView` even when " +
|
||||||
|
"markdown does not contain links",
|
||||||
artifacts = [MarkwonArtifact.CORE],
|
artifacts = [MarkwonArtifact.CORE],
|
||||||
tags = [Tags.plugin, Tags.movementMethod, Tags.links, Tags.recyclerView]
|
tags = [Tags.plugin, Tags.movementMethod, Tags.links, Tags.recyclerView]
|
||||||
)
|
)
|
||||||
|
@ -11,7 +11,7 @@ import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
|||||||
title = "Implicit movement method",
|
title = "Implicit movement method",
|
||||||
description = "By default movement method is applied for links to be clickable",
|
description = "By default movement method is applied for links to be clickable",
|
||||||
artifacts = [MarkwonArtifact.CORE],
|
artifacts = [MarkwonArtifact.CORE],
|
||||||
tags = [Tags.movementMethod, Tags.links]
|
tags = [Tags.movementMethod, Tags.links, Tags.defaults]
|
||||||
)
|
)
|
||||||
class ImplicitMovementMethodSample : MarkwonTextViewSample() {
|
class ImplicitMovementMethodSample : MarkwonTextViewSample() {
|
||||||
override fun render() {
|
override fun render() {
|
||||||
|
75
app-sample/src/main/res/layout/activity_edit_text.xml
Normal file
75
app-sample/src/main/res/layout/activity_edit_text.xml
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dip"
|
||||||
|
tools:ignore="HardcodedText">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0px"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:autofillHints="none"
|
||||||
|
android:hint="Markdown..."
|
||||||
|
android:inputType="text|textLongMessage|textMultiLine"
|
||||||
|
android:maxLines="100" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:ignore="ButtonStyle">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/bold"
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="B"
|
||||||
|
android:typeface="monospace" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/italic"
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="I"
|
||||||
|
android:typeface="monospace" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/strike"
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="S"
|
||||||
|
android:typeface="monospace" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/quote"
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text=">"
|
||||||
|
android:typeface="monospace" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/code"
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="`"
|
||||||
|
android:typeface="monospace" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -5,4 +5,16 @@
|
|||||||
<string name="tab_bar_preview">Preview</string>
|
<string name="tab_bar_preview">Preview</string>
|
||||||
<string name="tab_bar_code">Code</string>
|
<string name="tab_bar_code">Code</string>
|
||||||
|
|
||||||
|
<string name="lorem"><![CDATA[
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.
|
||||||
|
|
||||||
|
Sed eu enim neque. Maecenas dictum faucibus ullamcorper. In ullamcorper orci in neque varius, nec rutrum nisl eleifend. Vestibulum tincidunt, ipsum at porta suscipit, est nibh commodo ex, et ultrices eros lacus vel neque. Praesent nulla velit, hendrerit sed sodales at, feugiat non lectus. Vivamus vel ultricies mi. Ut finibus commodo feugiat. Sed tempor lorem tortor, tempor sodales leo varius id. Curabitur rutrum sem at euismod rhoncus. Ut iaculis sem pharetra neque accumsan vestibulum. Nunc ultrices pharetra massa, at luctus nulla maximus et. Donec rhoncus in nisi eu pellentesque.
|
||||||
|
|
||||||
|
Sed consequat convallis massa quis bibendum. Phasellus vel suscipit velit. Pellentesque vel nisi at nisi facilisis condimentum. Cras feugiat magna ex, ut ultricies eros porttitor id. Quisque iaculis rutrum arcu eget placerat. Vestibulum pellentesque, urna eget consectetur commodo, est metus gravida nisl, id lacinia ligula ipsum porta nulla. Etiam aliquam convallis sollicitudin. Etiam sit amet mi aliquet purus faucibus hendrerit pharetra eu quam. Cras ut ornare sapien. Nam sapien diam, porttitor eu sagittis nec, vehicula nec mi. In fringilla turpis nec nisi fringilla, a facilisis eros ultrices. Proin eget arcu velit.
|
||||||
|
|
||||||
|
Sed gravida auctor malesuada. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus non justo sem. Donec dictum a elit quis pretium. Fusce accumsan sodales ornare. Nunc facilisis ligula eu ultrices faucibus. Proin vel molestie augue, ut convallis enim. Curabitur efficitur eget urna quis tempor. In non arcu non ex vulputate pulvinar. In laoreet aliquam mauris. Suspendisse vulputate magna at lorem bibendum, quis dapibus sapien malesuada. Curabitur at leo sit amet est egestas vestibulum. Sed hendrerit mi vel massa vestibulum, non semper nisl iaculis. Pellentesque feugiat at dolor a viverra. Sed ut consectetur tellus. Maecenas venenatis nunc a arcu convallis, at semper nulla cursus.
|
||||||
|
|
||||||
|
Curabitur placerat neque a congue pulvinar. Nulla non commodo est. Aenean nec gravida odio. Cras tincidunt accumsan pulvinar. Vestibulum non imperdiet velit. Sed ut mollis velit, vel ornare metus. Morbi consequat mi quis dui consectetur, sed condimentum lacus pulvinar.
|
||||||
|
]]></string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user