diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt index dc295eea5..8c920edfb 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt @@ -22,6 +22,7 @@ import android.content.Context import android.support.v4.text.TextDirectionHeuristicsCompat import android.text.Editable import android.text.Spannable +import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextUtils @@ -57,6 +58,21 @@ import java.util.Comparator class AztecParser @JvmOverloads constructor(val plugins: List = listOf(), private val ignoredTags: List = listOf("body", "html")) { + /** + * A faster version of fromHtml(), intended for inspecting the span structure only. It doesn't prepare the text for + * visual editing. + */ + @Suppress("unused") // this method is used in wpandroid so, suppress the inspection + fun parseHtmlForInspection(source: String, context: Context): Spanned { + val tidySource = tidy(source) + + val spanned = SpannableString(Html.fromHtml(tidySource, + AztecTagHandler(context, plugins), context, plugins, ignoredTags)) + + postprocessSpans(spanned) + + return spanned + } fun fromHtml(source: String, context: Context): Spanned { val tidySource = tidy(source) @@ -117,7 +133,7 @@ class AztecParser @JvmOverloads constructor(val plugins: List = li return html } - private fun postprocessSpans(spannable: SpannableStringBuilder) { + private fun postprocessSpans(spannable: Spannable) { plugins.filter { it is ISpanPostprocessor } .map { it as ISpanPostprocessor } .forEach { @@ -236,7 +252,7 @@ class AztecParser @JvmOverloads constructor(val plugins: List = li } // Always try to put a visual newline before block elements and only put one after if needed - fun syncVisualNewlinesOfBlockElements(spanned: Editable) { + fun syncVisualNewlinesOfBlockElements(spanned: Spannable) { // clear any visual newline marking. We'll mark them with a fresh set of passes spanned.getSpans(0, spanned.length, AztecVisualLinebreak::class.java).forEach { spanned.removeSpan(it) diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt index d061fa115..3792239bb 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt @@ -53,6 +53,9 @@ import java.util.ArrayList class AztecTagHandler(val context: Context, val plugins: List = ArrayList()) : Html.TagHandler { private val loadingDrawable: Drawable + // Simple LIFO stack to track the html tag nesting for easy reference when we need to handle the ending of a tag + private val tagStack = mutableListOf() + init { val styles = context.obtainStyledAttributes(R.styleable.AztecText) loadingDrawable = ContextCompat.getDrawable(context, styles.getResourceId(R.styleable.AztecText_drawableLoading, R.drawable.ic_image_loading))!! @@ -163,8 +166,8 @@ class AztecTagHandler(val context: Context, val plugins: List = Ar start(output, AztecMediaClickableSpan(mediaSpan)) output.append(Constants.IMG_CHAR) } else { - end(output, mediaSpan.javaClass) end(output, AztecMediaClickableSpan::class.java) + end(output, mediaSpan.javaClass) } } @@ -177,11 +180,24 @@ class AztecTagHandler(val context: Context, val plugins: List = Ar } private fun start(output: Editable, mark: Any) { + tagStack.add(mark) + output.setSpan(mark, output.length, output.length, Spanned.SPAN_MARK_MARK) } private fun end(output: Editable, kind: Class<*>) { - val last = output.getLast(kind) + // Get most recent tag type from the stack instead of `getLast()`. This is a speed optimization as `getLast()` + // doesn't know that the tags are in fact nested and in pairs (since it's html), including empty html elements + // (they are treated as pairs by tagsoup anyway). + val last = if (tagStack.size > 0 && kind.equals(tagStack[tagStack.size - 1].javaClass)) { + tagStack.removeAt(tagStack.size - 1) // remove and return the top mark on the stack + } else { + // Warning: the tags stack is apparently inconsistent at this point + + // fall back to getting the last tag type from the Spannable + output.getLast(kind) + } + val start = output.getSpanStart(last) val end = output.length diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/plugins/CssUnderlinePlugin.kt b/aztec/src/main/kotlin/org/wordpress/aztec/plugins/CssUnderlinePlugin.kt index b0fe40e3b..c0b35efa0 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/plugins/CssUnderlinePlugin.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/plugins/CssUnderlinePlugin.kt @@ -1,5 +1,6 @@ package org.wordpress.aztec.plugins +import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import org.wordpress.aztec.plugins.html2visual.ISpanPostprocessor @@ -50,7 +51,7 @@ class CssUnderlinePlugin : ISpanPostprocessor, ISpanPreprocessor { } } - override fun afterSpansProcessed(spannable: SpannableStringBuilder) { + override fun afterSpansProcessed(spannable: Spannable) { spannable.getSpans(0, spannable.length, HiddenHtmlSpan::class.java).forEach { if (it.TAG == SPAN_TAG && CssStyleFormatter.containsStyleAttribute(it.attributes, CssStyleFormatter.CSS_TEXT_DECORATION_ATTRIBUTE)) { CssStyleFormatter.removeStyleAttribute(it.attributes, CssStyleFormatter.CSS_TEXT_DECORATION_ATTRIBUTE) @@ -62,4 +63,4 @@ class CssUnderlinePlugin : ISpanPostprocessor, ISpanPreprocessor { } } } -} \ No newline at end of file +} diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/plugins/html2visual/ISpanPostprocessor.kt b/aztec/src/main/kotlin/org/wordpress/aztec/plugins/html2visual/ISpanPostprocessor.kt index 70afa8b1c..be9ba8917 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/plugins/html2visual/ISpanPostprocessor.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/plugins/html2visual/ISpanPostprocessor.kt @@ -1,8 +1,8 @@ package org.wordpress.aztec.plugins.html2visual -import android.text.SpannableStringBuilder +import android.text.Spannable import org.wordpress.aztec.plugins.IAztecPlugin interface ISpanPostprocessor : IAztecPlugin { - fun afterSpansProcessed(spannable: SpannableStringBuilder) -} \ No newline at end of file + fun afterSpansProcessed(spannable: Spannable) +} diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/source/Format.kt b/aztec/src/main/kotlin/org/wordpress/aztec/source/Format.kt index 2f6c0d2ac..d73ba4810 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/source/Format.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/source/Format.kt @@ -298,7 +298,7 @@ object Format { } @JvmStatic - fun preProcessSpannedText(text: SpannableStringBuilder, isCalypsoFormat: Boolean) { + fun preProcessSpannedText(text: Spannable, isCalypsoFormat: Boolean) { if (isCalypsoFormat) { text.getSpans(0, text.length, AztecVisualLinebreak::class.java).forEach { val spanStart = text.getSpanStart(it)