diff --git a/aztec/build.gradle b/aztec/build.gradle index 42c4f6702..3cc63fef0 100644 --- a/aztec/build.gradle +++ b/aztec/build.gradle @@ -59,10 +59,11 @@ dependencies { testImplementation "junit:junit:$jUnitVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'androidx.test:core:1.4.0' + testImplementation 'androidx.test:core:1.6.1' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion" implementation 'org.apache.commons:commons-lang3:3.8.1' diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/Aztec.kt b/aztec/src/main/kotlin/org/wordpress/aztec/Aztec.kt index b3464792c..ced53f2f3 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/Aztec.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/Aztec.kt @@ -78,7 +78,7 @@ open class Aztec private constructor( } @JvmStatic - fun with(visualEditor: AztecText, toolbar: AztecToolbar, toolbarClickListener: IAztecToolbarClickListener): Aztec { + fun with(visualEditor: AztecText, toolbar: IAztecToolbar, toolbarClickListener: IAztecToolbarClickListener): Aztec { return Aztec(visualEditor, null, toolbar, toolbarClickListener) } } diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt index d7b81370b..a326e684a 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt @@ -429,6 +429,24 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown return super.getText()!! } + /** + * The getText method returns mutable version of the content. This means that it can change + * when being worked on. Call this method when you want your Editable to be immutable. + */ + fun getTextCopy(): Editable { + val copy = SpannableStringBuilder(text.toString()) + + val spans: Array = text.getSpans(0, text.length, Any::class.java) + + for (span in spans) { + val spanStart = text.getSpanStart(span) + val spanEnd = text.getSpanEnd(span) + val flags = text.getSpanFlags(span) + copy.setSpan(span, spanStart, spanEnd, flags) + } + return copy + } + @SuppressLint("ResourceType") private fun init(attrs: AttributeSet?) { disableTextChangedListener() @@ -1631,6 +1649,10 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown return toHtml(text, withCursorTag) } + suspend fun toHtmlAsync(withCursorTag: Boolean = false): String { + return toHtmlAsync(getTextCopy(), withCursorTag) + } + // general function accepts any Spannable and converts it to regular or "calypso" html // depending on the mode fun toHtml(content: Spannable, withCursorTag: Boolean = false): String { @@ -1645,6 +1667,10 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown } } + suspend fun toHtmlAsync(content: Editable, withCursorTag: Boolean = false): String { + return toPlainHtmlAsync(content, withCursorTag) + } + // platform agnostic HTML // default behavior returns HTML from this text fun toPlainHtml(withCursorTag: Boolean = false): String { @@ -1664,6 +1690,12 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown } } + suspend fun toPlainHtmlAsync(content: Editable, withCursorTag: Boolean = false): String { + return withContext(Dispatchers.Default) { + parseHtml(content, withCursorTag) + } + } + private fun parseHtml(content: Spannable, withCursorTag: Boolean): String { val parser = AztecParser(alignmentRendering, plugins) val output: SpannableStringBuilder diff --git a/aztec/src/test/kotlin/org/wordpress/aztec/AsyncAztecTest.kt b/aztec/src/test/kotlin/org/wordpress/aztec/AsyncAztecTest.kt new file mode 100644 index 000000000..eb1bf7a90 --- /dev/null +++ b/aztec/src/test/kotlin/org/wordpress/aztec/AsyncAztecTest.kt @@ -0,0 +1,105 @@ +package org.wordpress.aztec + +import android.app.Activity +import android.view.MenuItem +import android.widget.PopupMenu +import android.widget.ToggleButton +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.wordpress.aztec.source.SourceViewEditText +import org.wordpress.aztec.toolbar.AztecToolbar +import org.wordpress.aztec.toolbar.ToolbarAction +import org.wordpress.aztec.toolbar.ToolbarItems + +@RunWith(RobolectricTestRunner::class) +class AsyncAztecTest { + lateinit var editText: AztecText + lateinit var sourceText: SourceViewEditText + lateinit var toolbar: AztecToolbar + lateinit var buttonQuote: ToggleButton + lateinit var menuHeading: PopupMenu + lateinit var menuHeading1: MenuItem + lateinit var menuHeading2: MenuItem + lateinit var menuParagraph: MenuItem + lateinit var buttonPreformat: ToggleButton + lateinit var buttonBold: ToggleButton + + /** + * Initialize variables. + */ + @Before + fun init() { + val activity = Robolectric.buildActivity(Activity::class.java).create().visible().get() + editText = AztecText(activity) + editText.setCalypsoMode(false) + sourceText = SourceViewEditText(activity) + sourceText.setCalypsoMode(false) + toolbar = AztecToolbar(activity) + toolbar.setToolbarItems( + ToolbarItems.BasicLayout( + ToolbarAction.HEADING, + ToolbarAction.PREFORMAT, + ToolbarAction.LIST, + ToolbarAction.QUOTE, + ToolbarAction.BOLD, + ToolbarAction.ITALIC, + ToolbarAction.LINK, + ToolbarAction.UNDERLINE, + ToolbarAction.STRIKETHROUGH, + ToolbarAction.ALIGN_LEFT, + ToolbarAction.ALIGN_CENTER, + ToolbarAction.ALIGN_RIGHT, + ToolbarAction.HORIZONTAL_RULE, + ToolbarAction.HTML + )) + toolbar.setEditor(editText, sourceText) + buttonQuote = toolbar.findViewById(R.id.format_bar_button_quote) + menuHeading = toolbar.getHeadingMenu() as PopupMenu + menuHeading1 = menuHeading.menu.getItem(1) + menuHeading2 = menuHeading.menu.getItem(2) + menuParagraph = menuHeading.menu.getItem(0) + buttonPreformat = toolbar.findViewById(R.id.format_bar_button_pre) + buttonBold = toolbar.findViewById(R.id.format_bar_button_bold) + activity.setContentView(editText) + } + + @Test + @Throws(Exception::class) + fun asyncToHtmlWorksOnCurrentVersion() = runTest { + editText.append("One two three") + val textCopy = editText.getTextCopy() + Assert.assertEquals("One two three", editText.toHtmlAsync(textCopy)) + val jobs = mutableListOf() + jobs.add(launch { + Assert.assertEquals("One two three", editText.toHtmlAsync(textCopy)) + }) + toolbar.onMenuItemClick(menuHeading1) + val textCopy2 = editText.getTextCopy() + jobs.add(launch { + Assert.assertEquals("

One two three

", editText.toHtmlAsync(textCopy2)) + }) + toolbar.onMenuItemClick(menuParagraph) + val textCopy3 = editText.getTextCopy() + jobs.add(launch { + Assert.assertEquals("One two three", editText.toHtmlAsync(textCopy3)) + }) + val from = editText.editableText.indexOf("two") + editText.setSelection(from, from + 3) + editText.toggleFormatting(AztecTextFormat.FORMAT_BOLD) + val textCopy4 = editText.getTextCopy() + jobs.add(launch { + Assert.assertEquals("One two three", editText.toHtmlAsync(textCopy4)) + }) + jobs.forEach { + it.join() + Assert.assertTrue(it.isCompleted) + } + } +} diff --git a/build.gradle b/build.gradle index 1b93073cb..5e67c2414 100644 --- a/build.gradle +++ b/build.gradle @@ -73,12 +73,12 @@ ext { ext { // mixed gradlePluginVersion = '3.3.1' - kotlinCoroutinesVersion = '1.6.4' + kotlinCoroutinesVersion = '1.8.1' tagSoupVersion = '1.2.1' glideVersion = '4.10.0' picassoVersion = '2.5.2' - robolectricVersion = '4.11' - jUnitVersion = '4.12' + robolectricVersion = '4.13' + jUnitVersion = '4.13.2' jSoupVersion = '1.15.3' wordpressUtilsVersion = '3.5.0' espressoVersion = '3.0.1' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c747538fb..a4413138c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index d4e48466f..c73ca73d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ pluginManagement { gradle.ext.kotlinVersion = '1.9.24' - gradle.ext.agpVersion = '8.1.0' + gradle.ext.agpVersion = '8.5.0' gradle.ext.automatticPublishToS3Version = '0.8.0' gradle.ext.dependencyAnalysisVersion = '1.33.0'