preparing for markdown editor

This commit is contained in:
k0shk0sh 2019-07-22 19:08:55 +02:00
parent fa0f19cb21
commit efe7a89714
5 changed files with 553 additions and 12 deletions

View File

@ -2,6 +2,7 @@ package com.fastaccess.github.di.modules
import android.annotation.SuppressLint
import android.content.Context
import android.text.util.Linkify
import com.fastaccess.data.storage.FastHubSharedPreference
import com.fastaccess.github.R
import com.fastaccess.github.base.engine.ThemeEngine
@ -47,7 +48,7 @@ class FragmentModule {
.usePlugin(GlideImagesPlugin.create(context))
.usePlugin(TablePlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(LinkifyPlugin.create())
.usePlugin(LinkifyPlugin.create(Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS))
.usePlugin(
SyntaxHighlightPlugin.create(
Prism4j(GrammarLocatorDef()), if (ThemeEngine.isLightTheme(preference.theme)) {

View File

@ -1,5 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {

View File

@ -1,5 +1,7 @@
package com.fastaccess.markdown
import android.webkit.MimeTypeMap
import android.widget.EditText
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.core.view.doOnPreDraw
@ -15,17 +17,14 @@ import org.commonmark.renderer.html.HtmlRenderer
@PrismBundle(includeAll = true)
object MarkdownProvider {
private const val TOGGLE_START = "<span class=\"email-hidden-toggle\">"
private const val TOGGLE_END = "</span>"
private const val REPLY_START = "<div class=\"email-quoted-reply\">"
private const val REPLY_END = "</div>"
private const val SIGNATURE_START = "<div class=\"email-signature-reply\">"
private const val SIGNATURE_END = "</div>"
private const val HIDDEN_REPLY_START = "<div class=\"email-hidden-reply\" style=\" display:none\">"
private const val HIDDEN_REPLY_END = "</div>"
private const val BREAK = "<br>"
private const val PARAGRAPH_START = "<p>"
private const val PARAGRAPH_END = "</p>"
private val IMAGE_EXTENSIONS = arrayOf(".png", ".jpg", ".jpeg", ".gif", ".svg")
private val MARKDOWN_EXTENSIONS = arrayOf(".md", ".mkdn", ".mdwn", ".mdown", ".markdown", ".mkd", ".mkdown", ".ron", ".rst", "adoc")
private val ARCHIVE_EXTENSIONS = arrayOf(
".zip", ".7z", ".rar", ".tar.gz", ".tgz", ".tar.Z", ".tar.bz2", ".tbz2", ".tar.lzma", ".tlz", ".apk", ".jar", ".dmg", ".pdf", ".ico", ".docx",
".doc", ".xlsx", ".hwp", ".pptx", ".show", ".mp3", ".ogg", ".ipynb"
)
fun loadIntoTextView(
markwon: Markwon,
@ -64,4 +63,244 @@ object MarkdownProvider {
) {
markwon.setMarkdown(textView, html)
}
fun addList(
editText: EditText,
list: String
) {
val tag = "$list "
val source = editText.text.toString()
var selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
var substring = source.substring(0, selectionStart)
val line = substring.lastIndexOf(char = 10.toChar())
if (line != -1) {
selectionStart = line + 1
} else {
selectionStart = 0
}
substring = source.substring(selectionStart, selectionEnd)
val split = substring.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val stringBuffer = StringBuilder()
if (split.isNotEmpty())
for (s in split) {
if (s.isEmpty() && stringBuffer.isNotEmpty()) {
stringBuffer.append("\n")
continue
}
if (!s.trim { it <= ' ' }.startsWith(tag)) {
if (stringBuffer.isNotEmpty()) stringBuffer.append("\n")
stringBuffer.append(tag).append(s)
} else {
if (stringBuffer.isNotEmpty()) stringBuffer.append("\n")
stringBuffer.append(s)
}
}
if (stringBuffer.isEmpty()) {
stringBuffer.append(tag)
}
editText.text.replace(selectionStart, selectionEnd, stringBuffer.toString())
editText.setSelection(stringBuffer.length + selectionStart)
}
fun addHeader(
editText: EditText,
level: Int
) {
val source = editText.text.toString()
val selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
val result = StringBuilder()
val substring = source.substring(selectionStart, selectionEnd)
if (!hasNewLine(source, selectionStart)) result.append("\n")
for (i in 0..level) result.append("#")
result.append(" ").append(substring)
editText.text.replace(selectionStart, selectionEnd, result.toString())
editText.setSelection(selectionStart + result.length)
}
fun addItalic(editText: EditText) {
val source = editText.text.toString()
val selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
val substring = source.substring(selectionStart, selectionEnd)
val result = "_" + substring + "_ "
editText.text.replace(selectionStart, selectionEnd, result)
editText.setSelection(result.length + selectionStart - 2)
}
fun addBold(editText: EditText) {
val source = editText.text.toString()
val selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
val substring = source.substring(selectionStart, selectionEnd)
val result = "**$substring** "
editText.text.replace(selectionStart, selectionEnd, result)
editText.setSelection(result.length + selectionStart - 3)
}
fun addCode(editText: EditText) {
try {
val source = editText.text.toString()
val selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
val substring = source.substring(selectionStart, selectionEnd)
val result: String
result = if (hasNewLine(source, selectionStart))
"```\n$substring\n```\n"
else
"\n```\n$substring\n```\n"
editText.text.replace(selectionStart, selectionEnd, result)
editText.setSelection(result.length + selectionStart - 5)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun addInlinleCode(editText: EditText) {
val source = editText.text.toString()
val selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
val substring = source.substring(selectionStart, selectionEnd)
val result = "`$substring` "
editText.text.replace(selectionStart, selectionEnd, result)
editText.setSelection(result.length + selectionStart - 2)
}
fun addStrikeThrough(editText: EditText) {
val source = editText.text.toString()
val selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
val substring = source.substring(selectionStart, selectionEnd)
val result = "~~$substring~~ "
editText.text.replace(selectionStart, selectionEnd, result)
editText.setSelection(result.length + selectionStart - 3)
}
fun addQuote(editText: EditText) {
val source = editText.text.toString()
val selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
val substring = source.substring(selectionStart, selectionEnd)
val result: String
result = if (hasNewLine(source, selectionStart)) {
"> $substring"
} else {
"\n> $substring"
}
editText.text.replace(selectionStart, selectionEnd, result)
editText.setSelection(result.length + selectionStart)
}
fun addDivider(editText: EditText) {
val source = editText.text.toString()
val selectionStart = editText.selectionStart
val result: String
result = if (hasNewLine(source, selectionStart)) {
"-------\n"
} else {
"\n-------\n"
}
editText.text.replace(selectionStart, selectionStart, result)
editText.setSelection(result.length + selectionStart)
}
fun addPhoto(
editText: EditText,
title: String,
link: String
) {
val result = "![$title]($link)"
insertAtCursor(editText, result)
}
fun addLink(
editText: EditText,
title: String,
link: String
) {
val result = "[$title]($link)"
insertAtCursor(editText, result)
}
private fun hasNewLine(
source: String,
selectionStart: Int
): Boolean {
var _source = source
try {
if (_source.isEmpty()) return true
_source = _source.substring(0, selectionStart)
return _source[_source.length - 1].toInt() == 10
} catch (e: StringIndexOutOfBoundsException) {
return false
}
}
fun isImage(name: String?): Boolean {
var name = name
if (name.isNullOrEmpty()) return false
name = name.toLowerCase()
for (value in IMAGE_EXTENSIONS) {
val extension = MimeTypeMap.getFileExtensionFromUrl(name)
if (extension != null && value.replace(".", "") == extension || name.endsWith(value)) return true
}
return false
}
fun isMarkdown(name: String?): Boolean {
var _name = name
if (_name.isNullOrEmpty()) return false
_name = _name.toLowerCase()
for (value in MARKDOWN_EXTENSIONS) {
val extension = MimeTypeMap.getFileExtensionFromUrl(_name)
if (extension != null && value.replace(".", "") == extension ||
_name.equals("README", ignoreCase = true) || _name.endsWith(value)
)
return true
}
return false
}
fun isArchive(name: String?): Boolean {
var _name = name
if (_name.isNullOrEmpty()) return false
_name = _name.toLowerCase()
for (value in ARCHIVE_EXTENSIONS) {
val extension = MimeTypeMap.getFileExtensionFromUrl(_name)
if (extension != null && value.replace(".", "") == extension || _name.endsWith(value)) return true
}
return false
}
fun insertAtCursor(
editText: EditText,
text: String
) {
val oriContent = editText.text.toString()
val start = editText.selectionStart
val end = editText.selectionEnd
if (start >= 0 && end > 0 && start != end) {
editText.text = editText.text.replace(start, end, text)
} else {
val index = if (editText.selectionStart >= 0) editText.selectionStart else 0
val builder = StringBuilder(oriContent)
builder.insert(index, text)
editText.setText(builder.toString())
editText.setSelection(index + text.length)
}
}
}

View File

@ -0,0 +1,86 @@
package com.fastaccess.markdown.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import com.fastaccess.markdown.MarkdownProvider
import com.fastaccess.markdown.R
import kotlinx.android.synthetic.main.markdown_buttons_layout.view.*
/**
* Created by Kosh on 2019-07-22.
*/
class MarkdownLayout : LinearLayout, View.OnClickListener {
private lateinit var editText: EditText
constructor(context: Context?) : super(context)
constructor(
context: Context?,
attrs: AttributeSet?
) : super(context, attrs)
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
override fun onFinishInflate() {
super.onFinishInflate()
orientation = HORIZONTAL
View.inflate(context, R.layout.markdown_buttons_layout, this)
if (isInEditMode) return
}
fun init(editText: EditText) {
this.editText = editText
headerOne.setOnClickListener(this)
headerTwo.setOnClickListener(this)
headerThree.setOnClickListener(this)
bold.setOnClickListener(this)
italic.setOnClickListener(this)
strikethrough.setOnClickListener(this)
bullet.setOnClickListener(this)
header.setOnClickListener(this)
code.setOnClickListener(this)
numbered.setOnClickListener(this)
quote.setOnClickListener(this)
link.setOnClickListener(this)
image.setOnClickListener(this)
unCheckbox.setOnClickListener(this)
checkbox.setOnClickListener(this)
inlineCode.setOnClickListener(this)
addEmoji.setOnClickListener(this)
signature.setOnClickListener(this)
}
override fun onClick(v: View?) {
if (editText.selectionEnd == -1 || editText.selectionStart == -1) {
return
}
v?.let {
when (it.id) {
R.id.headerOne -> MarkdownProvider.addHeader(editText, 1)
R.id.headerTwo -> MarkdownProvider.addHeader(editText, 2)
R.id.headerThree -> MarkdownProvider.addHeader(editText, 3)
R.id.bold -> MarkdownProvider.addBold(editText)
R.id.italic -> MarkdownProvider.addItalic(editText)
R.id.strikethrough -> MarkdownProvider.addStrikeThrough(editText)
R.id.bullet -> MarkdownProvider.addList(editText, "-")
R.id.header -> MarkdownProvider.addDivider(editText)
R.id.code -> MarkdownProvider.addCode(editText)
R.id.numbered -> MarkdownProvider.addList(editText, "1")
R.id.quote -> MarkdownProvider.addQuote(editText)
R.id.link -> MarkdownProvider.addLink(editText, "", "")
R.id.image -> MarkdownProvider.addPhoto(editText, "", "")
R.id.unCheckbox -> MarkdownProvider.addList(editText, "- [x]")
R.id.checkbox -> MarkdownProvider.addList(editText, "- [ ]")
R.id.inlineCode -> MarkdownProvider.addInlinleCode(editText)
}
}
}
}

View File

@ -0,0 +1,214 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<HorizontalScrollView
android:id="@+id/editorIconsHolder"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:fadeScrollbars="true"
android:scrollbarFadeDuration="500"
android:scrollbarSize="1dp"
android:scrollbarStyle="outsideOverlay">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center"
android:gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/headerOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/header_one"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_header_one" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/headerTwo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/header_two"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_header_two" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/headerThree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/header_three"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_header_three" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/bold"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_format_bold" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/italic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/italic"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_format_italic" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/strikethrough"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/strike_through"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_format_strikethrough" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bullet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/bullet"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_format_list_bulleted" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/numbered"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/numbered_list"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_list_numbers" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/todo_checked"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_checkbox" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/unCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/todo_unchecked"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_checkbox_empty" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/divider"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_minus" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/inlineCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/code"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_inline_code" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/code"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_code" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/quote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/quote"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_format_quote" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/link"
android:padding="@dimen/spacing_micro"
android:src="@drawable/ic_insert_link" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/image"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_image" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/signature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/app_name"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_fasthub_mascot" />
</LinearLayout>
</HorizontalScrollView>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="?dividerColor" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/addEmoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/reactions"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_add_emoji" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/preview"
android:padding="@dimen/spacing_normal"
android:src="@drawable/ic_eye" />
</merge>