Skip to content
21 changes: 21 additions & 0 deletions app/src/androidTest/kotlin/org/wordpress/aztec/demo/Matchers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import android.widget.EditText
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.wordpress.aztec.AztecText
import org.wordpress.aztec.source.Format
import org.wordpress.aztec.source.SourceViewEditText

object Matchers {
fun withRegex(expected: Regex): Matcher<View> {
Expand Down Expand Up @@ -45,4 +47,23 @@ object Matchers {
}
}
}

fun hasContentChanges(shouldHaveChanges: AztecText.EditorHasChanges): TypeSafeMatcher<View> {

return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("User has made changes to the post: $shouldHaveChanges")
}

public override fun matchesSafely(view: View): Boolean {
if (view is SourceViewEditText) {
return view.hasChanges() == shouldHaveChanges
}
if (view is AztecText) {
return view.hasChanges() == shouldHaveChanges
}
return false
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ import android.support.test.espresso.matcher.ViewMatchers.withId
import android.support.test.espresso.matcher.ViewMatchers.withText
import android.view.KeyEvent
import android.view.View
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.hasToString
import org.hamcrest.TypeSafeMatcher
import org.wordpress.aztec.AztecText
import org.wordpress.aztec.demo.Actions
import org.wordpress.aztec.demo.BasePage
Expand Down Expand Up @@ -371,21 +369,12 @@ class EditorPage : BasePage() {
}

fun hasChanges(shouldHaveChanges : AztecText.EditorHasChanges): EditorPage {
val hasNoChangesMatcher = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("User has made changes to the post: $shouldHaveChanges")
}

public override fun matchesSafely(view: View): Boolean {
if (view is AztecText) {
return view.hasChanges() == shouldHaveChanges
}

return false
}
}
editor.check(matches(Matchers.hasContentChanges(shouldHaveChanges)))
return this
}

editor.check(matches(hasNoChangesMatcher))
fun hasChangesHTML(shouldHaveChanges : AztecText.EditorHasChanges): EditorPage {
htmlEditor.check(matches(Matchers.hasContentChanges(shouldHaveChanges)))
return this
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,6 @@ class MixedTextFormattingTests : BaseTest() {
.hasChanges(AztecText.EditorHasChanges.NO_CHANGES) // Verify that the user had not changed the input
}

@Ignore("Until this issue is fixed: https://github.com/wordpress-mobile/AztecEditor-Android/issues/698")
@Test
fun testHasChangesWithMixedBoldAndItalicFormatting() {
val input = "<b>bold <i>italic</i> bold</b>"
Expand All @@ -271,4 +270,35 @@ class MixedTextFormattingTests : BaseTest() {
.hasChanges(AztecText.EditorHasChanges.CHANGES)
.verifyHTML(afterParser)
}

@Test
fun testHasChangesOnHTMLEditor() {
val input = "<b>Test</b>"
val insertedText = " text added"
val afterParser = "<b>Test</b>$insertedText"

EditorPage().toggleHtml()
.insertHTML(input)
.toggleHtml()
.toggleHtml() // switch back to HTML editor
.insertHTML(insertedText)
.hasChangesHTML(AztecText.EditorHasChanges.CHANGES)
.verifyHTML(afterParser)
}

@Test
fun testHasChangesOnHTMLEditorTestedFromVisualEditor() {
val input = "<b>Test</b>"
val insertedText = " text added"
val afterParser = "Test$insertedText"

EditorPage().toggleHtml()
.insertHTML(input)
.toggleHtml()
.toggleHtml() // switch back to HTML editor
.insertHTML(insertedText)
.hasChangesHTML(AztecText.EditorHasChanges.CHANGES)
.toggleHtml() // switch back to Visual editor
.verify(afterParser)
}
}
70 changes: 38 additions & 32 deletions aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,40 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
bitmap.density = DisplayMetrics.DENSITY_DEFAULT
return BitmapDrawable(context.resources, bitmap)
}

@Throws(NoSuchAlgorithmException::class)
private fun calculateSHA256(s: String): ByteArray {
val digest = MessageDigest.getInstance("SHA-256")
digest.update(s.toByteArray())
return digest.digest()
}

fun calculateInitialHTMLSHA(initialHTMLParsed: String, initialEditorContentParsedSHA256: ByteArray): ByteArray {
try {
// Do not recalculate the hash if it's not the first call to `fromHTML`.
if (initialEditorContentParsedSHA256.isEmpty() || Arrays.equals(initialEditorContentParsedSHA256, calculateSHA256(""))) {
return calculateSHA256(initialHTMLParsed)
} else {
return initialEditorContentParsedSHA256
}
} catch (e: Throwable) {
// Do nothing here. `toPlainHtml` can throw exceptions, also calculateSHA256 -> NoSuchAlgorithmException
}

return ByteArray(0)
}

fun hasChanges(initialEditorContentParsedSHA256: ByteArray, newContent: String): EditorHasChanges {
try {
if (Arrays.equals(initialEditorContentParsedSHA256, calculateSHA256(newContent))) {
return EditorHasChanges.NO_CHANGES
}
return EditorHasChanges.CHANGES
} catch (e: Throwable) {
// Do nothing here. `toPlainHtml` can throw exceptions, also calculateSHA256 -> NoSuchAlgorithmException
return EditorHasChanges.UNKNOWN
}
}
}

enum class EditorHasChanges {
Expand All @@ -175,7 +209,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
private var consumeSelectionChangedEvent: Boolean = false
private var isInlineTextHandlerEnabled: Boolean = true
private var bypassObservationQueue: Boolean = false
private var initialEditorContentParsedSHA256: ByteArray = ByteArray(0)

var initialEditorContentParsedSHA256: ByteArray = ByteArray(0)

private var onSelectionChangedListener: OnSelectionChangedListener? = null
private var onImeBackListener: OnImeBackListener? = null
Expand Down Expand Up @@ -993,7 +1028,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown

setSelection(cursorPosition)

calculateInitialHTMLSHA()
initialEditorContentParsedSHA256 = calculateInitialHTMLSHA(toPlainHtml(false), initialEditorContentParsedSHA256)

loadImages()
loadVideos()
Expand Down Expand Up @@ -1069,37 +1104,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
}
}

private fun calculateInitialHTMLSHA() {
try {
// Do not recalculate the hash if it's not the first call to `fromHTML`.
if (initialEditorContentParsedSHA256.isEmpty() || Arrays.equals(initialEditorContentParsedSHA256, calculateSHA256(""))) {
val initialHTMLParsed = toPlainHtml(false)
initialEditorContentParsedSHA256 = calculateSHA256(initialHTMLParsed)
}
} catch (e: Throwable) {
// Do nothing here. `toPlainHtml` can throw exceptions, also calculateSHA256 -> NoSuchAlgorithmException
}
}

@Throws(NoSuchAlgorithmException::class)
private fun calculateSHA256(s: String): ByteArray {
val digest = MessageDigest.getInstance("SHA-256")
digest.update(s.toByteArray())
return digest.digest()
}

open fun hasChanges(): EditorHasChanges {
if (!initialEditorContentParsedSHA256.isEmpty()) {
try {
if (Arrays.equals(initialEditorContentParsedSHA256, calculateSHA256(toPlainHtml(false)))) {
return EditorHasChanges.NO_CHANGES
}
return EditorHasChanges.CHANGES
} catch (e: Throwable) {
// Do nothing here. `toPlainHtml` can throw exceptions, also calculateSHA256 -> NoSuchAlgorithmException
}
}
return EditorHasChanges.UNKNOWN
return hasChanges(initialEditorContentParsedSHA256, toPlainHtml(false))
}

// returns regular or "calypso" html depending on the mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import org.wordpress.aztec.AztecText
import org.wordpress.aztec.AztecText.EditorHasChanges
import org.wordpress.aztec.AztecTextAccessibilityDelegate
import org.wordpress.aztec.History
import org.wordpress.aztec.R
Expand Down Expand Up @@ -44,6 +45,8 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex

private var accessibilityDelegate = AztecTextAccessibilityDelegate(this)

private var initialEditorContentParsedSHA256: ByteArray = ByteArray(0)

constructor(context: Context) : super(context) {
init(null)
}
Expand Down Expand Up @@ -94,6 +97,7 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex
visibility = customState.getInt("visibility")
val retainedContent = InstanceStateUtils.readAndPurgeTempInstance<String>(RETAINED_CONTENT_KEY, "", savedState.state)
setText(retainedContent)
initialEditorContentParsedSHA256 = customState.getByteArray(AztecText.RETAINED_INITIAL_HTML_PARSED_SHA256_KEY)
}

// Do not include the content of the editor when saving state to bundle.
Expand All @@ -106,6 +110,8 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex

override fun onSaveInstanceState(): Parcelable {
val bundle = Bundle()
bundle.putByteArray(org.wordpress.aztec.AztecText.RETAINED_INITIAL_HTML_PARSED_SHA256_KEY,
initialEditorContentParsedSHA256)
InstanceStateUtils.writeTempInstance(context, null, RETAINED_CONTENT_KEY, text.toString(), bundle)
val superState = super.onSaveInstanceState()
val savedState = SavedState(superState)
Expand Down Expand Up @@ -189,6 +195,8 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex
disableTextChangedListener()
val cursorPosition = consumeCursorTag(styledHtml)
text = styledHtml
initialEditorContentParsedSHA256 = AztecText.calculateInitialHTMLSHA(getPureHtml(false),
initialEditorContentParsedSHA256)
enableTextChangedListener()

if (cursorPosition > 0)
Expand Down Expand Up @@ -248,18 +256,27 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex
return isThereClosingBracketBeforeOpeningBracket && isThereOpeningBracketBeforeClosingBracket
}

fun hasChanges(): EditorHasChanges {
return AztecText.hasChanges(initialEditorContentParsedSHA256, getPureHtml(false))
}

fun getPureHtml(withCursorTag: Boolean = false): String {
val str: String

if (withCursorTag) {
disableTextChangedListener()
val withCursor = StringBuffer(text)
if (!isCursorInsideTag()) {
text.insert(selectionEnd, "<aztec_cursor></aztec_cursor>")
withCursor.insert(selectionEnd, "<aztec_cursor></aztec_cursor>")
} else {
text.insert(text.lastIndexOf("<", selectionEnd), "<aztec_cursor></aztec_cursor>")
withCursor.insert(withCursor.lastIndexOf("<", selectionEnd), "<aztec_cursor></aztec_cursor>")
}
enableTextChangedListener()

str = withCursor.toString()
} else {
str = text.toString()
}

return Format.removeSourceEditorFormatting(text.toString(), isInCalypsoMode)
return Format.removeSourceEditorFormatting(str, isInCalypsoMode)
}

fun disableTextChangedListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import android.widget.Toast
import android.widget.ToggleButton
import org.wordpress.android.util.AppLog
import org.wordpress.aztec.AztecText
import org.wordpress.aztec.AztecText.EditorHasChanges.NO_CHANGES
import org.wordpress.aztec.AztecTextFormat
import org.wordpress.aztec.ITextFormat
import org.wordpress.aztec.R
Expand Down Expand Up @@ -565,13 +566,17 @@ class AztecToolbar : FrameLayout, IAztecToolbar, OnMenuItemClickListener {
if (sourceEditor == null) return

if (editor!!.visibility == View.VISIBLE) {
sourceEditor!!.displayStyledAndFormattedHtml(editor!!.toPlainHtml(true))
if (editor!!.hasChanges() != NO_CHANGES) {
sourceEditor!!.displayStyledAndFormattedHtml(editor!!.toPlainHtml(true))
}
editor!!.visibility = View.GONE
sourceEditor!!.visibility = View.VISIBLE

toggleHtmlMode(true)
} else {
editor!!.fromHtml(sourceEditor!!.getPureHtml(true))
if (sourceEditor!!.hasChanges() != NO_CHANGES) {
editor!!.fromHtml(sourceEditor!!.getPureHtml(true))
}
editor!!.visibility = View.VISIBLE
sourceEditor!!.visibility = View.GONE

Expand Down