From 13b1b0e51408ab5630229a8a627a46e62c4149d7 Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:21:47 -0600 Subject: [PATCH 1/2] Escape HTML that isn't parsed --- .../com/jocmp/capy/accounts/ParsedItem.kt | 6 ++- .../jocmp/capy/common/StringCharactersExt.kt | 48 +++++++++++++++++++ .../com/jocmp/capy/accounts/ParsedItemTest.kt | 10 ++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/capy/src/main/java/com/jocmp/capy/accounts/ParsedItem.kt b/capy/src/main/java/com/jocmp/capy/accounts/ParsedItem.kt index b2a71f63..cdc7f6a6 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/ParsedItem.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/ParsedItem.kt @@ -1,7 +1,9 @@ package com.jocmp.capy.accounts +import com.jocmp.capy.common.escapingHTMLCharacters import com.jocmp.rssparser.model.RssItem import org.jsoup.Jsoup +import org.jsoup.safety.Safelist import java.net.URI import java.net.URL @@ -28,12 +30,12 @@ internal class ParsedItem(private val item: RssItem, private val siteURL: String if (it.isBlank()) { null } else { - Jsoup.parse(it).text() + Jsoup.clean(it, Safelist.none()) } } val title: String - get() = Jsoup.parse(item.title.orEmpty()).text() + get() = Jsoup.parse(item.title.orEmpty()).text().escapingHTMLCharacters val imageURL: String? get() = cleanedURL(item.image)?.toString() diff --git a/capy/src/main/java/com/jocmp/capy/common/StringCharactersExt.kt b/capy/src/main/java/com/jocmp/capy/common/StringCharactersExt.kt index 898b3103..ee039fc0 100644 --- a/capy/src/main/java/com/jocmp/capy/common/StringCharactersExt.kt +++ b/capy/src/main/java/com/jocmp/capy/common/StringCharactersExt.kt @@ -29,3 +29,51 @@ val String.unescapingHTMLCharacters: String .replace("<", "<") .replace(">", ">") } + +/** + * Returns an HTML escaped representation of the given plain text. + * + * Copied from android.text.Html + */ +val String.escapingHTMLCharacters: String + get() { + val out = java.lang.StringBuilder() + withinStyle(out, this, 0, this.length) + return out.toString() + } + +private fun withinStyle(out: StringBuilder, text: CharSequence, start: Int, end: Int) { + var i: Int = start + while (i < end) { + val c: Char = text.get(i) + + if (c == '<') { + out.append("<") + } else if (c == '>') { + out.append(">") + } else if (c == '&') { + out.append("&") + } else if (c.code in 0xD800..0xDFFF) { + if (c.code < 0xDC00 && i + 1 < end) { + val d: Char = text.get(i + 1) + if (d.code in 0xDC00..0xDFFF) { + i++ + val codepoint = 0x010000 or (c.code - 0xD800 shl 10) or d.code - 0xDC00 + out.append("&#").append(codepoint).append(";") + } + } + } else if (c.code > 0x7E || c < ' ') { + out.append("&#").append(c.code).append(";") + } else if (c == ' ') { + while (i + 1 < end && text.get(i + 1) == ' ') { + out.append(" ") + i++ + } + + out.append(' ') + } else { + out.append(c) + } + i++ + } +} diff --git a/capy/src/test/java/com/jocmp/capy/accounts/ParsedItemTest.kt b/capy/src/test/java/com/jocmp/capy/accounts/ParsedItemTest.kt index 1c5cf3b4..e593aede 100644 --- a/capy/src/test/java/com/jocmp/capy/accounts/ParsedItemTest.kt +++ b/capy/src/test/java/com/jocmp/capy/accounts/ParsedItemTest.kt @@ -24,6 +24,16 @@ class ParsedItemTest { assertEquals(expected = "My Fancy Title", actual = parsedItem.title) } + @Test + fun title_withNestedHTML() { + val title = "The `<details>` and `<summary>` elements are getting an upgrade" + + val item = RssItem.Builder().title(title).build() + val parsedItem = ParsedItem(item, siteURL = "") + + assertEquals(expected = "The `<details>` and `<summary>` elements are getting an upgrade", actual = parsedItem.title) + } + @Test fun title_whenNull() { val item = RssItem.Builder().title(null).build() From 1c90b58e1a82253a2e042026f9ce6e4f724c68cc Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:27:50 -0600 Subject: [PATCH 2/2] Annotate article row title with html --- .../main/java/com/capyreader/app/ui/articles/ArticleRow.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt index e66bf40f..c24b269d 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt @@ -40,7 +40,9 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -105,7 +107,7 @@ fun ArticleRow( ArticleListItem( headlineContent = { Text( - article.title, + AnnotatedString.fromHtml(article.title), fontWeight = FontWeight.Bold, ) },