Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve HTML in native TextViews #4273

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9392739
Disable headings button from the wikitext keyboard overlay
cooltey Nov 6, 2023
6f979fc
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Nov 7, 2023
bf36353
Show small image properly
cooltey Nov 7, 2023
fcd92d1
Convert multiple unit to px
cooltey Nov 7, 2023
f3da1bc
Add more small image support
cooltey Nov 8, 2023
1e81829
Add parameter back
cooltey Nov 8, 2023
ec0dd65
Listener onLoadFail
cooltey Nov 8, 2023
f9106d2
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Nov 9, 2023
7ed1575
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Nov 14, 2023
2135c9e
Prevent showing non-image file
cooltey Nov 15, 2023
89201e5
Initial support of li and ol
cooltey Nov 15, 2023
594daf9
Only add necessary span
cooltey Nov 15, 2023
9ddd453
Fix possible crash
cooltey Nov 15, 2023
754924f
Refine the logic of showing numbers
cooltey Nov 15, 2023
7dd62df
Add start margin for list items
cooltey Nov 15, 2023
da89db6
Simplified a bit
cooltey Nov 15, 2023
11d6d4f
Add dd and dl supporval spans = output.getSpans<LeadingMarginSpan>(ou…
cooltey Nov 16, 2023
f888906
Remove indent code
cooltey Nov 16, 2023
0b91efa
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Nov 16, 2023
03a5a9e
Add alignment support
cooltey Nov 17, 2023
8edd54c
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Nov 17, 2023
278d393
Add todo
cooltey Nov 18, 2023
3fda90e
Add an upper layer of try-catch logic
cooltey Nov 18, 2023
5a152ff
Apply a better try catch logic
cooltey Nov 18, 2023
8d3baad
Merge branch 'main' into wikitext-keyboard-html-design
sharvaniharan Nov 20, 2023
137795f
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Nov 21, 2023
3045b0d
Adding support of dl dd and dt
cooltey Nov 21, 2023
dc33175
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Nov 22, 2023
cd52def
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Dec 4, 2023
2ca2602
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Dec 14, 2023
c7f8bc2
Merge branch 'main' into wikitext-keyboard-html-design
cooltey Jan 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 81 additions & 13 deletions app/src/main/java/org/wikipedia/richtext/CustomHtmlParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.Html.ImageGetter
import android.text.Html.TagHandler
import android.text.Layout
import android.text.Spannable
import android.text.Spanned
import android.text.style.AlignmentSpan
import android.text.style.LeadingMarginSpan
import android.text.style.StyleSpan
import android.text.style.TypefaceSpan
import android.text.style.URLSpan
import android.webkit.MimeTypeMap
import android.widget.TextView
import androidx.core.graphics.applyCanvas
import androidx.core.text.HtmlCompat
Expand All @@ -24,10 +29,12 @@ import androidx.core.text.toSpanned
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import org.wikipedia.WikipediaApp
import org.wikipedia.dataclient.Service
import org.wikipedia.dataclient.WikiSite
import org.wikipedia.util.DimenUtil
import org.wikipedia.util.ResourceUtil
import org.wikipedia.util.UriUtil
import org.wikipedia.util.WhiteBackgroundTransformation
import org.wikipedia.util.log.L
import org.xml.sax.Attributes
Expand Down Expand Up @@ -107,8 +114,12 @@ class CustomHtmlParser constructor(private val handler: TagHandler) : TagHandler
wrapped?.skippedEntity(name)
}

class CustomTagHandler(private val view: TextView?) : TagHandler {
class CustomTagHandler(private val view: TextView?,
private val noSmallSizeImage: Boolean = true) : TagHandler {
private var lastAClass = ""
private var lastDivClass = ""
private var lastDivStyle = ""
private var lastSpannedDivString = ""
private var listItemCount = 0
private val listParents = mutableListOf<String>()
private val leadingMarginSize = DimenUtil.dpToPx(16f).toInt()
Expand All @@ -117,18 +128,42 @@ class CustomHtmlParser constructor(private val handler: TagHandler) : TagHandler
if (tag == "img" && view == null) {
return true
} else if (tag == "img" && opening && view != null) {
var imgWidth = DimenUtil.htmlPxToInt(getValue(attributes, "width").orEmpty())
var imgHeight = DimenUtil.htmlPxToInt(getValue(attributes, "height").orEmpty())
var imgWidthStr = getValue(attributes, "width").orEmpty()
var imgHeightStr = getValue(attributes, "height").orEmpty()
val styleStr = getValue(attributes, "style").orEmpty()
val widthRegex = "width:\\s*([\\d.]+)\\w{2}".toRegex()
if (imgWidthStr.isEmpty()) {
imgWidthStr = widthRegex.find(styleStr)?.value.orEmpty().replace("width:", "")
}
val heightRegex = "height:\\s*([\\d.]+)\\w{2}".toRegex()
if (imgHeightStr.isEmpty()) {
imgHeightStr = heightRegex.find(styleStr)?.value.orEmpty().replace("height:", "")
}
var imgWidth = DimenUtil.htmlUnitToPxInt(imgWidthStr) ?: MIN_IMAGE_SIZE
var imgHeight = DimenUtil.htmlUnitToPxInt(imgHeightStr) ?: MIN_IMAGE_SIZE
val imgSrc = getValue(attributes, "src").orEmpty()

if (imgWidth < MIN_IMAGE_SIZE || imgHeight < MIN_IMAGE_SIZE) {
if (noSmallSizeImage && (imgWidth < MIN_IMAGE_SIZE || imgHeight < MIN_IMAGE_SIZE)) {
return true
}

imgWidth = DimenUtil.roundedDpToPx(imgWidth.toFloat())
imgHeight = DimenUtil.roundedDpToPx(imgHeight.toFloat())

if (imgWidth > 0 && imgHeight > 0 && imgSrc.isNotEmpty()) {
val uri = if (imgSrc.startsWith("//")) {
WikiSite.DEFAULT_SCHEME + ":" + imgSrc
} else if (imgSrc.startsWith("./")) {
Service.COMMONS_URL + imgSrc.replace("./", "")
} else {
UriUtil.resolveProtocolRelativeUrl(WikiSite.forLanguageCode(WikipediaApp.instance.appOrSystemLanguageCode), imgSrc)
}

val extension = MimeTypeMap.getFileExtensionFromUrl(uri)
if (!MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension).orEmpty().contains("image", true)) {
return true
}

val bmpMap = contextBmpMap.getOrPut(view.context) { mutableMapOf() }
var drawable = bmpMap[imgSrc]

Expand All @@ -140,16 +175,10 @@ class CustomHtmlParser constructor(private val handler: TagHandler) : TagHandler

drawable.setBounds(0, 0, imgWidth, imgHeight)

var uri = imgSrc
if (uri.startsWith("//")) {
uri = WikiSite.DEFAULT_SCHEME + ":" + uri
} else if (uri.startsWith("./")) {
uri = Service.COMMONS_URL + uri.replace("./", "")
}

Glide.with(view)
.asBitmap()
.load(uri)
.transform(WhiteBackgroundTransformation())
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
if (!drawable.bitmap.isRecycled) {
Expand Down Expand Up @@ -199,6 +228,45 @@ class CustomHtmlParser constructor(private val handler: TagHandler) : TagHandler
listItemCount = 0
} else if (tag == "li" && listParents.isNotEmpty() && !opening && output != null) {
handleListTag(output)
} else if (tag == "div" && output != null) {
if (opening) {
lastDivStyle = getValue(attributes, "style").orEmpty()
lastDivClass = getValue(attributes, "class").orEmpty()
lastSpannedDivString = output.toString()
} else {
val alignmentSpan = if (lastDivClass == "center" || lastDivStyle.contains("margin-left: auto", true) && lastDivStyle.contains("margin-right: auto", true)) {
Layout.Alignment.ALIGN_CENTER
} else if (lastDivClass == "floatright" || lastDivStyle.contains("text-align: right", true)) {
Layout.Alignment.ALIGN_OPPOSITE
} else {
Layout.Alignment.ALIGN_NORMAL
}
val start = lastSpannedDivString.length
val end = output.length
val spans = output.getSpans<AlignmentSpan>(end)
if (start < end && spans.isEmpty()) {
// TODO: fix unexpected error that cannot be escaped.
output.setSpan(AlignmentSpan.Standard(alignmentSpan), start, end, 0)
}
}
} else if ((tag == "dd" || tag == "dl") && output != null) {
if (opening) {
// TODO: maybe replace with LeadingMarginSpan
output.append("\n")
output.append(" ")
}
} else if (tag == "dt" && output != null) {
if (opening) {
output.setSpan(StyleSpan(Typeface.BOLD), output.length, output.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
} else {
val spans = output.getSpans<StyleSpan>(output.length)
if (spans.isNotEmpty()) {
val span = spans.last()
val start = output.getSpanStart(span)
output.removeSpan(span)
output.setSpan(StyleSpan(Typeface.BOLD), start, output.length, 0)
}
}
}
return false
}
Expand Down Expand Up @@ -250,7 +318,7 @@ class CustomHtmlParser constructor(private val handler: TagHandler) : TagHandler
private const val MIN_IMAGE_SIZE = 64
private val contextBmpMap = mutableMapOf<Context, MutableMap<String, BitmapDrawable>>()

fun fromHtml(html: String?, view: TextView? = null): Spanned {
fun fromHtml(html: String?, view: TextView? = null, hasMinImageSize: Boolean = true): Spanned {
var sourceStr = html.orEmpty()

if ("<" !in sourceStr && "&" !in sourceStr) {
Expand All @@ -269,7 +337,7 @@ class CustomHtmlParser constructor(private val handler: TagHandler) : TagHandler
// This would become something like "<inject/>$sourceStr".parseAsHtml(...)
return sourceStr.parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY,
if (view == null) null else CustomImageGetter(view.context),
CustomHtmlParser(CustomTagHandler(view)))
CustomHtmlParser(CustomTagHandler(view, hasMinImageSize)))
}

fun pruneBitmaps(context: Context) {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/org/wikipedia/richtext/TextViewHtml.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package org.wikipedia.richtext

import android.widget.TextView
import org.wikipedia.util.StringUtil

fun TextView.setHtml(source: String?) {
this.text = CustomHtmlParser.fromHtml(source, this).trim()
this.text = StringUtil.fromHtml(source).trim()
}
10 changes: 8 additions & 2 deletions app/src/main/java/org/wikipedia/talk/TalkThreadItemView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.wikipedia.databinding.ItemTalkThreadItemBinding
import org.wikipedia.dataclient.discussiontools.ThreadItem
import org.wikipedia.richtext.CustomHtmlParser
import org.wikipedia.util.*
import org.wikipedia.util.log.L

@SuppressLint("RestrictedApi")
class TalkThreadItemView constructor(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) {
Expand Down Expand Up @@ -74,8 +75,13 @@ class TalkThreadItemView constructor(context: Context, attrs: AttributeSet? = nu
val timestamp = DateUtil.getTimeAndDateString(context, it)
StringUtil.setHighlightedAndBoldenedText(binding.timeStampText, timestamp, searchQuery)
}
val body = CustomHtmlParser.fromHtml(StringUtil.removeStyleTags(item.html), binding.bodyText).trim()
StringUtil.setHighlightedAndBoldenedText(binding.bodyText, body, searchQuery)

try {
StringUtil.setHighlightedAndBoldenedText(binding.bodyText, CustomHtmlParser.fromHtml(StringUtil.removeStyleTags(item.html), binding.bodyText, hasMinImageSize = false).trim(), searchQuery)
} catch (e: Exception) {
L.e("Error on parsing HTML tags $e")
StringUtil.setHighlightedAndBoldenedText(binding.bodyText, StringUtil.fromHtml(item.html).trim(), searchQuery)
}
binding.bodyText.movementMethod = movementMethod

if (replying) {
Expand Down
16 changes: 10 additions & 6 deletions app/src/main/java/org/wikipedia/util/DimenUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,21 @@ object DimenUtil {
private val resources: Resources
get() = WikipediaApp.instance.resources

fun htmlPxToInt(str: String): Int {
fun htmlUnitToPxInt(str: String): Int? {
try {
return if (str.contains("px")) {
str.replace("px", "").toInt()
} else {
str.toInt()
val unitRegex = "[A-Za-z]{2}".toRegex()
val unit = unitRegex.find(str)?.value.orEmpty()
var value = str.replace(unit, "").toFloat().toInt()
if (unit == "ex") {
value *= 6
} else if (unit == "em") {
value *= 10
}
return value
} catch (e: Exception) {
L.e(e)
}
return 0
return null
}

fun getNavigationBarHeight(context: Context): Float {
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/java/org/wikipedia/util/StringUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.text.style.StyleSpan
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.IntRange
import androidx.core.text.HtmlCompat
import androidx.core.text.buildSpannedString
import androidx.core.text.set
import okio.ByteString.Companion.encodeUtf8
Expand Down Expand Up @@ -109,7 +110,11 @@ object StringUtil {
}

fun fromHtml(source: String?): Spanned {
return CustomHtmlParser.fromHtml(source)
return try {
CustomHtmlParser.fromHtml(source)
} catch (e: Exception) {
HtmlCompat.fromHtml(source.orEmpty(), HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}

fun highlightEditText(editText: EditText, parentText: String, highlightText: String) {
Expand Down
Loading