Add markdown support

pull/1459/head
Alex Baker 3 years ago
parent 9960e9a0a9
commit c4e98052de

@ -185,6 +185,12 @@ dependencies {
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
implementation("androidx.paging:paging-runtime:2.1.2")
implementation("io.noties.markwon:core:${Versions.markwon}")
implementation("io.noties.markwon:editor:${Versions.markwon}")
implementation("io.noties.markwon:ext-tasklist:${Versions.markwon}")
implementation("io.noties.markwon:ext-strikethrough:${Versions.markwon}")
implementation("io.noties.markwon:ext-tables:${Versions.markwon}")
implementation("io.noties.markwon:linkify:${Versions.markwon}")
implementation("me.saket:better-link-movement-method:2.2.0")
debugImplementation("com.facebook.flipper:flipper:${Versions.flipper}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${Versions.flipper}")

@ -916,3 +916,47 @@
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/activity#1.3.0-alpha04
- artifact: io.noties.markwon:editor:+
name: editor
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: io.noties.markwon:ext-tasklist:+
name: ext-tasklist
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: io.noties.markwon:ext-strikethrough:+
name: ext-strikethrough
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:+
name: commonmark-ext-gfm-strikethrough
copyrightHolder: Atlassian and others
license: BSD 2-Clause
- artifact: me.saket:better-link-movement-method:+
name: better-link-movement-method
copyrightHolder: Saket Narayan
license: Apache License v2
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0
url: https://github.com/Saketme/BetterLinkMovementMethod
- artifact: io.noties.markwon:linkify:+
name: linkify
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: io.noties.markwon:ext-tables:+
name: ext-tables
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: com.atlassian.commonmark:commonmark-ext-gfm-tables:+
name: commonmark-ext-gfm-tables
copyrightHolder: Atlassian and others
license: BSD 2-Clause

@ -2175,6 +2175,114 @@
"normalizedLicense": "apache2",
"url": "https://developer.android.com/jetpack/androidx/releases/activity#1.3.0-alpha04",
"libraryName": "activity-compose"
},
{
"artifactId": {
"name": "editor",
"group": "io.noties.markwon",
"version": "+"
},
"copyrightHolder": "Dimitry Ivanov",
"copyrightStatement": "Copyright © Dimitry Ivanov. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "https://github.com/noties/Markwon",
"libraryName": "editor"
},
{
"artifactId": {
"name": "ext-tasklist",
"group": "io.noties.markwon",
"version": "+"
},
"copyrightHolder": "Dimitry Ivanov",
"copyrightStatement": "Copyright © Dimitry Ivanov. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "https://github.com/noties/Markwon",
"libraryName": "ext-tasklist"
},
{
"artifactId": {
"name": "ext-strikethrough",
"group": "io.noties.markwon",
"version": "+"
},
"copyrightHolder": "Dimitry Ivanov",
"copyrightStatement": "Copyright © Dimitry Ivanov. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "https://github.com/noties/Markwon",
"libraryName": "ext-strikethrough"
},
{
"artifactId": {
"name": "commonmark-ext-gfm-strikethrough",
"group": "com.atlassian.commonmark",
"version": "+"
},
"copyrightHolder": "Atlassian and others",
"copyrightStatement": "Copyright © Atlassian and others. All rights reserved.",
"license": "BSD 2-Clause",
"normalizedLicense": "bsd_2_clauses",
"libraryName": "commonmark-ext-gfm-strikethrough"
},
{
"artifactId": {
"name": "better-link-movement-method",
"group": "me.saket",
"version": "+"
},
"copyrightHolder": "Saket Narayan",
"copyrightStatement": "Copyright © Saket Narayan. All rights reserved.",
"license": "Apache License v2",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0",
"normalizedLicense": "apache2",
"url": "https://github.com/Saketme/BetterLinkMovementMethod",
"libraryName": "better-link-movement-method"
},
{
"artifactId": {
"name": "linkify",
"group": "io.noties.markwon",
"version": "+"
},
"copyrightHolder": "Dimitry Ivanov",
"copyrightStatement": "Copyright © Dimitry Ivanov. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "https://github.com/noties/Markwon",
"libraryName": "linkify"
},
{
"artifactId": {
"name": "ext-tables",
"group": "io.noties.markwon",
"version": "+"
},
"copyrightHolder": "Dimitry Ivanov",
"copyrightStatement": "Copyright © Dimitry Ivanov. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "https://github.com/noties/Markwon",
"libraryName": "ext-tables"
},
{
"artifactId": {
"name": "commonmark-ext-gfm-tables",
"group": "com.atlassian.commonmark",
"version": "+"
},
"copyrightHolder": "Atlassian and others",
"copyrightStatement": "Copyright © Atlassian and others. All rights reserved.",
"license": "BSD 2-Clause",
"normalizedLicense": "bsd_2_clauses",
"libraryName": "commonmark-ext-gfm-tables"
}
]
}

@ -37,6 +37,8 @@ import com.todoroo.astrid.repeats.RepeatControlSet
import com.todoroo.astrid.timers.TimerPlugin
import com.todoroo.astrid.ui.StartDateControlSet
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.editor.MarkwonEditor
import io.noties.markwon.editor.MarkwonEditorTextWatcher
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -49,6 +51,7 @@ import org.tasks.databinding.FragmentTaskEditBinding
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.markwon
import org.tasks.extensions.Context.openUri
import org.tasks.files.FileHelper
import org.tasks.fragments.TaskEditControlSetFragmentManager
@ -58,6 +61,7 @@ import org.tasks.themes.ThemeColor
import org.tasks.ui.SubtaskControlSet
import org.tasks.ui.TaskEditControlFragment
import org.tasks.ui.TaskEditViewModel
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlin.math.abs
@ -138,14 +142,28 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
toolbar.setOnMenuItemClickListener(this)
themeColor.apply(binding.collapsingtoolbarlayout, toolbar)
val title = binding.title
val markdown = if (preferences.markdown) {
MarkwonEditorTextWatcher.withPreRender(
MarkwonEditor.create(requireContext().markwon),
Executors.newCachedThreadPool(),
title
)
} else {
null
}
title.addTextChangedListener(
onTextChanged = { _, _, _, _ ->
editViewModel.title = title.text.toString().trim { it <= ' ' }
},
afterTextChanged = {
markdown?.afterTextChanged(it)
}
)
title.setText(model.title)
title.setHorizontallyScrolling(false)
title.setTextColor(themeColor.colorOnPrimary)
title.setHintTextColor(themeColor.hintOnPrimary)
title.maxLines = 5
title.addTextChangedListener {
editViewModel.title = title.text.toString().trim { it <= ' ' }
}
if (model.isNew || preferences.getBoolean(R.string.p_hide_check_button, false)) {
binding.fab.visibility = View.INVISIBLE
} else if (editViewModel.completed!!) {

@ -87,7 +87,7 @@ class CommentsController @Inject constructor(
// name
val nameView = view.findViewById<TextView>(R.id.title)
nameView.text = Html.fromHtml(item.message)
Linkify.safeLinkify(nameView, android.text.util.Linkify.ALL)
Linkify.safeLinkify(nameView)
// date
val date = view.findViewById<TextView>(R.id.date)

@ -4,16 +4,13 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.URLSpan
import android.text.util.Linkify
import android.view.View
import android.view.MotionEvent
import android.widget.TextView
import androidx.core.text.util.LinkifyCompat
import dagger.hilt.android.qualifiers.ActivityContext
import me.saket.bettermovementmethod.BetterLinkMovementMethod
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import timber.log.Timber
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
@ -23,79 +20,81 @@ class Linkify @Inject constructor(
@ActivityContext private val context: Context,
private val dialogBuilder: DialogBuilder
) {
fun linkify(textView: TextView, onClick: Runnable = Runnable {}) {
if (textView.length() == 0) {
fun linkify(tv: TextView) {
if (tv.length() == 0) {
return
}
safeLinkify(textView, Linkify.ALL)
textView.setOnClickListener {
if (textView.selectionStart == -1 && textView.selectionEnd == -1) {
onClick.run()
}
safeLinkify(tv)
setMovementMethod(tv)
}
fun setMovementMethod(tv: TextView, handle: (() -> Unit)? = null) {
val blmm = BetterLinkMovementMethod.newInstance().apply {
setOnLinkClickListener { _, url -> onClick(url, handle ?: {}) }
setOnLinkLongClickListener { _, _ -> tv.performLongClick() }
}
val text = textView.text
if (text is SpannableStringBuilder || text is SpannableString) {
val spannable = text as Spannable
val spans = spannable.getSpans(0, text.length, URLSpan::class.java)
for (span in spans) {
val start = spannable.getSpanStart(span)
val end = spannable.getSpanEnd(span)
spannable.removeSpan(span)
spannable.setSpan(ClickHandlingURLSpan(span.url, onClick), start, end, 0)
tv.movementMethod = blmm
tv.setOnTouchListener { _, event ->
val text = tv.text
when {
text is Spannable && blmm.onTouchEvent(tv, text, event) -> true
event.action == MotionEvent.ACTION_UP -> {
handle?.invoke()
false
}
else -> false
}
}
}
private inner class ClickHandlingURLSpan constructor(
url: String?,
private val onEdit: Runnable
) : URLSpan(url) {
override fun onClick(widget: View) {
var title: String?
val edit = context.getString(R.string.TAd_actionEditTask)
val action: String
val uri = Uri.parse(url)
var scheme = uri.scheme
if (isNullOrEmpty(scheme)) {
scheme = ""
fun onClick(url: String, onEdit: () -> Unit): Boolean {
var title: String?
val edit = context.getString(R.string.TAd_actionEditTask)
val action: String
val uri = Uri.parse(url).let {
if (it.scheme.isNullOrBlank()) {
Uri.parse("https://$url")
} else {
it
}
when (scheme) {
"tel" -> {
title = uri.encodedSchemeSpecificPart
action = context.getString(R.string.action_call)
}
"mailto" -> {
title = uri.encodedSchemeSpecificPart
action = context.getString(R.string.action_open)
}
"geo" -> {
title = uri.encodedQuery!!.replaceFirst("q=".toRegex(), "")
try {
title = URLDecoder.decode(title, "utf-8")
} catch (ignored: UnsupportedEncodingException) {
}
action = context.getString(R.string.action_open)
}
else -> {
title = url
action = context.getString(R.string.action_open)
}
}
when (uri.scheme) {
"tel" -> {
title = uri.encodedSchemeSpecificPart
action = context.getString(R.string.action_call)
}
dialogBuilder
.newDialog(title)
.setItems(listOf(action, edit)) { _, selected ->
if (selected == 0) {
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
} else {
onEdit.run()
}
"mailto" -> {
title = uri.encodedSchemeSpecificPart
action = context.getString(R.string.action_open)
}
"geo" -> {
title = uri.encodedQuery!!.replaceFirst("q=".toRegex(), "")
try {
title = URLDecoder.decode(title, "utf-8")
} catch (ignored: UnsupportedEncodingException) {
}
.show()
action = context.getString(R.string.action_open)
}
else -> {
title = url
action = context.getString(R.string.action_open)
}
}
dialogBuilder
.newDialog(title)
.setItems(listOf(action, edit)) { _, selected ->
if (selected == 0) {
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
} else {
onEdit()
}
}
.show()
return true
}
companion object {
fun safeLinkify(textView: TextView?, mask: Int) {
fun safeLinkify(textView: TextView?, mask: Int = Linkify.ALL) {
try {
LinkifyCompat.addLinks(textView!!, mask)
} catch (e: UnsatisfiedLinkError) {

@ -8,13 +8,13 @@ import android.text.method.LinkMovementMethod
import android.view.View
import androidx.fragment.app.DialogFragment
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.Markwon
import org.tasks.R
import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity
import org.tasks.databinding.DialogWhatsNewBinding
import org.tasks.extensions.Context.markwon
import org.tasks.extensions.Context.openUri
import org.tasks.preferences.Preferences
import java.io.BufferedReader
@ -37,9 +37,8 @@ class WhatsNewDialog : DialogFragment() {
val textStream = requireContext().assets.open("CHANGELOG.md")
val text = BufferedReader(textStream.reader()).readText()
val markwon = Markwon.builder(requireContext()).build()
binding.changelog.movementMethod = LinkMovementMethod.getInstance()
binding.changelog.text = markwon.toMarkdown(text)
requireContext().markwon.setMarkdown(binding.changelog, text)
val begForSubscription = !inventory.hasPro
val begForRating = !preferences.getBoolean(R.string.p_clicked_rate, false)

@ -7,6 +7,11 @@ import android.content.Intent.ACTION_VIEW
import android.net.Uri
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import io.noties.markwon.Markwon
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.linkify.LinkifyPlugin
import org.tasks.R
object Context {
@ -46,4 +51,18 @@ object Context {
fun Context.toast(text: String?, duration: Int = Toast.LENGTH_LONG) =
text?.let { Toast.makeText(this, it, duration).show() }
val Context.markwon: Markwon
get() =
Markwon
.builder(this)
.usePlugins(
listOf(
TaskListPlugin.create(this),
TablePlugin.create(this),
LinkifyPlugin.create(android.text.util.Linkify.ALL, true),
StrikethroughPlugin.create()
)
)
.build()
}

@ -486,6 +486,9 @@ class Preferences @JvmOverloads constructor(
fun showGroupHeaders(): Boolean =
!usePagedQueries() && !getBoolean(R.string.p_disable_sort_groups, false)
val markdown: Boolean
get() = getBoolean(R.string.p_markdown, false)
companion object {
private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$

@ -19,6 +19,7 @@ import org.tasks.data.TaskContainer
import org.tasks.databinding.TaskAdapterRowBinding
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.markwon
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils.startOfDay
import org.tasks.ui.CheckBoxProvider
@ -45,7 +46,7 @@ class TaskViewHolder internal constructor(
private val linkify: Linkify,
private val locale: Locale
) : RecyclerView.ViewHolder(binding.root) {
private val markwon = if (preferences.markdown) context.markwon else null
private val row: ViewGroup = binding.row
private val dueDate: TextView = binding.dueDate.apply {
setOnClickListener { changeDueDate() }
@ -129,17 +130,29 @@ class TaskViewHolder internal constructor(
fun bindView(task: TaskContainer, filter: Filter, sortMode: Int) {
this.task = task
indent = task.indent
nameView.text = task.title
if (markwon == null) {
nameView.text = task.title
} else {
task.title?.let { markwon.setMarkdown(nameView, it) }
}
setupTitleAndCheckbox()
setupDueDate(sortMode == SORT_DUE)
setupChips(filter, sortMode == SORT_START)
if (preferences.getBoolean(R.string.p_show_description, true)) {
description.text = task.notes
if (markwon == null) {
description.text = task.notes
} else {
task.notes?.let { markwon.setMarkdown(description, it) }
}
description.visibility = if (task.hasNotes()) View.VISIBLE else View.GONE
}
if (preferences.getBoolean(R.string.p_linkify_task_list, false)) {
linkify.linkify(nameView) { onRowBodyClick() }
linkify.linkify(description) { onRowBodyClick() }
linkify.setMovementMethod(nameView) { onRowBodyClick() }
linkify.setMovementMethod(description) { onRowBodyClick() }
if (markwon == null) {
Linkify.safeLinkify(nameView)
Linkify.safeLinkify(description)
}
nameView.setOnLongClickListener { onRowBodyLongClick() }
description.setOnLongClickListener { onRowBodyLongClick() }
}

@ -5,10 +5,14 @@ import android.view.ViewGroup
import android.widget.EditText
import androidx.core.widget.addTextChangedListener
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.editor.MarkwonEditor
import io.noties.markwon.editor.MarkwonEditorTextWatcher
import org.tasks.R
import org.tasks.databinding.ControlSetDescriptionBinding
import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.markwon
import org.tasks.preferences.Preferences
import java.util.concurrent.Executors
import javax.inject.Inject
@AndroidEntryPoint
@ -27,11 +31,20 @@ class DescriptionControlSet : TaskEditControlFragment() {
override fun bind(parent: ViewGroup?) =
ControlSetDescriptionBinding.inflate(layoutInflater, parent, true).let {
editText = it.notes.apply {
addTextChangedListener(
onTextChanged = { text, _, _, _ -> textChanged(text) }
editText = it.notes
val markdown = if (preferences.markdown) {
MarkwonEditorTextWatcher.withPreRender(
MarkwonEditor.create(requireContext().markwon),
Executors.newCachedThreadPool(),
editText
)
} else {
null
}
editText.addTextChangedListener(
onTextChanged = { text, _, _, _ -> textChanged(text) },
afterTextChanged = { editable -> markdown?.afterTextChanged(editable) }
)
it.root
}

@ -46,7 +46,7 @@
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/completeBox"
android:layout_toStartOf="@id/due_date"
@ -66,7 +66,7 @@
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_marginTop="@dimen/task_list_item_spacing"

@ -444,4 +444,5 @@
<string name="p_map_theme">map_theme</string>
<string name="p_picker_mode_date">picker_mode_date</string>
<string name="p_picker_mode_time">picker_mode_time</string>
<string name="p_markdown">markdown</string>
</resources>

@ -724,4 +724,6 @@ File %1$s contained %2$s.\n\n
<string name="upgrade_automation">Automation</string>
<string name="upgrade_automation_description">Plugins for Tasker, Automate, and Locale</string>
<string name="more_options">More options</string>
<string name="markdown">Markdown</string>
<string name="markdown_description">Enable Markdown in title and description</string>
</resources>

@ -172,6 +172,12 @@
android:targetPackage="@string/app_package" />
</Preference>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/p_markdown"
android:summary="@string/markdown_description"
android:title="@string/markdown" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/p_linkify_task_edit"

@ -292,6 +292,21 @@
++--- io.noties.markwon:core:4.6.2
+| +--- androidx.annotation:annotation:1.1.0 -> 1.2.0
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- io.noties.markwon:editor:4.6.2
+| \--- io.noties.markwon:core:4.6.2 (*)
++--- io.noties.markwon:ext-tasklist:4.6.2
+| \--- io.noties.markwon:core:4.6.2 (*)
++--- io.noties.markwon:ext-strikethrough:4.6.2
+| +--- io.noties.markwon:core:4.6.2 (*)
+| \--- com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.13.0
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- io.noties.markwon:ext-tables:4.6.2
+| +--- io.noties.markwon:core:4.6.2 (*)
+| \--- com.atlassian.commonmark:commonmark-ext-gfm-tables:0.13.0
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- io.noties.markwon:linkify:4.6.2
+| \--- io.noties.markwon:core:4.6.2 (*)
++--- me.saket:better-link-movement-method:2.2.0
++--- org.jetbrains.kotlin:kotlin-stdlib:1.4.32 (*)
++--- org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.4
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.30 -> 1.4.32 (*)

@ -409,6 +409,21 @@
++--- io.noties.markwon:core:4.6.2
+| +--- androidx.annotation:annotation:1.1.0 -> 1.2.0
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- io.noties.markwon:editor:4.6.2
+| \--- io.noties.markwon:core:4.6.2 (*)
++--- io.noties.markwon:ext-tasklist:4.6.2
+| \--- io.noties.markwon:core:4.6.2 (*)
++--- io.noties.markwon:ext-strikethrough:4.6.2
+| +--- io.noties.markwon:core:4.6.2 (*)
+| \--- com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.13.0
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- io.noties.markwon:ext-tables:4.6.2
+| +--- io.noties.markwon:core:4.6.2 (*)
+| \--- com.atlassian.commonmark:commonmark-ext-gfm-tables:0.13.0
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- io.noties.markwon:linkify:4.6.2
+| \--- io.noties.markwon:core:4.6.2 (*)
++--- me.saket:better-link-movement-method:2.2.0
++--- org.jetbrains.kotlin:kotlin-stdlib:1.4.32 (*)
++--- org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.4
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.30 -> 1.4.32 (*)

Loading…
Cancel
Save