Skip to content
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
90 changes: 25 additions & 65 deletions legacy/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt
Original file line number Diff line number Diff line change
@@ -1,65 +1,40 @@
package com.fsck.k9.message.html

import app.k9mail.html.cleaner.HtmlHeadProvider
import org.intellij.lang.annotations.Language

class DisplayHtml(private val settings: HtmlSettings) : HtmlHeadProvider {
override val headHtml: String
get() {
@Language("HTML")
val html = """
<meta name="viewport" content="width=device-width"/>
${cssStyleGlobal()}
${cssStylePre()}
${cssStyleSignature()}
""".trimIndent()

return html
return """<meta name="viewport" content="width=device-width"/>""" +
cssStyleTheme() +
cssStylePre() +
cssStyleSignature()
}

fun wrapStatusMessage(status: CharSequence): String {
@Language("HTML")
val html = """
<div style="text-align:center; color: grey;">$status</div>
""".trimIndent()

return wrapMessageContent(html)
return wrapMessageContent("<div style=\"text-align:center; color: grey;\">$status</div>")
}

@Language("HTML")
fun wrapMessageContent(messageContent: CharSequence): String {
// Include a meta tag so the WebView will not use a fixed viewport width of 980 px
return """
<html dir="auto">
<head>
$headHtml
</head>
<body>
$messageContent
</body>
</html>
""".trimIndent()
return "<html dir=\"auto\"><head><meta name=\"viewport\" content=\"width=device-width\"/>" +
cssStyleTheme() +
cssStylePre() +
"</head><body>" +
messageContent +
"</body></html>"
}

/**
* Dynamically generates a CSS style block that applies global rules to all elements (`*` selector).
*
* The style enforces word-breaking and overflow wrapping to prevent content overflow
* and ensures long text strings break correctly without causing horizontal scrolling.
*
* @return A `<style>` element string that can be dynamically injected into the HTML `<head>`
* to apply these global styles when rendering messages.
*/
@Language("HTML")
private fun cssStyleGlobal(): String {
return """
<style type="text/css">
* {
word-break: break-word;
overflow-wrap: break-word;
}
</style>
""".trimIndent()
private fun cssStyleTheme(): String {
return if (settings.useDarkMode) {
// TODO: Don't hardcode these values. Inject them via HtmlSettings.
"<style type=\"text/css\">" +
"* { background: #121212 ! important; color: #F3F3F3 !important }" +
":link, :link * { color: #CCFF33 !important }" +
":visited, :visited * { color: #551A8B !important }</style> "
} else {
""
}
}

/**
Expand All @@ -70,30 +45,15 @@ class DisplayHtml(private val settings: HtmlSettings) : HtmlHeadProvider {
* @return A `<style>` element that can be dynamically included in the HTML `<head>` element when messages are
* displayed.
*/
@Language("HTML")
private fun cssStylePre(): String {
val font = if (settings.useFixedWidthFont) "monospace" else "sans-serif"

return """
<style type="text/css">
pre.${EmailTextToHtml.K9MAIL_CSS_CLASS} {
white-space: pre-wrap;
word-wrap: break-word;
font-family: $font;
margin-top: 0px;
}
</style>
""".trimIndent()
return "<style type=\"text/css\"> pre." + EmailTextToHtml.K9MAIL_CSS_CLASS +
" {white-space: pre-wrap; word-wrap:break-word; " +
"font-family: " + font + "; margin-top: 0px}</style>"
}

@Language("HTML")
private fun cssStyleSignature(): String {
return """
<style type="text/css">
.k9mail-signature {
opacity: 0.5;
}
</style>
""".trimIndent()
return """<style type="text/css">.k9mail-signature { opacity: 0.5 }</style>"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package com.fsck.k9.message.html

import assertk.Assert
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import org.jsoup.Jsoup
import org.junit.Test

class DisplayHtmlTest {
val htmlSettings = HtmlSettings(useDarkMode = false, useFixedWidthFont = false)
val displayHtml = DisplayHtml(htmlSettings)
val displayHtml = DisplayHtml(HtmlSettings(useDarkMode = false, useFixedWidthFont = false))

@Test
fun wrapMessageContent_addsViewportMetaElement() {
Expand All @@ -27,45 +25,19 @@ class DisplayHtmlTest {
}

@Test
fun wrapMessageContent_addsPreCSSStyles() {
fun wrapMessageContent_addsPreCSS() {
val html = displayHtml.wrapMessageContent("Some text")

assertThat(html).containsHtmlElement("head > style", 3)
}

@Test
fun wrapMessageContent_addsGlobalStyleRules() {
val html = displayHtml.wrapMessageContent("test")

assertThat(html).containsStyleRulesFor(
selector = "*",
"word-break: break-word;",
"overflow-wrap: break-word;",
)
assertThat(html).containsHtmlElement("head > style")
}

@Test
fun wrapMessageContent_addsPreCSS() {
val html = displayHtml.wrapMessageContent("test")
val expectedFont = if (htmlSettings.useFixedWidthFont) "monospace" else "sans-serif"

assertThat(html).containsStyleRulesFor(
selector = "pre.${EmailTextToHtml.K9MAIL_CSS_CLASS}",
"white-space: pre-wrap;",
"word-wrap: break-word;",
"font-family: $expectedFont;",
"margin-top: 0px;",
)
}
fun wrapMessageContent_whenDarkMessageViewTheme_addsDarkThemeCSS() {
val darkModeDisplayHtml = DisplayHtml(HtmlSettings(useDarkMode = true, useFixedWidthFont = false))

@Test
fun wrapMessageContent_addsSignatureStyleRules() {
val html = displayHtml.wrapMessageContent("test")
val html = darkModeDisplayHtml.wrapMessageContent("Some text")

assertThat(html).containsStyleRulesFor(
selector = ".k9mail-signature",
"opacity: 0.5;",
)
assertThat(html).htmlElements("head > style").hasSize(2)
}

@Test
Expand All @@ -77,26 +49,8 @@ class DisplayHtmlTest {
assertThat(html).bodyText().isEqualTo(content)
}

private fun Assert<String>.containsStyleRulesFor(selector: String, vararg expectedRules: String) = given { html ->
val styleContent = Jsoup.parse(html)
.select("style")
.joinToString("\n") { it.data() }

val selectorPattern = Regex.escape(selector).replace("\\*", "\\\\*")
val selectorBlock = Regex("$selectorPattern\\s*\\{([^}]*)\\}", RegexOption.MULTILINE)
.find(styleContent)
?.groupValues?.get(1)
?.trim()

requireNotNull(selectorBlock) { "No style block found for selector: $selector" }

expectedRules.forEach { rule ->
assertThat(selectorBlock).contains(rule)
}
}

private fun Assert<String>.containsHtmlElement(cssQuery: String, expectedCount: Int = 1) = given { actual ->
assertThat(actual).htmlElements(cssQuery).hasSize(expectedCount)
private fun Assert<String>.containsHtmlElement(cssQuery: String) = given { actual ->
assertThat(actual).htmlElements(cssQuery).hasSize(1)
}

private fun Assert<String>.htmlElements(cssQuery: String) = transform { html ->
Expand Down