From efe7a897146acccf82e02ec3f9665b8a5f26df27 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Mon, 22 Jul 2019 19:08:55 +0200 Subject: [PATCH] preparing for markdown editor --- .../github/di/modules/FragmentModule.kt | 3 +- markdown/build.gradle | 1 + .../fastaccess/markdown/MarkdownProvider.kt | 261 +++++++++++++++++- .../markdown/widget/MarkdownLayout.kt | 86 ++++++ .../res/layout/markdown_buttons_layout.xml | 214 ++++++++++++++ 5 files changed, 553 insertions(+), 12 deletions(-) create mode 100644 markdown/src/main/java/com/fastaccess/markdown/widget/MarkdownLayout.kt create mode 100644 markdown/src/main/res/layout/markdown_buttons_layout.xml diff --git a/app/src/main/java/com/fastaccess/github/di/modules/FragmentModule.kt b/app/src/main/java/com/fastaccess/github/di/modules/FragmentModule.kt index e011e332..e0e83f04 100644 --- a/app/src/main/java/com/fastaccess/github/di/modules/FragmentModule.kt +++ b/app/src/main/java/com/fastaccess/github/di/modules/FragmentModule.kt @@ -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)) { diff --git a/markdown/build.gradle b/markdown/build.gradle index 7bf1b91e..fd54051e 100644 --- a/markdown/build.gradle +++ b/markdown/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { diff --git a/markdown/src/main/java/com/fastaccess/markdown/MarkdownProvider.kt b/markdown/src/main/java/com/fastaccess/markdown/MarkdownProvider.kt index c81cd05a..d3bfa60b 100644 --- a/markdown/src/main/java/com/fastaccess/markdown/MarkdownProvider.kt +++ b/markdown/src/main/java/com/fastaccess/markdown/MarkdownProvider.kt @@ -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 = "" - private const val TOGGLE_END = "" - private const val REPLY_START = "
" - private const val REPLY_END = "
" - private const val SIGNATURE_START = "
" - private const val SIGNATURE_END = "
" - private const val HIDDEN_REPLY_START = "
" - private const val HIDDEN_REPLY_END = "
" - private const val BREAK = "
" - private const val PARAGRAPH_START = "

" - private const val PARAGRAPH_END = "

" + 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) + } + } } \ No newline at end of file diff --git a/markdown/src/main/java/com/fastaccess/markdown/widget/MarkdownLayout.kt b/markdown/src/main/java/com/fastaccess/markdown/widget/MarkdownLayout.kt new file mode 100644 index 00000000..e5c042f6 --- /dev/null +++ b/markdown/src/main/java/com/fastaccess/markdown/widget/MarkdownLayout.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/markdown/src/main/res/layout/markdown_buttons_layout.xml b/markdown/src/main/res/layout/markdown_buttons_layout.xml new file mode 100644 index 00000000..c95a6790 --- /dev/null +++ b/markdown/src/main/res/layout/markdown_buttons_layout.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file