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

Escape HTML that isn't parsed in local feeds #708

Merged
merged 2 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,7 +107,7 @@ fun ArticleRow(
ArticleListItem(
headlineContent = {
Text(
article.title,
AnnotatedString.fromHtml(article.title),
fontWeight = FontWeight.Bold,
)
},
Expand Down
6 changes: 4 additions & 2 deletions capy/src/main/java/com/jocmp/capy/accounts/ParsedItem.kt
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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()
Expand Down
48 changes: 48 additions & 0 deletions capy/src/main/java/com/jocmp/capy/common/StringCharactersExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,51 @@ val String.unescapingHTMLCharacters: String
.replace("&lt;", "<")
.replace("&gt;", ">")
}

/**
* 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("&lt;")
} else if (c == '>') {
out.append("&gt;")
} else if (c == '&') {
out.append("&amp;")
} 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("&nbsp;")
i++
}

out.append(' ')
} else {
out.append(c)
}
i++
}
}
10 changes: 10 additions & 0 deletions capy/src/test/java/com/jocmp/capy/accounts/ParsedItemTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ class ParsedItemTest {
assertEquals(expected = "My Fancy Title", actual = parsedItem.title)
}

@Test
fun title_withNestedHTML() {
val title = "<mark>The `&lt;details&gt;` and `&lt;summary&gt;` elements are getting an upgrade</mark>"

val item = RssItem.Builder().title(title).build()
val parsedItem = ParsedItem(item, siteURL = "")

assertEquals(expected = "The `&lt;details&gt;` and `&lt;summary&gt;` elements are getting an upgrade", actual = parsedItem.title)
}

@Test
fun title_whenNull() {
val item = RssItem.Builder().title(null).build()
Expand Down
Loading