From 36240e049925182d62898c8a77fbabc5f04fea47 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Sat, 26 Jan 2019 04:46:04 +0200 Subject: [PATCH 1/7] 1-deep cache of the spanned result --- aztec/src/main/java/org/wordpress/aztec/Html.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/aztec/src/main/java/org/wordpress/aztec/Html.java b/aztec/src/main/java/org/wordpress/aztec/Html.java index a4f3aaa8b..7457926b1 100644 --- a/aztec/src/main/java/org/wordpress/aztec/Html.java +++ b/aztec/src/main/java/org/wordpress/aztec/Html.java @@ -251,7 +251,14 @@ public HtmlToSpannedConverter( this.ignoredTags = ignoredTags; } + private static Spanned cachedResult; + private static int cachedSourceHash; + public Spanned convert() { + if (cachedSourceHash == source.hashCode() && cachedResult != null) { + return new SpannableStringBuilder(cachedResult); + } + reader.setContentHandler(this); try { reader.setProperty(Parser.lexicalHandlerProperty, this); @@ -288,7 +295,10 @@ public Spanned convert() { } } - return spannableStringBuilder; + cachedResult = spannableStringBuilder; + cachedSourceHash = source.hashCode(); + + return new SpannableStringBuilder(spannableStringBuilder); } private void handleStartTag(String tag, Attributes attributes, int nestingLevel) { From 4d461f5a889a6b1c147e7abd2a64456966bbc3f6 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Thu, 7 Feb 2019 02:05:27 +0200 Subject: [PATCH 2/7] Fast variant of fromHtml, suitable for inspecting spans --- .../kotlin/org/wordpress/aztec/AztecParser.kt | 19 +++++++++++++++++-- .../aztec/plugins/CssUnderlinePlugin.kt | 5 +++-- .../plugins/html2visual/ISpanPostprocessor.kt | 6 +++--- .../org/wordpress/aztec/source/Format.kt | 2 +- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt index dc295eea5..ea9b3f8bc 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,20 @@ 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. + */ + 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 +132,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 +251,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/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) From 0d4e08c196fe36887faa4dcf378cb22579da1a63 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Wed, 6 Mar 2019 11:41:04 +0200 Subject: [PATCH 3/7] Revert "1-deep cache of the spanned result" This reverts commit 36240e049925182d62898c8a77fbabc5f04fea47. --- aztec/src/main/java/org/wordpress/aztec/Html.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/aztec/src/main/java/org/wordpress/aztec/Html.java b/aztec/src/main/java/org/wordpress/aztec/Html.java index eff6b3da6..434ad16cf 100644 --- a/aztec/src/main/java/org/wordpress/aztec/Html.java +++ b/aztec/src/main/java/org/wordpress/aztec/Html.java @@ -252,14 +252,7 @@ public HtmlToSpannedConverter( this.ignoredTags = ignoredTags; } - private static Spanned cachedResult; - private static int cachedSourceHash; - public Spanned convert() { - if (cachedSourceHash == source.hashCode() && cachedResult != null) { - return new SpannableStringBuilder(cachedResult); - } - reader.setContentHandler(this); try { reader.setProperty(Parser.lexicalHandlerProperty, this); @@ -296,10 +289,7 @@ public Spanned convert() { } } - cachedResult = spannableStringBuilder; - cachedSourceHash = source.hashCode(); - - return new SpannableStringBuilder(spannableStringBuilder); + return spannableStringBuilder; } private void handleStartTag(String tag, Attributes attributes, int nestingLevel) { From 7112c2074487002c71b12a07be39a8c0a8369301 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Wed, 6 Mar 2019 11:41:12 +0200 Subject: [PATCH 4/7] LIFO stack of marks for speed optimization --- .../org/wordpress/aztec/AztecTagHandler.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt index d061fa115..bac271d9a 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt @@ -53,6 +53,8 @@ import java.util.ArrayList class AztecTagHandler(val context: Context, val plugins: List = ArrayList()) : Html.TagHandler { private val loadingDrawable: Drawable + private val markStack = 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 +165,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 +179,23 @@ class AztecTagHandler(val context: Context, val plugins: List = Ar } private fun start(output: Editable, mark: Any) { + markStack.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 the most recent mark from the stack. + // This is a speed optimization instead of getting it from the spannable via `getLast()` + val last = if (markStack.size > 0 && kind.equals(markStack[markStack.size - 1].javaClass)) { + markStack.removeAt(markStack.size - 1) // remove and return the top mark on the stack + } else { + // Warning: the marks stack is apparently incosistent at this point + + // fall back to getting the last mark from the Spannable + output.getLast(kind) + } + val start = output.getSpanStart(last) val end = output.length From 3c1b41f4bbd3c9697c5deafaea54814f83b1907d Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Wed, 6 Mar 2019 14:01:51 +0200 Subject: [PATCH 5/7] Renaming and some more comments --- .../org/wordpress/aztec/AztecTagHandler.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt index bac271d9a..0ec55ed80 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt @@ -53,7 +53,8 @@ import java.util.ArrayList class AztecTagHandler(val context: Context, val plugins: List = ArrayList()) : Html.TagHandler { private val loadingDrawable: Drawable - private val markStack = mutableListOf() + // 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) @@ -179,20 +180,20 @@ class AztecTagHandler(val context: Context, val plugins: List = Ar } private fun start(output: Editable, mark: Any) { - markStack.add(mark) + tagStack.add(mark) output.setSpan(mark, output.length, output.length, Spanned.SPAN_MARK_MARK) } private fun end(output: Editable, kind: Class<*>) { - // Get the most recent mark from the stack. - // This is a speed optimization instead of getting it from the spannable via `getLast()` - val last = if (markStack.size > 0 && kind.equals(markStack[markStack.size - 1].javaClass)) { - markStack.removeAt(markStack.size - 1) // remove and return the top mark on the stack + // 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) + 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 marks stack is apparently incosistent at this point + // Warning: the tags stack is apparently inconsistent at this point - // fall back to getting the last mark from the Spannable + // fall back to getting the last tag type from the Spannable output.getLast(kind) } From bd1c9fa37ff5f332720186c3f5d1dee5f4e0944d Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Wed, 6 Mar 2019 14:14:25 +0200 Subject: [PATCH 6/7] Suppress the unused inspection, method is used in wpandroid --- aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt index ea9b3f8bc..8c920edfb 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt @@ -62,6 +62,7 @@ class AztecParser @JvmOverloads constructor(val plugins: List = li * 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) From ee83fa8f21b2f60d0b3ca147ecb9e4747898e60a Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Thu, 7 Mar 2019 14:57:52 +0200 Subject: [PATCH 7/7] Add detail about empty html elements in the comment --- aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt index 0ec55ed80..3792239bb 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt @@ -187,7 +187,8 @@ class AztecTagHandler(val context: Context, val plugins: List = Ar private fun end(output: Editable, kind: Class<*>) { // 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) + // 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 {