├── sample
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── menu
│ │ │ └── main_menu.xml
│ │ ├── color
│ │ │ └── style_button_colors.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout
│ │ │ └── activity_main.xml
│ │ └── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── yahiaangelo
│ │ └── markdownedittext
│ │ └── sample
│ │ └── MainActivity.kt
├── proguard-rules.pro
└── build.gradle
├── MarkdownEditText
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── yahiaangelo
│ │ │ │ └── markdownedittext
│ │ │ │ ├── model
│ │ │ │ ├── StyleButton.kt
│ │ │ │ └── EnhancedMovementMethod.kt
│ │ │ │ ├── MarkdownStylesBar.kt
│ │ │ │ ├── OrderedListItemSpan.kt
│ │ │ │ ├── adapter
│ │ │ │ └── StylesBarAdapter.kt
│ │ │ │ └── MarkdownEditText.kt
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── attrs.xml
│ │ │ │ └── ids.xml
│ │ │ ├── color
│ │ │ │ └── style_button_color.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_format_italic.xml
│ │ │ │ ├── ic_format_quote.xml
│ │ │ │ ├── ic_format_strikethrough.xml
│ │ │ │ ├── ic_format_clear.xml
│ │ │ │ ├── ic_check_box.xml
│ │ │ │ ├── ic_format_list_numbered.xml
│ │ │ │ ├── ic_format_bold.xml
│ │ │ │ ├── ic_format_list_bulleted.xml
│ │ │ │ └── ic_insert_link.xml
│ │ │ └── layout
│ │ │ │ ├── styles_bar_item.xml
│ │ │ │ └── link_input_layout.xml
│ │ └── AndroidManifest.xml
│ └── test
│ │ └── java
│ │ └── com
│ │ └── yahiaangelo
│ │ └── markdownedittext
│ │ └── MarkdownEditTextTest.kt
├── proguard-rules.pro
└── build.gradle
├── .github
└── FUNDING.yml
├── settings.gradle
├── preview
└── preview.gif
├── gradle
└── wrapper
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/MarkdownEditText/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/MarkdownEditText/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://paypal.me/YahiaMostafa"]
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':MarkdownEditText'
2 | rootProject.name = "MarkdownEditText"
--------------------------------------------------------------------------------
/preview/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/preview/preview.gif
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | sample
3 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YahiaAngelo/MarkdownEditText/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/java/com/yahiaangelo/markdownedittext/model/StyleButton.kt:
--------------------------------------------------------------------------------
1 | package com.yahiaangelo.markdownedittext.model
2 |
3 | class StyleButton(var icon: Int, var id: Int)
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #03A9F4
4 | #0288D1
5 | #448AFF
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Oct 18 21:40:57 EET 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
7 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #BDBDBD
4 | #2196F3
5 | #FFFFFF
6 | #212121
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/menu/main_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/src/main/res/color/style_button_colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/color/style_button_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_format_italic.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_format_quote.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_format_strikethrough.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_format_clear.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_check_box.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_format_list_numbered.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_format_bold.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_format_list_bulleted.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/layout/styles_bar_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/MarkdownEditText/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/drawable/ic_insert_link.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | .idea/workspace.xml
38 | .idea/tasks.xml
39 | .idea/gradle.xml
40 | .idea/dictionaries
41 | .idea/libraries
42 | .idea/modules.xml
43 | .idea/misc.xml
44 | .idea/assetWizardSettings.xml
45 |
46 | # Keystore files
47 | *.jks
48 |
49 | # External native build folder generated in Android Studio 2.2 and later
50 | .externalNativeBuild
51 |
52 | # Google Services (e.g. APIs or Firebase)
53 | google-services.json
54 |
55 | # Freeline
56 | freeline.py
57 | freeline/
58 | freeline_project_description.json
59 | .idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/sample/src/main/java/com/yahiaangelo/markdownedittext/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.yahiaangelo.markdownedittext.sample
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 | import android.text.SpannableStringBuilder
6 | import com.yahiaangelo.markdownedittext.MarkdownEditText
7 | import com.yahiaangelo.markdownedittext.sample.databinding.ActivityMainBinding
8 |
9 | class MainActivity : AppCompatActivity() {
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | val binding = ActivityMainBinding.inflate(layoutInflater)
13 | setContentView(binding.root)
14 | binding.edittext.setStylesBar(binding.stylesbar)
15 | binding.toolbar.setOnMenuItemClickListener {
16 | when(it.itemId){
17 | R.id.to_md -> binding.edittext.text = SpannableStringBuilder(binding.edittext.getMD())
18 | R.id.render_md -> binding.edittext.renderMD()
19 | }
20 | return@setOnMenuItemClickListener true
21 | }
22 | //Select specific Styles to show
23 | //binding.stylesbar.stylesList = arrayOf(MarkdownEditText.TextStyle.BOLD, MarkdownEditText.TextStyle.ITALIC)
24 | }
25 | }
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 30
7 | buildToolsVersion "30.0.2"
8 |
9 | defaultConfig {
10 | applicationId "com.yahiaangelo.markdownedittext.sample"
11 | minSdkVersion 23
12 | targetSdkVersion 30
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 |
26 | viewBinding {
27 | enabled = true
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation fileTree(dir: "libs", include: ["*.jar"])
33 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
34 | implementation 'androidx.core:core-ktx:1.3.0'
35 | implementation 'androidx.appcompat:appcompat:1.1.0'
36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
37 | implementation 'com.google.android.material:material:1.1.0-rc01'
38 | implementation(project(':MarkdownEditText'))
39 | testImplementation 'junit:junit:4.12'
40 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
42 |
43 | }
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/res/layout/link_input_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
21 |
22 |
23 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/MarkdownEditText/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'com.vanniktech.maven.publish'
5 |
6 |
7 | android {
8 | compileSdkVersion 31
9 | buildToolsVersion "30.0.3"
10 |
11 | defaultConfig {
12 | minSdkVersion 23
13 | targetSdkVersion 31
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | testOptions {
25 | unitTests {
26 | includeAndroidResources = true
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_1_8
31 | targetCompatibility JavaVersion.VERSION_1_8
32 | }
33 | kotlinOptions {
34 | jvmTarget = '1.8'
35 | }
36 | }
37 |
38 | dependencies {
39 | implementation fileTree(dir: "libs", include: ["*.jar"])
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
41 | implementation 'androidx.core:core-ktx:1.6.0'
42 | implementation 'androidx.appcompat:appcompat:1.3.1'
43 | testImplementation 'junit:junit:4.13'
44 | implementation 'com.google.android.material:material:1.4.0'
45 | implementation "io.noties.markwon:core:4.6.1"
46 | implementation "io.noties.markwon:html:4.6.1"
47 | implementation "io.noties.markwon:ext-strikethrough:4.6.1"
48 | implementation "io.noties.markwon:ext-tasklist:4.6.1"
49 | androidTestImplementation 'androidx.test:runner:1.2.0'
50 | androidTestImplementation 'androidx.test:rules:1.2.0'
51 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
52 | testImplementation 'org.robolectric:robolectric:4.4-alpha-3'
53 | }
54 |
55 | repositories {
56 | mavenCentral()
57 | }
58 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
21 |
22 |
23 |
35 |
36 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/test/java/com/yahiaangelo/markdownedittext/MarkdownEditTextTest.kt:
--------------------------------------------------------------------------------
1 | package com.yahiaangelo.markdownedittext
2 |
3 | import android.app.Activity
4 | import android.text.SpannableStringBuilder
5 | import androidx.core.view.children
6 | import org.junit.Before
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 | import org.robolectric.Robolectric
10 | import org.robolectric.RobolectricTestRunner
11 | import org.robolectric.android.controller.ActivityController
12 |
13 | //Must run with java9
14 | @RunWith(RobolectricTestRunner::class)
15 | class MarkdownEditTextTest {
16 |
17 | private lateinit var activityController: ActivityController
18 | private lateinit var activity: Activity
19 | private lateinit var markdownEditText: MarkdownEditText
20 | private lateinit var markdownStylesBar: MarkdownStylesBar
21 | private val testText = "Test test testTest 1234 \n" +
22 | " 1234 testTest test Test Test test testTest 1234 \n" +
23 | " 1234 testTest test Test Test test testTest 1234 \n" +
24 | " 1234 testTest test Test Test test testTest 1234 \n" +
25 | " 1234 testTest test Test"
26 | @Before
27 | fun setup(){
28 | activityController = Robolectric.buildActivity(Activity::class.java)
29 | activity = activityController.get()
30 |
31 | markdownEditText = MarkdownEditText(activity)
32 | markdownStylesBar = MarkdownStylesBar(activity)
33 | }
34 |
35 | @Test
36 | fun stylesBarTest(){
37 | markdownEditText.setStylesBar(markdownStylesBar)
38 | for(stylesButton in markdownStylesBar.children){
39 | stylesButton.performClick()
40 | for (char in testText.toCharArray()){
41 | markdownEditText.append("$char")
42 | }
43 | }
44 | }
45 |
46 | @Test
47 | fun markdownExportTest(){
48 | markdownEditText.setStylesBar(markdownStylesBar)
49 | for(stylesButton in markdownStylesBar.children){
50 | stylesButton.performClick()
51 | for (char in testText.toCharArray()){
52 | markdownEditText.append("$char")
53 | markdownEditText.text = SpannableStringBuilder(markdownEditText.getMD())
54 | markdownEditText.renderMD()
55 | }
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/java/com/yahiaangelo/markdownedittext/model/EnhancedMovementMethod.kt:
--------------------------------------------------------------------------------
1 | package com.yahiaangelo.markdownedittext.model
2 |
3 | import android.text.Selection
4 | import android.text.Spannable
5 | import android.text.method.ArrowKeyMovementMethod
6 | import android.text.method.MovementMethod
7 | import android.text.style.ClickableSpan
8 | import android.view.MotionEvent
9 | import android.widget.TextView
10 |
11 | /**
12 | * ArrowKeyMovementMethod does support selection of text but not the clicking of links.
13 | * LinkMovementMethod does support clicking of links but not the selection of text.
14 | * This class adds the link clicking to the ArrowKeyMovementMethod.
15 | * We basically take the LinkMovementMethod onTouchEvent code and remove the line
16 | * Selection.removeSelection(buffer);
17 | * which deselects all text when no link was found.
18 | * https://stackoverflow.com/a/23566268
19 | */
20 | class EnhancedMovementMethod : ArrowKeyMovementMethod() {
21 | private var sInstance: EnhancedMovementMethod? = null
22 |
23 | fun getsInstance(): MovementMethod? {
24 | if (sInstance == null) {
25 | sInstance = EnhancedMovementMethod()
26 | }
27 | return sInstance
28 | }
29 |
30 | override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
31 | val action = event.action
32 | if (action == MotionEvent.ACTION_UP ||
33 | action == MotionEvent.ACTION_DOWN
34 | ) {
35 | var x = event.x.toInt()
36 | var y = event.y.toInt()
37 | x -= widget.totalPaddingLeft
38 | y -= widget.totalPaddingTop
39 | x += widget.scrollX
40 | y += widget.scrollY
41 | val layout = widget.layout
42 | val line = layout.getLineForVertical(y)
43 | val off = layout.getOffsetForHorizontal(line, x.toFloat())
44 | val link = buffer.getSpans(
45 | off, off,
46 | ClickableSpan::class.java
47 | )
48 | if (link.isNotEmpty()) {
49 | if (action == MotionEvent.ACTION_UP) {
50 | if (x < layout.getLineMax(0)){
51 | link[0].onClick(widget)
52 | }
53 | } else if (action == MotionEvent.ACTION_DOWN) {
54 | Selection.setSelection(
55 | buffer,
56 | buffer.getSpanStart(link[0]),
57 | buffer.getSpanEnd(link[0])
58 | )
59 | }
60 | return true
61 | }
62 | /*else {
63 | Selection.removeSelection(buffer);
64 | }*/
65 | }
66 | return super.onTouchEvent(widget, buffer, event)
67 | }
68 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MarkdownEditText [](https://maven-badges.herokuapp.com/maven-central/io.github.yahiaangelo.markdownedittext/markdownedittext)
2 |
3 | A native Rich text editor for android based on [Markwon](https://github.com/noties/Markwon) library with export to Markdown option
4 |
5 | ---
6 | ## Preview
7 |
8 |
9 | ---
10 |
11 | ## Usage:
12 |
13 | ### Adding the depencency
14 | Add the dependency to your app build.gradle file:
15 | ```
16 | implementation 'io.github.yahiaangelo.markdownedittext:markdownedittext:$latestVersion'
17 | ```
18 | ### XML
19 | ```xml
20 |
26 |
27 |
36 | ```
37 | ### Code
38 | ```kotlin
39 | val markdownEditText = findViewById(R.id.edittext)
40 | val stylesBar = findViewById(R.id.stylesbar)
41 | markdownEditText.setStylesBar(stylesBar)
42 | ```
43 | #### Customize default Styles bar :
44 | ```Kotlin
45 | //Select specific Styles to show
46 | stylesbar.stylesList = arrayOf(MarkdownEditText.TextStyle.BOLD, MarkdownEditText.TextStyle.ITALIC)
47 | ```
48 | ---
49 |
50 | ### Available styles:
51 | * bold
52 | * italic
53 | * strike through
54 | * links
55 | * bullet list
56 | * numbered list
57 | * tasks list
58 | ---
59 |
60 | ### Libraries:
61 | [Material Design components](https://github.com/material-components/material-components-android)
62 |
63 | [Markwon](https://github.com/noties/Markwon)
64 |
65 | ---
66 | ## License:
67 | ```
68 | Copyright 2020 YahiaAngelo
69 |
70 | Licensed under the Apache License, Version 2.0 (the "License");
71 | you may not use this file except in compliance with the License.
72 | You may obtain a copy of the License at
73 |
74 | http://www.apache.org/licenses/LICENSE-2.0
75 |
76 | Unless required by applicable law or agreed to in writing, software
77 | distributed under the License is distributed on an "AS IS" BASIS,
78 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
79 | See the License for the specific language governing permissions and
80 | limitations under the License.
81 | ```
82 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/java/com/yahiaangelo/markdownedittext/MarkdownStylesBar.kt:
--------------------------------------------------------------------------------
1 | package com.yahiaangelo.markdownedittext
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.View
6 | import android.widget.LinearLayout
7 | import androidx.recyclerview.widget.LinearLayoutManager
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.yahiaangelo.markdownedittext.adapter.StylesBarAdapter
10 | import com.yahiaangelo.markdownedittext.model.StyleButton
11 |
12 | class MarkdownStylesBar @JvmOverloads constructor(
13 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
14 | ) : LinearLayout(context, attrs, defStyleAttr) {
15 |
16 | private var stylesBarAdapter: StylesBarAdapter
17 | private var styleButtons: ArrayList
18 | private var recyclerView: RecyclerView? = RecyclerView(context)
19 | var stylesList: Array = MarkdownEditText.TextStyle.values()
20 | set(value) {
21 | field = value
22 | updateStyles()
23 | }
24 | var markdownEditText: MarkdownEditText? = null
25 | set(value) {
26 | stylesBarAdapter.markdownEditText = value
27 | field = value
28 | }
29 |
30 | init {
31 | val linearLayoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
32 | recyclerView?.layoutManager = linearLayoutManager
33 | styleButtons = ArrayList()
34 | setStylesButtons()
35 | stylesBarAdapter = StylesBarAdapter(styleButtons)
36 | recyclerView?.adapter = stylesBarAdapter
37 |
38 | val a = context.obtainStyledAttributes(attrs, R.styleable.MarkdownStylesBar)
39 | val buttonColorStateList = a.getColorStateList(R.styleable.MarkdownStylesBar_buttonColor)
40 | if (buttonColorStateList != null){
41 | stylesBarAdapter.buttonsColor = buttonColorStateList
42 | }
43 | a.recycle()
44 | addView(recyclerView)
45 |
46 | }
47 |
48 | private fun updateStyles(){
49 | styleButtons.clear()
50 | setStylesButtons()
51 | stylesBarAdapter.setStyles(styleButtons)
52 | }
53 |
54 | private fun setStylesButtons(){
55 | for (style in stylesList){
56 | when(style){
57 | MarkdownEditText.TextStyle.BOLD -> styleButtons.add(StyleButton(R.drawable.ic_format_bold, R.id.style_button_bold))
58 | MarkdownEditText.TextStyle.ITALIC -> styleButtons.add(StyleButton(R.drawable.ic_format_italic, R.id.style_button_italic))
59 | MarkdownEditText.TextStyle.STRIKE -> styleButtons.add(StyleButton(R.drawable.ic_format_strikethrough, R.id.style_button_strike))
60 | MarkdownEditText.TextStyle.UNORDERED_LIST -> styleButtons.add(StyleButton(R.drawable.ic_format_list_bulleted, R.id.style_button_unordered_list))
61 | MarkdownEditText.TextStyle.ORDERED_LIST -> styleButtons.add(StyleButton(R.drawable.ic_format_list_numbered, R.id.style_button_ordered_list))
62 | MarkdownEditText.TextStyle.TASKS_LIST -> styleButtons.add(StyleButton(R.drawable.ic_check_box, R.id.style_button_task_list))
63 | MarkdownEditText.TextStyle.LINK -> styleButtons.add(StyleButton(R.drawable.ic_insert_link, R.id.style_button_link))
64 | else ->{}
65 | }
66 | }
67 | }
68 |
69 | fun getViewWithId(id: Int): View?{
70 | return recyclerView?.findViewById(id)
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/java/com/yahiaangelo/markdownedittext/OrderedListItemSpan.kt:
--------------------------------------------------------------------------------
1 | package com.yahiaangelo.markdownedittext
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 | import android.text.Layout
6 | import android.text.Spanned
7 | import android.text.style.LeadingMarginSpan
8 | import android.widget.TextView
9 | import io.noties.markwon.core.MarkwonTheme
10 | import io.noties.markwon.utils.LeadingMarginUtils
11 |
12 |
13 | class OrderedListItemSpan(
14 | private val theme: MarkwonTheme,
15 | val number: String
16 | ) : LeadingMarginSpan {
17 | private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
18 |
19 | // we will use this variable to check if our order number text exceeds block margin,
20 | // so we will use it instead of block margin
21 | // @since 1.0.3
22 | private var margin = 0
23 | override fun getLeadingMargin(first: Boolean): Int {
24 | // @since 2.0.1 we return maximum value of both (now we should measure number before)
25 | return margin.coerceAtLeast(theme.blockMargin)
26 | }
27 |
28 | override fun drawLeadingMargin(
29 | c: Canvas,
30 | p: Paint,
31 | x: Int,
32 | dir: Int,
33 | top: Int,
34 | baseline: Int,
35 | bottom: Int,
36 | text: CharSequence,
37 | start: Int,
38 | end: Int,
39 | first: Boolean,
40 | layout: Layout
41 | ) {
42 |
43 | // if there was a line break, we don't need to draw anything
44 | if (!first
45 | || !LeadingMarginUtils.selfStart(start, text, this)
46 | ) {
47 | return
48 | }
49 | paint.set(p)
50 | theme.applyListItemStyle(paint)
51 |
52 | // if we could force usage of #measure method then we might want skip this measuring here
53 | // but this won't hold against new values that a TextView can receive (new text size for
54 | // example...)
55 | val numberWidth = (paint.measureText(number) + .5f).toInt()
56 |
57 | // @since 1.0.3
58 | var width = theme.blockMargin
59 | if (numberWidth > width) {
60 | // let's keep this logic here in case a user decided not to call #measure and is fine
61 | // with current implementation
62 | width = numberWidth
63 | margin = numberWidth
64 | } else {
65 | margin = 0
66 | }
67 | val left: Int
68 | left = if (dir > 0) {
69 | x + width * dir - numberWidth
70 | } else {
71 | x + width * dir + (width - numberWidth)
72 | }
73 |
74 | // @since 1.1.1 we are using `baseline` argument to position text
75 | c.drawText(number, left.toFloat(), baseline.toFloat(), paint)
76 | }
77 |
78 | companion object {
79 | /**
80 | * Process supplied `text` argument and supply TextView paint to all OrderedListItemSpans
81 | * in order for them to measure number.
82 | *
83 | *
84 | * NB, this method must be called *before* setting text to a TextView (`TextView#setText`
85 | * internally can trigger new Layout creation which will ask for leading margins right away)
86 | *
87 | * @param textView to which markdown will be applied
88 | * @param text parsed markdown to process
89 | * @since 2.0.1
90 | */
91 | fun measure(textView: TextView, text: CharSequence) {
92 | if (text !is Spanned) {
93 | // nothing to do here
94 | return
95 | }
96 | val spans = text.getSpans(
97 | 0,
98 | text.length,
99 | OrderedListItemSpan::class.java
100 | )
101 | if (spans != null) {
102 | val paint = textView.paint
103 | for (span in spans) {
104 | span.margin = (paint.measureText(span.number) + .5f).toInt()
105 | }
106 | }
107 | }
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/java/com/yahiaangelo/markdownedittext/adapter/StylesBarAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.yahiaangelo.markdownedittext.adapter
2 |
3 | import android.content.res.ColorStateList
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.google.android.material.button.MaterialButton
8 | import com.yahiaangelo.markdownedittext.MarkdownEditText
9 | import com.yahiaangelo.markdownedittext.R
10 | import com.yahiaangelo.markdownedittext.model.StyleButton
11 |
12 | class StylesBarAdapter(private var buttonsList: ArrayList) : RecyclerView.Adapter() {
13 |
14 | private var selectedButton: MaterialButton? = null
15 | var buttonsColor: ColorStateList? = null
16 | var markdownEditText: MarkdownEditText? = null
17 |
18 | class MyViewHolder(val styleButton: MaterialButton) : RecyclerView.ViewHolder(styleButton)
19 |
20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
21 | val styleButtonView = LayoutInflater.from(parent.context).inflate(R.layout.styles_bar_item, parent, false) as MaterialButton
22 | return MyViewHolder(styleButtonView)
23 | }
24 |
25 |
26 | override fun getItemCount(): Int {
27 | return buttonsList.size
28 | }
29 |
30 | fun setStyles(newButtonsList: ArrayList){
31 | buttonsList = newButtonsList
32 | notifyDataSetChanged()
33 | }
34 |
35 | override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
36 | val styleButtonModel = buttonsList[position]
37 | if (buttonsColor != null){
38 | holder.styleButton.backgroundTintList = buttonsColor
39 | }
40 | holder.styleButton.icon = holder.itemView.context.getDrawable(styleButtonModel.icon)
41 | holder.styleButton.id = styleButtonModel.id
42 | if (styleButtonModel.id == R.id.style_button_link){
43 | holder.styleButton.isCheckable = false
44 | holder.styleButton.setOnClickListener {
45 | styleButtonClick(holder.styleButton)
46 | }
47 | }
48 | holder.styleButton.addOnCheckedChangeListener { button, _ ->
49 | if (selectedButton != null) {
50 | if (selectedButton != button) {
51 | selectedButton!!.isChecked = false
52 | styleButtonClick(selectedButton!!)
53 | }
54 | styleButtonClick(button)
55 | selectedButton = button
56 | } else {
57 | styleButtonClick(button)
58 | selectedButton = button
59 | }
60 |
61 | }
62 | }
63 |
64 | private fun styleButtonClick(button: MaterialButton) {
65 | if(markdownEditText != null) {
66 | when (button.id) {
67 | R.id.style_button_bold -> markdownEditText!!.triggerStyle(
68 | MarkdownEditText.TextStyle.BOLD,
69 | !button.isChecked
70 | )
71 | R.id.style_button_italic -> markdownEditText!!.triggerStyle(
72 | MarkdownEditText.TextStyle.ITALIC,
73 | !button.isChecked
74 | )
75 | R.id.style_button_strike -> markdownEditText!!.triggerStyle(
76 | MarkdownEditText.TextStyle.STRIKE,
77 | !button.isChecked
78 | )
79 | R.id.style_button_unordered_list -> markdownEditText!!.triggerStyle(
80 | MarkdownEditText.TextStyle.UNORDERED_LIST,
81 | !button.isChecked
82 | )
83 | R.id.style_button_ordered_list -> markdownEditText!!.triggerStyle(
84 | MarkdownEditText.TextStyle.ORDERED_LIST,
85 | !button.isChecked)
86 | R.id.style_button_task_list -> markdownEditText!!.triggerStyle(
87 | MarkdownEditText.TextStyle.TASKS_LIST,
88 | !button.isChecked)
89 | R.id.style_button_link -> markdownEditText!!.showInsertLinkDialog()
90 | }
91 |
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/MarkdownEditText/src/main/java/com/yahiaangelo/markdownedittext/MarkdownEditText.kt:
--------------------------------------------------------------------------------
1 | package com.yahiaangelo.markdownedittext
2 |
3 | import android.content.Context
4 | import android.text.*
5 | import android.text.style.ClickableSpan
6 | import android.text.style.QuoteSpan
7 | import android.text.style.StrikethroughSpan
8 | import android.util.AttributeSet
9 | import android.util.Log
10 | import android.view.LayoutInflater
11 | import android.view.View
12 | import androidx.appcompat.widget.AppCompatEditText
13 | import androidx.core.content.res.ResourcesCompat
14 | import androidx.core.text.getSpans
15 | import com.google.android.material.button.MaterialButton
16 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
17 | import com.google.android.material.textfield.TextInputEditText
18 | import com.yahiaangelo.markdownedittext.model.EnhancedMovementMethod
19 | import io.noties.markwon.*
20 | import io.noties.markwon.core.spans.*
21 | import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
22 | import io.noties.markwon.ext.tasklist.TaskListDrawable
23 | import io.noties.markwon.ext.tasklist.TaskListItem
24 | import io.noties.markwon.ext.tasklist.TaskListPlugin
25 | import io.noties.markwon.ext.tasklist.TaskListSpan
26 | import org.commonmark.node.SoftLineBreak
27 |
28 | class MarkdownEditText : AppCompatEditText {
29 |
30 | private var markwon: Markwon
31 | private var textWatcher: TextWatcher? = null
32 | private var markdownStylesBar: MarkdownStylesBar? = null
33 | private var isSelectionStyling = false
34 | private var bulletSpanStart = 0
35 | private var numberedSpanStart = 0
36 | private var taskSpanStart = 0
37 | var taskBoxColor: Int = ResourcesCompat.getColor(resources, R.color.primary, context.theme)
38 | var taskBoxBackgroundColor: Int =
39 | ResourcesCompat.getColor(resources, R.color.icon, context.theme)
40 | private val textWatchers: MutableList = emptyList().toMutableList()
41 | var onCopyPasteListener: OnCopyPasteListener? = null
42 |
43 | constructor(context: Context) : super(context, null) {
44 | markwon = markwonBuilder(context)
45 | }
46 |
47 | constructor(context: Context, attrs: AttributeSet?) : super(
48 | context,
49 | attrs,
50 | R.attr.editTextStyle
51 | ) {
52 | markwon = markwonBuilder(context)
53 | }
54 |
55 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
56 | context,
57 | attrs,
58 | defStyleAttr
59 | ) {
60 | markwon = markwonBuilder(context)
61 | }
62 |
63 |
64 | private fun markwonBuilder(context: Context): Markwon {
65 | movementMethod = EnhancedMovementMethod().getsInstance()
66 | return Markwon.builder(context)
67 | .usePlugin(StrikethroughPlugin.create())
68 | .usePlugin(TaskListPlugin.create(taskBoxColor, taskBoxColor, taskBoxBackgroundColor))
69 | .usePlugin(object : AbstractMarkwonPlugin() {
70 | override fun configureVisitor(builder: MarkwonVisitor.Builder) {
71 | super.configureVisitor(builder)
72 | builder.on(
73 | SoftLineBreak::class.java
74 | ) { visitor, _ -> visitor.forceNewLine() }
75 | }
76 |
77 | override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
78 | val origin = builder.getFactory(TaskListItem::class.java)
79 |
80 | builder.setFactory(
81 | TaskListItem::class.java
82 | ) { configuration, props ->
83 | val span = origin?.getSpans(configuration, props)
84 |
85 | if (span !is TaskListSpan) {
86 | null
87 | } else {
88 | val taskClick = object : ClickableSpan() {
89 | override fun onClick(widget: View) {
90 | span.isDone = !span.isDone
91 | text?.setSpan(
92 | span,
93 | text?.getSpanStart(span)!!,
94 | text?.getSpanEnd(span)!!,
95 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
96 | )
97 | }
98 |
99 | override fun updateDrawState(ds: TextPaint) {
100 | }
101 | }
102 | arrayOf(span, taskClick)
103 | }
104 | }
105 |
106 |
107 | }
108 | })
109 | .build()
110 | }
111 |
112 | fun setStylesBar(stylesBar: MarkdownStylesBar) {
113 | stylesBar.markdownEditText = this
114 | this.markdownStylesBar = stylesBar
115 | }
116 |
117 | fun triggerStyle(textStyle: TextStyle, stop: Boolean) {
118 | if (stop) {
119 | clearTextWatchers()
120 | } else {
121 | when(textStyle){
122 | TextStyle.UNORDERED_LIST -> triggerUnOrderedListStyle(stop)
123 | TextStyle.ORDERED_LIST -> triggerOrderedListStyle(stop)
124 | TextStyle.TASKS_LIST -> triggerTasksListStyle(stop)
125 | else-> {
126 | if (isSelectionStyling) {
127 | styliseText(textStyle, selectionStart, selectionEnd)
128 | isSelectionStyling = false
129 | } else {
130 | textWatcher = object : TextWatcher {
131 | override fun afterTextChanged(s: Editable?) {}
132 |
133 | override fun beforeTextChanged(
134 | s: CharSequence?,
135 | start: Int,
136 | count: Int,
137 | after: Int
138 | ) {
139 | }
140 |
141 | override fun onTextChanged(
142 | s: CharSequence?,
143 | start: Int,
144 | before: Int,
145 | count: Int
146 | ) {
147 |
148 | if (before < count) {
149 | styliseText(textStyle, start)
150 | }
151 | }
152 |
153 | }
154 | addTextWatcher(textWatcher!!)
155 | }
156 | }
157 | }
158 |
159 |
160 |
161 | }
162 |
163 |
164 | }
165 |
166 | fun triggerUnOrderedListStyle(stop: Boolean) {
167 | if (stop) {
168 | clearTextWatchers()
169 | } else {
170 | val currentLineStart = layout.getLineStart(getCurrentCursorLine())
171 | if (text!!.length < currentLineStart + 1 || text!!.getGivenSpansAt(
172 | span = arrayOf(
173 | TextStyle.UNORDERED_LIST
174 | ), currentLineStart, currentLineStart + 1
175 | ).isEmpty()
176 | ) {
177 | if (text!!.isNotEmpty()) {
178 | if (text!!.length > 1 && text!!.getGivenSpansAt(
179 | span = arrayOf(
180 | TextStyle.ORDERED_LIST,
181 | TextStyle.TASKS_LIST,
182 | ), selectionStart - 2, selectionStart
183 | ).isEmpty()
184 | ) {
185 | if (text.toString().substring(text!!.length - 2, text!!.length) != "\n") {
186 | text!!.insert(selectionStart, "\n ")
187 | } else {
188 | text!!.insert(selectionStart," ")
189 | }
190 | } else {
191 | text!!.insert(selectionStart,"\n ")
192 | }
193 |
194 | } else {
195 | text!!.insert(selectionStart," ")
196 | }
197 |
198 | bulletSpanStart = selectionStart - 1
199 | text!!.setSpan(
200 | BulletListItemSpan(markwon.configuration().theme(), 0),
201 | bulletSpanStart,
202 | selectionStart,
203 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
204 | )
205 | }
206 |
207 | addTextWatcher(object : TextWatcher {
208 | override fun afterTextChanged(s: Editable?) {}
209 |
210 | override fun beforeTextChanged(
211 | s: CharSequence?,
212 | start: Int,
213 | count: Int,
214 | after: Int
215 | ) {
216 | }
217 | var lineCount = getLineCount()
218 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
219 | if (before < count) {
220 | // If there's a new line
221 | if (selectionStart == selectionEnd && lineCount < getLineCount()) {
222 | lineCount = getLineCount()
223 | val string = text.toString()
224 | // If user hit enter
225 | if (string[selectionStart - 1] == '\n') {
226 | bulletSpanStart = selectionStart
227 | text!!.insert(selectionStart, " ")
228 | text!!.setSpan(
229 | BulletListItemSpan(markwon.configuration().theme(), 0),
230 | bulletSpanStart,
231 | bulletSpanStart + 1,
232 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
233 | )
234 | }else{
235 | for (bulletSpan in text?.getGivenSpansAt(span = arrayOf(TextStyle.UNORDERED_LIST), bulletSpanStart, bulletSpanStart + 1)!!){
236 | text?.removeSpan(bulletSpan)
237 | text?.setSpan(
238 | BulletListItemSpan(markwon.configuration().theme(), 0),
239 | bulletSpanStart,
240 | selectionStart,
241 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
242 | )
243 | }
244 | }
245 | }
246 |
247 | }
248 |
249 | }
250 |
251 | })
252 |
253 | }
254 |
255 |
256 | }
257 |
258 | fun triggerOrderedListStyle(stop: Boolean) {
259 | if (stop) {
260 | clearTextWatchers()
261 | } else {
262 | var currentNum = 1
263 | val currentLineStart = layout.getLineStart(getCurrentCursorLine())
264 | if (text!!.length < currentLineStart + 1 || text!!.getGivenSpansAt(
265 | span = arrayOf(
266 | TextStyle.ORDERED_LIST
267 | ), currentLineStart, currentLineStart + 1
268 | ).isEmpty()
269 | ) {
270 | if (text!!.isNotEmpty()) {
271 | if (text!!.length > 1 && text!!.getGivenSpansAt(
272 | span = arrayOf(
273 | TextStyle.UNORDERED_LIST,
274 | TextStyle.TASKS_LIST,
275 | ), selectionStart - 2, selectionStart
276 | ).isEmpty()
277 | ) {
278 | if (text.toString().substring(text!!.length - 2, text!!.length) != "\n") {
279 | text!!.insert(selectionStart, "\n ")
280 | } else {
281 | text!!.insert(selectionStart," ")
282 | }
283 | } else {
284 | text!!.insert(selectionStart,"\n ")
285 | }
286 |
287 | } else {
288 | text!!.insert(selectionStart," ")
289 | }
290 |
291 | numberedSpanStart = selectionStart - 1
292 | text!!.setSpan(
293 | OrderedListItemSpan(markwon.configuration().theme(), "${currentNum}-"),
294 | numberedSpanStart,
295 | selectionStart,
296 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
297 | )
298 | }
299 |
300 | currentNum++
301 |
302 | addTextWatcher(object : TextWatcher {
303 | override fun afterTextChanged(s: Editable?) {}
304 |
305 | override fun beforeTextChanged(
306 | s: CharSequence?,
307 | start: Int,
308 | count: Int,
309 | after: Int
310 | ) {
311 | }
312 | var lineCount = getLineCount()
313 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
314 | if (before < count) {
315 | if (selectionStart == selectionEnd && lineCount < getLineCount()) {
316 | lineCount = getLineCount()
317 | val string = text.toString()
318 | // If user hit enter
319 | if (string[selectionStart - 1] == '\n') {
320 | numberedSpanStart = selectionStart
321 | text!!.insert(selectionStart, " ")
322 | text!!.setSpan(
323 | OrderedListItemSpan(
324 | markwon.configuration().theme(),
325 | "${currentNum}-"
326 | ),
327 | numberedSpanStart,
328 | numberedSpanStart + 1,
329 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
330 | )
331 | currentNum++
332 | }else{
333 | for (numberedSpan in text?.getGivenSpansAt(span = arrayOf(TextStyle.ORDERED_LIST), numberedSpanStart, numberedSpanStart + 1)!!){
334 | val orderedSpan = numberedSpan as OrderedListItemSpan
335 | text?.removeSpan(numberedSpan)
336 | text?.setSpan(
337 | OrderedListItemSpan(
338 | markwon.configuration().theme(),
339 | orderedSpan.number
340 | ),
341 | numberedSpanStart,
342 | selectionStart,
343 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
344 | )
345 | }
346 | }
347 | }
348 |
349 | }
350 | }
351 |
352 | })
353 | }
354 | }
355 |
356 | fun triggerTasksListStyle(stop: Boolean) {
357 |
358 | if (stop) {
359 | clearTextWatchers()
360 | } else {
361 | val currentLineStart = layout.getLineStart(getCurrentCursorLine())
362 | if (text!!.length < currentLineStart + 1 || text!!.getGivenSpansAt(
363 | span = arrayOf(
364 | TextStyle.TASKS_LIST
365 | ), currentLineStart, currentLineStart + 1
366 | ).isEmpty()
367 | ) {
368 | if (text!!.isNotEmpty()) {
369 | if (text!!.length > 1 && text!!.getGivenSpansAt(
370 | span = arrayOf(
371 | TextStyle.ORDERED_LIST,
372 | TextStyle.UNORDERED_LIST,
373 | ), selectionStart - 2, selectionStart
374 | ).isEmpty()
375 | ) {
376 | if (text.toString().substring(text!!.length - 2, text!!.length) != "\n") {
377 | text!!.insert(selectionStart,"\n ")
378 | } else {
379 | text!!.insert(selectionStart," ")
380 | }
381 | } else {
382 | text!!.insert(selectionStart,"\n ")
383 | }
384 |
385 | } else {
386 | text!!.insert(selectionStart," ")
387 | }
388 | taskSpanStart = selectionStart - 1
389 | setTaskSpan(
390 | taskSpanStart,
391 | selectionStart, false
392 | )
393 | }
394 |
395 |
396 |
397 | addTextWatcher(object : TextWatcher {
398 | override fun afterTextChanged(s: Editable?) {}
399 |
400 | override fun beforeTextChanged(
401 | s: CharSequence?,
402 | start: Int,
403 | count: Int,
404 | after: Int
405 | ) {
406 | }
407 | var lineCount = getLineCount()
408 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
409 | if (before < count) {
410 | // If there's a new line
411 | if (selectionStart == selectionEnd && lineCount < getLineCount()) {
412 | lineCount = getLineCount()
413 | val string = text.toString()
414 | // If user hit enter
415 | if (string[selectionStart - 1] == '\n') {
416 | taskSpanStart = selectionStart
417 | text!!.insert(selectionStart, " ")
418 | setTaskSpan(
419 | taskSpanStart,
420 | taskSpanStart + 1, false
421 | )
422 | }else{
423 | for (span in text?.getGivenSpansAt(span = arrayOf(TextStyle.TASKS_LIST), taskSpanStart, taskSpanStart + 1)!!){
424 | val taskSpan = span as TaskListSpan
425 | text?.removeSpan(span)
426 | setTaskSpan(
427 | taskSpanStart,
428 | selectionStart, taskSpan.isDone
429 | )
430 |
431 | }
432 | }
433 | }
434 |
435 | }
436 |
437 | }
438 |
439 | })
440 |
441 | }
442 | }
443 |
444 | fun showInsertLinkDialog() {
445 | val textInputView = LayoutInflater.from(context).inflate(R.layout.link_input_layout, null)
446 | MaterialAlertDialogBuilder(context)
447 | .setView(textInputView)
448 | .setPositiveButton(
449 | "Add"
450 | ) { _, _ ->
451 | val title =
452 | textInputView.findViewById(R.id.link_input_title).text
453 | val url = textInputView.findViewById(R.id.link_input_url).text
454 | if (!url.isNullOrEmpty()) {
455 | addLinkSpan(title.toString(), url.toString())
456 | }
457 | }
458 | .setNegativeButton("Cancel") { _, _ -> }
459 | .show()
460 |
461 |
462 | }
463 |
464 | private fun addLinkSpan(title: String?, link: String) {
465 | val title1 = if (title.isNullOrEmpty()) link else title
466 | if (selectionStart == selectionEnd) {
467 | val cursorStart = selectionStart
468 | text!!.insert(cursorStart, title1)
469 | text!!.setSpan(
470 | LinkSpan(markwon.configuration().theme(), link, LinkResolverDef()),
471 | cursorStart,
472 | cursorStart + title1.length,
473 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
474 | )
475 | }
476 | }
477 |
478 | private fun setTaskSpan(start: Int, end: Int, isDone: Boolean) {
479 | val taskSpan = TaskListSpan(
480 | markwon.configuration().theme(),
481 | TaskListDrawable(taskBoxColor, taskBoxColor, taskBoxBackgroundColor),
482 | isDone
483 | )
484 | text!!.setSpan(
485 | taskSpan,
486 | start,
487 | end,
488 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
489 | )
490 |
491 | text?.setSpan(object : ClickableSpan() {
492 | override fun onClick(widget: View) {
493 | val spanStart = text?.getSpanStart(taskSpan)
494 | val spanEnd = text?.getSpanEnd(taskSpan)
495 | taskSpan.isDone = !taskSpan.isDone
496 | if (spanStart != null && spanEnd != null && spanStart >= 0) {
497 | text!!.setSpan(
498 | taskSpan,
499 | spanStart,
500 | spanEnd,
501 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
502 | )
503 | }
504 |
505 | }
506 |
507 | override fun updateDrawState(ds: TextPaint) {
508 |
509 | }
510 | }, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
511 | }
512 |
513 |
514 | private fun styliseText(
515 | textStyle: TextStyle,
516 | start: Int
517 | ) {
518 | when (textStyle) {
519 | TextStyle.BOLD -> {
520 | text!!.setSpan(
521 | StrongEmphasisSpan(),
522 | start,
523 | start + 1,
524 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
525 | )
526 | }
527 | TextStyle.ITALIC -> {
528 | text!!.setSpan(
529 | EmphasisSpan(),
530 | start,
531 | start + 1,
532 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
533 | )
534 | }
535 | TextStyle.STRIKE -> {
536 | text!!.setSpan(
537 | StrikethroughSpan(),
538 | start,
539 | start + 1,
540 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
541 | )
542 |
543 | }
544 | else -> {
545 | }
546 | }
547 |
548 |
549 | }
550 |
551 | private fun styliseText(
552 | textStyle: TextStyle,
553 | start: Int,
554 | end: Int
555 | ) {
556 | when (textStyle) {
557 | TextStyle.BOLD -> {
558 | text!!.setSpan(
559 | StrongEmphasisSpan(),
560 | start,
561 | end,
562 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
563 | )
564 | }
565 | TextStyle.ITALIC -> {
566 | text!!.setSpan(
567 | EmphasisSpan(),
568 | start,
569 | end,
570 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
571 | )
572 | }
573 | TextStyle.STRIKE -> {
574 | text!!.setSpan(
575 | StrikethroughSpan(),
576 | start,
577 | end,
578 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
579 | )
580 |
581 | }
582 | else -> {
583 | }
584 | }
585 |
586 |
587 | }
588 |
589 | enum class TextStyle {
590 | BOLD,
591 | ITALIC,
592 | STRIKE,
593 | QUOTE,
594 | LINK,
595 | UNORDERED_LIST,
596 | ORDERED_LIST,
597 | TASKS_LIST
598 | }
599 |
600 | fun getMD(): String {
601 | clearTextWatchers()
602 | var mdText = text
603 | val startList = emptyList().toMutableList()
604 | val endList = emptyList().toMutableList()
605 | var i = 0
606 | val appliedListSpans = mutableListOf()
607 |
608 | filterSpans()
609 | for ((index, span) in text!!.getGivenSpans(
610 | span = TextStyle.values()
611 | ).withIndex()) {
612 | val start = text!!.getSpanStart(span)
613 | val end = text!!.getSpanEnd(span)
614 | startList.add(index, start)
615 | endList.add(index, end)
616 | }
617 |
618 | for ((index, start) in startList.sorted().withIndex()) {
619 | val end = endList.sorted()[index]
620 | val spannedText = end.let { text!!.substring(start, it) }
621 | val span = end.let {
622 | text!!.getGivenSpansAt(
623 | span = TextStyle.values(), start, it
624 | )
625 | }
626 |
627 | for (selectedSpan in span) {
628 |
629 |
630 | if (selectedSpan is BulletListItemSpan) {
631 | if (!appliedListSpans.contains(start)) {
632 | val mdString = "* $spannedText"
633 | mdText = SpannableStringBuilder(
634 | mdText!!.replaceRange(
635 | start + i,
636 | end + i,
637 | mdString
638 | )
639 | )
640 | i += 2
641 | appliedListSpans.add(start)
642 | }
643 |
644 | } else if (selectedSpan is TaskListSpan) {
645 | if (!appliedListSpans.contains(start)) {
646 |
647 | val mdString =
648 | if (selectedSpan.isDone) "* [x] $spannedText" else "* [ ] $spannedText"
649 | mdText = SpannableStringBuilder(
650 | mdText!!.replaceRange(
651 | start + i,
652 | end + i,
653 | mdString
654 | )
655 | )
656 | i += 6
657 | appliedListSpans.add(start)
658 | }
659 | } else {
660 | if (spannedText.length > 1) {
661 | when (selectedSpan) {
662 | is StrongEmphasisSpan -> {
663 | val mdString = "**$spannedText**"
664 | mdText = SpannableStringBuilder(
665 | mdText!!.replaceRange(
666 | start + i,
667 | end + i,
668 | mdString
669 | )
670 | )
671 | i += 4
672 | }
673 | is EmphasisSpan -> {
674 | val mdString = "_${spannedText}_"
675 | mdText = SpannableStringBuilder(
676 | mdText!!.replaceRange(
677 | start + i,
678 | end + i,
679 | mdString
680 | )
681 | )
682 | i += 2
683 | }
684 | is StrikethroughSpan -> {
685 | val mdString = "~~$spannedText~~"
686 | mdText = SpannableStringBuilder(
687 | mdText!!.replaceRange(
688 | start + i,
689 | end + i,
690 | mdString
691 | )
692 | )
693 | i += 4
694 | }
695 | is OrderedListItemSpan -> {
696 | val mdString = "${selectedSpan.number}$spannedText"
697 | mdText = SpannableStringBuilder(
698 | mdText!!.replaceRange(
699 | start + i,
700 | end + i,
701 | mdString
702 | )
703 | )
704 | i += 2
705 | }
706 | is LinkSpan -> {
707 | val mdString = "[$spannedText](${selectedSpan.link})"
708 | mdText = SpannableStringBuilder(
709 | mdText!!.replaceRange(
710 | start + i,
711 | end + i,
712 | mdString
713 | )
714 | )
715 | i += 4 + (selectedSpan.link.length - spannedText.length)
716 | }
717 |
718 | }
719 | }
720 |
721 | }
722 |
723 | }
724 |
725 | }
726 | return mdText.toString()
727 | }
728 |
729 | private fun filterSpans() {
730 | val spans = text?.getGivenSpans(
731 | span = arrayOf(
732 | TextStyle.BOLD,
733 | TextStyle.ITALIC,
734 | TextStyle.STRIKE,
735 | TextStyle.LINK
736 | )
737 | )
738 |
739 | if (spans != null) {
740 | for (span in spans) {
741 | val selectedSpans = text?.getGivenSpansAt(
742 | span = arrayOf(span),
743 | text?.getSpanStart(span)!!,
744 | text?.getSpanEnd(span)!!
745 | )
746 | if (selectedSpans!!.size > 1) {
747 | var smallSpanIndex = 0
748 | var spanSize: Int? = null
749 | for ((index, selectedSpan) in selectedSpans.withIndex()) {
750 | if (text?.getSpanStart(selectedSpan) != null) {
751 | val spanStart = text?.getSpanStart(selectedSpan)
752 | val spanEnd = text?.getSpanEnd(selectedSpan)!!
753 | if (spanSize == null) {
754 | spanSize = spanEnd - spanStart!!
755 | smallSpanIndex = index
756 | } else {
757 | if (spanEnd - spanStart!! < spanSize) {
758 | spanSize = spanEnd - spanStart
759 | smallSpanIndex = index
760 | }
761 | }
762 | }
763 | }
764 | text?.removeSpan(selectedSpans[smallSpanIndex])
765 | }
766 | }
767 | }
768 |
769 | val listsSpans = text?.getGivenSpans(
770 | span = arrayOf(
771 | TextStyle.UNORDERED_LIST,
772 | TextStyle.TASKS_LIST
773 | )
774 | )
775 |
776 | if (!listsSpans.isNullOrEmpty()) {
777 | for (span in listsSpans) {
778 | val spanStart = text?.getSpanStart(span)
779 | val spanEnd = text?.getSpanEnd(span)
780 |
781 | if (spanEnd!! - spanStart!! > 1) {
782 | text?.removeSpan(span)
783 | text?.setSpan(
784 | span,
785 | spanStart,
786 | spanStart + 1,
787 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
788 | )
789 | }
790 | }
791 | }
792 | }
793 |
794 |
795 | private fun Editable.getGivenSpans(vararg span: TextStyle): MutableList {
796 | val spanList = emptyArray().toMutableList()
797 | for (selectedSpan in span) {
798 | when (selectedSpan) {
799 | TextStyle.BOLD -> {
800 | this.getSpans().forEach {
801 | spanList.add(it)
802 | }
803 | }
804 | TextStyle.ITALIC -> {
805 | this.getSpans().forEach {
806 | spanList.add(it)
807 | }
808 | }
809 | TextStyle.STRIKE -> {
810 | this.getSpans().forEach {
811 | spanList.add(it)
812 | }
813 | }
814 | TextStyle.QUOTE -> {
815 | this.getSpans().forEach {
816 | spanList.add(it)
817 | }
818 | }
819 | TextStyle.UNORDERED_LIST -> {
820 | this.getSpans().forEach {
821 | spanList.add(it)
822 | }
823 | }
824 | TextStyle.ORDERED_LIST -> {
825 | this.getSpans().forEach {
826 | spanList.add(it)
827 | }
828 | }
829 | TextStyle.TASKS_LIST -> {
830 | this.getSpans().forEach {
831 | spanList.add(it)
832 | }
833 | }
834 | TextStyle.LINK -> {
835 | this.getSpans().forEach {
836 | spanList.add(it)
837 | }
838 | }
839 | }
840 | }
841 | return spanList
842 | }
843 |
844 | private fun Editable.getGivenSpansAt(
845 | vararg span: Any,
846 | start: Int,
847 | end: Int
848 | ): MutableList {
849 | val spanList = emptyArray().toMutableList()
850 | for (selectedSpan in span) {
851 | this.getSpans(start, end, selectedSpan::class.java).forEach {
852 | spanList.add(it)
853 | }
854 | }
855 | return spanList
856 | }
857 |
858 | private fun Editable.getGivenSpansAt(
859 | vararg span: TextStyle,
860 | start: Int,
861 | end: Int
862 | ): MutableList {
863 | val spanList = emptyArray().toMutableList()
864 | for (selectedSpan in span) {
865 | when (selectedSpan) {
866 | TextStyle.BOLD -> {
867 | this.getSpans(start, end).forEach {
868 | spanList.add(it)
869 | }
870 | }
871 | TextStyle.ITALIC -> {
872 | this.getSpans(start, end).forEach {
873 | spanList.add(it)
874 | }
875 | }
876 | TextStyle.STRIKE -> {
877 | this.getSpans(start, end).forEach {
878 | spanList.add(it)
879 | }
880 | }
881 | TextStyle.QUOTE -> {
882 | this.getSpans(start, end).forEach {
883 | spanList.add(it)
884 | }
885 | }
886 | TextStyle.UNORDERED_LIST -> {
887 | this.getSpans(start, end).forEach {
888 | spanList.add(it)
889 | }
890 | }
891 | TextStyle.ORDERED_LIST -> {
892 | this.getSpans(start, end).forEach {
893 | spanList.add(it)
894 | }
895 | }
896 | TextStyle.TASKS_LIST -> {
897 | this.getSpans(start, end).forEach {
898 | spanList.add(it)
899 | }
900 | }
901 | TextStyle.LINK -> {
902 | this.getSpans(start, end).forEach {
903 | spanList.add(it)
904 | }
905 | }
906 | }
907 | }
908 | return spanList
909 | }
910 |
911 | private var selectedButtonId: Int? = null
912 |
913 | override fun onSelectionChanged(selStart: Int, selEnd: Int) {
914 | if (selStart == selEnd && markdownStylesBar != null && selStart > 0) {
915 | val currentLineStart = layout.getLineStart(getCurrentCursorLine())
916 | val listsSpans = text!!.getGivenSpansAt(
917 | span = arrayOf(
918 | TextStyle.UNORDERED_LIST,
919 | TextStyle.TASKS_LIST
920 | ),
921 | start = currentLineStart, end = currentLineStart + 1
922 | )
923 | if (listsSpans.size > 0) {
924 | when (listsSpans[0]) {
925 | is BulletListItemSpan -> {
926 | val bulletButton =
927 | markdownStylesBar?.getViewWithId(R.id.style_button_unordered_list) as MaterialButton?
928 | if (bulletButton != null && !bulletButton.isChecked) {
929 | bulletButton.isChecked = true
930 | }
931 | selectedButtonId = bulletButton?.id
932 | }
933 | is OrderedListItemSpan -> {
934 | val listButton =
935 | markdownStylesBar!!.getViewWithId(R.id.style_button_ordered_list) as MaterialButton?
936 | if (listButton != null && !listButton.isChecked) {
937 | listButton.isChecked = true
938 | }
939 | selectedButtonId = listButton?.id
940 | }
941 | is TaskListSpan -> {
942 | val taskButton =
943 | markdownStylesBar!!.getViewWithId(R.id.style_button_task_list) as MaterialButton?
944 | if (taskButton != null && !taskButton.isChecked) {
945 | taskButton.isChecked = true
946 | }
947 | selectedButtonId = taskButton?.id
948 | }
949 | }
950 | } else {
951 | val selectedSpans = text!!.getGivenSpansAt(
952 | span = arrayOf(
953 | TextStyle.BOLD,
954 | TextStyle.ITALIC,
955 | TextStyle.STRIKE
956 | ),
957 | start = selStart - 1, end = selStart
958 | )
959 | if (selectedSpans.size > 0) {
960 | for (span in selectedSpans.distinctBy { it.javaClass }) {
961 | when (span) {
962 | is StrongEmphasisSpan -> {
963 | val boldButton =
964 | markdownStylesBar!!.getViewWithId(R.id.style_button_bold) as MaterialButton?
965 | if (boldButton != null && !boldButton.isChecked) {
966 | boldButton.isChecked = true
967 | }
968 | selectedButtonId = boldButton?.id
969 | }
970 | is EmphasisSpan -> {
971 | val italicButton =
972 | markdownStylesBar!!.getViewWithId(R.id.style_button_italic) as MaterialButton?
973 | if (italicButton != null && !italicButton.isChecked) {
974 | italicButton.isChecked = true
975 | }
976 | selectedButtonId = italicButton?.id
977 | }
978 | is StrikethroughSpan -> {
979 | val strikeThroughButton =
980 | markdownStylesBar!!.getViewWithId(R.id.style_button_strike) as MaterialButton?
981 | if (strikeThroughButton != null && !strikeThroughButton.isChecked) {
982 | strikeThroughButton.isChecked = true
983 | }
984 | selectedButtonId = strikeThroughButton?.id
985 | }
986 |
987 | }
988 | }
989 | } else {
990 | if (selectedButtonId != null) {
991 | val button =
992 | markdownStylesBar!!.getViewWithId(
993 | selectedButtonId!!
994 | ) as MaterialButton?
995 | if (button != null && button.isChecked) {
996 | button.isChecked = false
997 | }
998 | }
999 | }
1000 | }
1001 |
1002 | } else if (selStart != selEnd && markdownStylesBar != null) {
1003 | isSelectionStyling = true
1004 | }
1005 |
1006 |
1007 | }
1008 |
1009 | private fun addTextWatcher(textWatcher: TextWatcher) {
1010 | textWatchers.add(textWatcher)
1011 | addTextChangedListener(textWatcher)
1012 | }
1013 |
1014 | private fun clearTextWatchers() {
1015 | for (textWatcher in textWatchers) {
1016 | removeTextChangedListener(textWatcher)
1017 | }
1018 | }
1019 |
1020 | private fun getCurrentCursorLine(): Int {
1021 | return if (selectionStart != -1) layout.getLineForOffset(selectionStart) else -1
1022 | }
1023 |
1024 | private fun getLineCharPosition(line: Int): Int {
1025 | var chars = 1
1026 | return if (line == 0) {
1027 | 0
1028 | } else {
1029 | for (i in 0 until line) {
1030 | chars += text!!.lines()[i].length
1031 | }
1032 | chars
1033 | }
1034 | }
1035 |
1036 | override fun onTextContextMenuItem(id: Int): Boolean {
1037 | when(id){
1038 | android.R.id.cut -> onCut()
1039 | android.R.id.copy -> onCopy()
1040 | android.R.id.paste -> onPaste()
1041 |
1042 | }
1043 | return super.onTextContextMenuItem(id)
1044 | }
1045 | fun onCut(){
1046 | onCopyPasteListener?.onCut()
1047 | }
1048 | fun onCopy(){
1049 | onCopyPasteListener?.onCopy()
1050 | }
1051 | fun onPaste(){
1052 | onCopyPasteListener?.onPaste()
1053 | }
1054 |
1055 | //https://gist.github.com/guillermomuntaner/82491cbf0c88dec560a5
1056 | interface OnCopyPasteListener{
1057 | fun onCut()
1058 | fun onCopy()
1059 | fun onPaste()
1060 | }
1061 |
1062 | //Renders md text in editText
1063 | fun renderMD() {
1064 | this.text = SpannableStringBuilder(markwon.toMarkdown(text.toString()))
1065 | }
1066 |
1067 | //Renders given md string
1068 | fun renderMD(md: String) {
1069 | this.text = SpannableStringBuilder(markwon.toMarkdown(md))
1070 | }
1071 | }
1072 |
--------------------------------------------------------------------------------