diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 267d9ae7..414e13fa 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -110,7 +110,6 @@ dependencies { implementation(libs.androidx.paging.compose) implementation(libs.androidx.profileinstaller) implementation(libs.androidx.work.runtime) - implementation(libs.copydown) implementation(libs.dagger) implementation(libs.eithernet) implementation(libs.haze) diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/BaseActivity.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/BaseActivity.kt index 183d761c..f25690c6 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/BaseActivity.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/BaseActivity.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.core.net.toUri import com.deliveryhero.whetstone.Whetstone import dev.msfjarvis.claw.android.viewmodel.ClawViewModel -import dev.msfjarvis.claw.common.comments.HTMLConverter import dev.msfjarvis.claw.common.theme.LobstersTheme import dev.msfjarvis.claw.common.urllauncher.UrlLauncher import javax.inject.Inject @@ -29,7 +28,6 @@ import javax.inject.Inject abstract class BaseActivity : ComponentActivity() { @Inject lateinit var urlLauncher: UrlLauncher - @Inject lateinit var htmlConverter: HTMLConverter @Inject lateinit var viewModel: ClawViewModel var webUri: String? = null diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/MainActivity.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/MainActivity.kt index 3133a49a..ba69c706 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/MainActivity.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/MainActivity.kt @@ -29,18 +29,13 @@ class MainActivity : BaseActivity() { WindowWidthSizeClass.Compact -> { LobstersPostsScreen( urlLauncher = urlLauncher, - htmlConverter = htmlConverter, windowSizeClass = windowSizeClass, setWebUri = { url -> webUri = url }, ) } else -> { - TabletScreen( - urlLauncher = urlLauncher, - htmlConverter = htmlConverter, - modifier = Modifier.fillMaxSize(), - ) + TabletScreen(urlLauncher = urlLauncher, modifier = Modifier.fillMaxSize()) } } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/SearchActivity.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/SearchActivity.kt index 914b2928..bd9ad7d7 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/SearchActivity.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/SearchActivity.kt @@ -14,11 +14,6 @@ import dev.msfjarvis.claw.android.ui.screens.SearchScreen class SearchActivity : BaseActivity() { @Composable override fun Content() { - SearchScreen( - urlLauncher = urlLauncher, - htmlConverter = htmlConverter, - setWebUri = { webUri = it }, - viewModel = viewModel, - ) + SearchScreen(urlLauncher = urlLauncher, setWebUri = { webUri = it }, viewModel = viewModel) } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/HTMLConverterModule.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/HTMLConverterModule.kt deleted file mode 100644 index edefa3a4..00000000 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/HTMLConverterModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright © Harsh Shandilya. - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE file or at - * https://opensource.org/licenses/MIT. - */ -package dev.msfjarvis.claw.android.injection - -import com.deliveryhero.whetstone.app.ApplicationScope -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import dev.msfjarvis.claw.android.ui.util.HTMLConverterImpl -import dev.msfjarvis.claw.common.comments.HTMLConverter - -@Module -@ContributesTo(ApplicationScope::class) -interface HTMLConverterModule { - @Binds fun bindHTMLConverter(impl: HTMLConverterImpl): HTMLConverter -} diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt index a5bd3d82..11c80178 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt @@ -76,7 +76,6 @@ import dev.msfjarvis.claw.android.ui.navigation.any import dev.msfjarvis.claw.android.ui.navigation.none import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.comments.CommentsPage -import dev.msfjarvis.claw.common.comments.HTMLConverter import dev.msfjarvis.claw.common.urllauncher.UrlLauncher import dev.msfjarvis.claw.common.user.UserProfile import kotlinx.collections.immutable.persistentListOf @@ -88,7 +87,6 @@ import kotlinx.coroutines.launch @Composable fun LobstersPostsScreen( urlLauncher: UrlLauncher, - htmlConverter: HTMLConverter, windowSizeClass: WindowSizeClass, setWebUri: (String?) -> Unit, modifier: Modifier = Modifier, @@ -244,7 +242,6 @@ fun LobstersPostsScreen( CommentsPage( postId = postId, postActions = postActions, - htmlConverter = htmlConverter, getSeenComments = viewModel::getSeenComments, markSeenComments = viewModel::markSeenComments, contentPadding = contentPadding, diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt index 4b24b250..306e8299 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt @@ -25,14 +25,12 @@ import dev.msfjarvis.claw.android.ui.navigation.Search import dev.msfjarvis.claw.android.ui.navigation.User import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.comments.CommentsPage -import dev.msfjarvis.claw.common.comments.HTMLConverter import dev.msfjarvis.claw.common.urllauncher.UrlLauncher import dev.msfjarvis.claw.common.user.UserProfile @Composable fun SearchScreen( urlLauncher: UrlLauncher, - htmlConverter: HTMLConverter, setWebUri: (String?) -> Unit, modifier: Modifier = Modifier, viewModel: ClawViewModel = injectedViewModel(), @@ -64,11 +62,10 @@ fun SearchScreen( CommentsPage( postId = postId, postActions = postActions, - htmlConverter = htmlConverter, getSeenComments = viewModel::getSeenComments, markSeenComments = viewModel::markSeenComments, - openUserProfile = { navController.navigate(User(it)) }, contentPadding = contentPadding, + openUserProfile = { navController.navigate(User(it)) }, ) } composable { backStackEntry -> diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TabletScreen.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TabletScreen.kt index eaef96e8..9714f37a 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TabletScreen.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TabletScreen.kt @@ -66,7 +66,6 @@ import dev.msfjarvis.claw.android.ui.navigation.Saved import dev.msfjarvis.claw.android.ui.navigation.User import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.comments.CommentsPage -import dev.msfjarvis.claw.common.comments.HTMLConverter import dev.msfjarvis.claw.common.urllauncher.UrlLauncher import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf @@ -82,7 +81,6 @@ private fun ThreePaneScaffoldNavigator<*>.isDetailExpanded() = @Composable fun TabletScreen( urlLauncher: UrlLauncher, - htmlConverter: HTMLConverter, modifier: Modifier = Modifier, viewModel: ClawViewModel = injectedViewModel(), ) { @@ -219,12 +217,11 @@ fun TabletScreen( CommentsPage( postId = contentKey.postId, postActions = postActions, - htmlConverter = htmlConverter, getSeenComments = viewModel::getSeenComments, markSeenComments = viewModel::markSeenComments, - openUserProfile = { navController.navigate(User(it)) }, contentPadding = PaddingValues(), modifier = Modifier.fillMaxSize(), + openUserProfile = { navController.navigate(User(it)) }, ) } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TabletScreen2.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TabletScreen2.kt index e4fcd7a8..90c9e90f 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TabletScreen2.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TabletScreen2.kt @@ -40,7 +40,6 @@ import dev.msfjarvis.claw.android.ui.navigation.Settings import dev.msfjarvis.claw.android.ui.navigation.User import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.comments.CommentsPage -import dev.msfjarvis.claw.common.comments.HTMLConverter import dev.msfjarvis.claw.common.urllauncher.UrlLauncher import dev.msfjarvis.claw.common.user.UserProfile import kotlinx.collections.immutable.persistentMapOf @@ -48,7 +47,6 @@ import kotlinx.collections.immutable.persistentMapOf @Composable fun TabletScreen2( urlLauncher: UrlLauncher, - htmlConverter: HTMLConverter, setWebUri: (String?) -> Unit, modifier: Modifier = Modifier, viewModel: ClawViewModel = injectedViewModel(), @@ -140,7 +138,6 @@ fun TabletScreen2( CommentsPage( postId = postId, postActions = postActions, - htmlConverter = htmlConverter, getSeenComments = viewModel::getSeenComments, markSeenComments = viewModel::markSeenComments, contentPadding = contentPadding, diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/util/HTMLConverterImpl.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/util/HTMLConverterImpl.kt deleted file mode 100644 index 6544851c..00000000 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/util/HTMLConverterImpl.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright © Harsh Shandilya. - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE file or at - * https://opensource.org/licenses/MIT. - */ -package dev.msfjarvis.claw.android.ui.util - -import androidx.compose.runtime.Stable -import dev.msfjarvis.claw.common.comments.HTMLConverter -import io.github.furstenheim.CopyDown -import javax.inject.Inject - -@Stable -class HTMLConverterImpl @Inject constructor() : HTMLConverter { - private val copydown = CopyDown() - - override fun convertHTMLToMarkdown(html: String): String { - return copydown.convert(html) - } -} diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 42c5a17d..6325541e 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(libs.compose.richtext.markdown) implementation(libs.compose.richtext.material3) implementation(libs.compose.richtext.ui) + implementation(libs.htmlconverter) implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinResult) diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt index 2ac6b0b8..e68a04b5 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt @@ -46,7 +46,6 @@ import kotlinx.collections.immutable.toImmutableList internal fun CommentsHeader( post: UIPost, postActions: PostActions, - htmlConverter: HTMLConverter, openUserProfile: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -77,7 +76,7 @@ internal fun CommentsHeader( } if (post.description.isNotBlank()) { - ThemedRichText(htmlConverter.convertHTMLToMarkdown(post.description)) + ThemedRichText(post.description) Spacer(Modifier.height(4.dp)) } Submitter( diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPage.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPage.kt index ac4bb774..c911eab6 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPage.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPage.kt @@ -32,7 +32,6 @@ import dev.msfjarvis.claw.model.UIPost fun CommentsPage( postId: String, postActions: PostActions, - htmlConverter: HTMLConverter, getSeenComments: suspend (String) -> PostComments?, markSeenComments: (String, List) -> Unit, contentPadding: PaddingValues, @@ -57,7 +56,6 @@ fun CommentsPage( CommentsPageInternal( details = (postDetails as Success).data, postActions = postActions, - htmlConverter = htmlConverter, commentState = commentState, markSeenComments = markSeenComments, openUserProfile = openUserProfile, diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt index 1d6e41c4..6af3e3cc 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt @@ -63,7 +63,6 @@ private const val AnimationDuration = 100 internal fun CommentsPageInternal( details: UIPost, postActions: PostActions, - htmlConverter: HTMLConverter, commentState: PostComments?, markSeenComments: (String, List) -> Unit, openUserProfile: (String) -> Unit, @@ -103,12 +102,7 @@ internal fun CommentsPageInternal( Surface(color = MaterialTheme.colorScheme.surfaceVariant) { LazyColumn(modifier = modifier, contentPadding = contentPadding, state = commentListState) { item { - CommentsHeader( - post = details, - postActions = postActions, - htmlConverter = htmlConverter, - openUserProfile = openUserProfile, - ) + CommentsHeader(post = details, postActions = postActions, openUserProfile = openUserProfile) } if (commentNodes.isNotEmpty()) { @@ -121,7 +115,7 @@ internal fun CommentsPageInternal( } items(items = commentNodes, key = { node -> node.comment.shortId }) { node -> - Node(node, htmlConverter, openUserProfile, onToggleExpandedState) + Node(node, openUserProfile, onToggleExpandedState) } item(key = "bottom_spacer") { @@ -149,7 +143,6 @@ internal fun CommentsPageInternal( @Composable private fun Node( node: CommentNode, - htmlConverter: HTMLConverter, openUserProfile: (String) -> Unit, onToggleExpandedState: (String, Boolean) -> Unit, modifier: Modifier = Modifier, @@ -158,7 +151,6 @@ private fun Node( NodeBox( node = node, isExpanded = node.isExpanded, - htmlConverter = htmlConverter, openUserProfile, modifier = modifier.clickable( @@ -172,9 +164,7 @@ private fun Node( exit = fadeOut(tween(AnimationDuration)) + shrinkVertically(tween(AnimationDuration)), ) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { - node.children.forEach { model -> - Node(model, htmlConverter, openUserProfile, onToggleExpandedState) - } + node.children.forEach { model -> Node(model, openUserProfile, onToggleExpandedState) } } } } @@ -184,11 +174,10 @@ private fun Node( private fun NodeBox( node: CommentNode, isExpanded: Boolean, - htmlConverter: HTMLConverter, openUserProfile: (String) -> Unit, modifier: Modifier = Modifier, ) { - CommentEntry(isExpanded, node, htmlConverter, openUserProfile, modifier) + CommentEntry(isExpanded, node, openUserProfile, modifier) HorizontalDivider() } @@ -198,7 +187,6 @@ private val CommentEntryPadding = 16f.dp private fun CommentEntry( isExpanded: Boolean, commentNode: CommentNode, - htmlConverter: HTMLConverter, openUserProfile: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -234,10 +222,7 @@ private fun CommentEntry( modifier = Modifier.clickable { openUserProfile(comment.user) }, ) if (isExpanded) { - ThemedRichText( - text = htmlConverter.convertHTMLToMarkdown(comment.comment), - modifier = Modifier.padding(top = 8.dp), - ) + ThemedRichText(text = comment.comment, modifier = Modifier.padding(top = 8.dp)) } } } diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/HTMLConverter.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/HTMLConverter.kt deleted file mode 100644 index 39e25018..00000000 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/HTMLConverter.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright © Harsh Shandilya. - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE file or at - * https://opensource.org/licenses/MIT. - */ -package dev.msfjarvis.claw.common.comments - -import androidx.compose.runtime.Stable - -/** Defines a contract to convert strings of HTML to Markdown. */ -@Stable -fun interface HTMLConverter { - fun convertHTMLToMarkdown(html: String): String -} diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTML.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTML.kt new file mode 100644 index 00000000..87366dd8 --- /dev/null +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTML.kt @@ -0,0 +1,231 @@ +/* + * Copyright © Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import be.digitalia.compose.htmlconverter.HtmlStyle +import be.digitalia.compose.htmlconverter.htmlToAnnotatedString +import dev.msfjarvis.claw.common.theme.LobstersTheme +import dev.msfjarvis.claw.common.ui.preview.ThemePreviews + +private sealed class Segment { + data class Text(val content: String) : Segment() + + data class Blockquote(val content: String) : Segment() + + data class Code(val content: String) : Segment() +} + +private fun parseSegments(text: String): List { + val segments = mutableListOf() + var i = 0 + val blockquoteStart = "
" + val blockquoteEnd = "
" + val preStart = "
"
+  val preEnd = "
" + val codeStart = " segments.add(Segment.Blockquote(content)) + isCode -> segments.add(Segment.Code(content)) + else -> segments.add(Segment.Text(content)) + } + } + } + + while (i < text.length) { + val nextBlockquote = text.indexOf(blockquoteStart, i, ignoreCase = true) + val nextPre = text.indexOf(preStart, i, ignoreCase = true) + val (nextTag, isBlockquote) = + when { + nextBlockquote != -1 && (nextPre == -1 || nextBlockquote < nextPre) -> + nextBlockquote to true + nextPre != -1 -> nextPre to false + else -> -1 to false + } + if (nextTag == -1) { + addSegment(text.substring(i)) + break + } + if (nextTag > i) { + addSegment(text.substring(i, nextTag)) + } + if (isBlockquote) { + val endIdx = text.indexOf(blockquoteEnd, nextTag + blockquoteStart.length, ignoreCase = true) + if (endIdx == -1) { + addSegment(text.substring(nextTag)) + break + } + addSegment(text.substring(nextTag + blockquoteStart.length, endIdx), isBlockquote = true) + i = endIdx + blockquoteEnd.length + } else { + val endIdx = text.indexOf(preEnd, nextTag + preStart.length, ignoreCase = true) + if (endIdx == -1) { + addSegment(text.substring(nextTag)) + break + } + var codeContent = text.substring(nextTag + preStart.length, endIdx) + val codeTagStart = codeContent.indexOf(codeStart, ignoreCase = true) + val codeTagEnd = codeContent.indexOf(codeEnd, ignoreCase = true) + if (codeTagStart != -1 && codeTagEnd != -1 && codeTagStart < codeTagEnd) { + val codeOpenEnd = codeContent.indexOf('>', codeTagStart) + if (codeOpenEnd != -1 && codeOpenEnd < codeTagEnd) { + codeContent = codeContent.substring(codeOpenEnd + 1, codeTagEnd) + } + } + addSegment(codeContent, isCode = true) + i = endIdx + preEnd.length + } + } + + return segments +} + +@Composable +internal fun ThemedRichText(text: String, modifier: Modifier = Modifier) { + val linkSpanStyle = + SpanStyle( + background = MaterialTheme.colorScheme.surfaceVariant, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + textDecoration = TextDecoration.Underline, + ) + + val segments = parseSegments(text) + + Column(modifier = modifier) { + for (segment in segments) { + when (segment) { + is Segment.Blockquote -> { + Row( + modifier = Modifier.padding(vertical = 4.dp).fillMaxWidth().height(IntrinsicSize.Min) + ) { + Box( + modifier = + Modifier.width(4.dp) + .fillMaxHeight() + .clip(RoundedCornerShape(2.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = + remember(segment.content) { + htmlToAnnotatedString( + html = segment.content, + compactMode = false, + style = HtmlStyle(textLinkStyles = TextLinkStyles(linkSpanStyle)), + ) + }, + style = MaterialTheme.typography.bodyLarge, + color = contentColorFor(MaterialTheme.colorScheme.background), + ) + } + } + } + is Segment.Code -> { + Box( + modifier = + Modifier.fillMaxWidth() + .padding(vertical = 4.dp) + .background( + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f), + shape = RoundedCornerShape(6.dp), + ) + .padding(12.dp) + ) { + Text( + text = segment.content, + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + is Segment.Text -> { + Text( + text = + remember(segment.content) { + htmlToAnnotatedString( + html = segment.content, + compactMode = false, + style = HtmlStyle(textLinkStyles = TextLinkStyles(linkSpanStyle)), + ) + }, + style = MaterialTheme.typography.bodyLarge, + color = contentColorFor(MaterialTheme.colorScheme.background), + ) + } + } + } + } +} + +@ThemePreviews +@Composable +internal fun ThemedRichTextPreview() { + val text = + """ +

Hello HTML Converter for Compose

+

This the first paragraph of the sample app running on Nothing!

+
    +
  • Bold
  • +
  • Italic
  • +
  • Underline
  • +
  • Strikethrough
  • +
  • Code
  • +
  • Hyperlink with custom styling
  • +
  • Bigger and smaller text
  • +
  • Supertext and subtext
  • +
  • A nested ordered list: +
      +
    1. Item 1
    2. +
    3. Item 2
    4. +
    +
  • +
+
+
Term
+
Description.
+
+ A few HTML entities: » © « ✓ +
A blockquote, indented relatively to the main text.
+
Preformatted text, preserving
+    line breaks...    and spaces.
+

Inline code example. You reached the end of the document.
Thank you for reading!

+ """ + .trimIndent() + + LobstersTheme { ThemedRichText(text = text) } +} diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/Markdown.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/Markdown.kt deleted file mode 100644 index 9655300c..00000000 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/Markdown.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright © Harsh Shandilya. - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE file or at - * https://opensource.org/licenses/MIT. - */ -package dev.msfjarvis.claw.common.ui - -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration -import com.halilibo.richtext.commonmark.Markdown -import com.halilibo.richtext.ui.RichTextStyle -import com.halilibo.richtext.ui.material3.RichText -import com.halilibo.richtext.ui.string.RichTextStringStyle -import dev.msfjarvis.claw.common.theme.LobstersTheme -import dev.msfjarvis.claw.common.ui.preview.ThemePreviews - -@Composable -internal fun ThemedRichText(text: String, modifier: Modifier = Modifier) { - val linkSpanStyle = - SpanStyle( - background = MaterialTheme.colorScheme.surfaceVariant, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Bold, - textDecoration = TextDecoration.Underline, - ) - val stringStyle = RichTextStringStyle(linkStyle = TextLinkStyles(linkSpanStyle)) - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodyLarge, - LocalContentColor provides MaterialTheme.colorScheme.onBackground, - ) { - RichText(modifier = modifier, style = RichTextStyle.Default.copy(stringStyle = stringStyle)) { - Markdown(text) - } - } -} - -@ThemePreviews -@Composable -internal fun ThemedRichTextPreview() { - val text = - """ - ### Heading - This is a paragraph body - - ``` - This is a code block - ``` - - This is an `inline code block` - - [This is a link](https://github.com/msfjarvis/compose-lobsters) - - ![Image](https://avatars.githubusercontent.com/u/13348378?v=4) - """ - .trimIndent() - - LobstersTheme { ThemedRichText(text = text) } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b51a738..921e76be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -77,7 +77,6 @@ coil3-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", versio compose-richtext-markdown = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } compose-richtext-material3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" } compose-richtext-ui = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "richtext" } -copydown = "io.github.furstenheim:copy_down:1.1" dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } eithernet = { module = "com.slack.eithernet:eithernet", version.ref = "eithernet" } @@ -85,6 +84,7 @@ eithernet-integration-retrofit = { module = "com.slack.eithernet:eithernet-integ eithernet-test-fixtures = { module = "com.slack.eithernet:eithernet-test-fixtures", version.ref = "eithernet" } haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } +htmlconverter = "be.digitalia.compose.htmlconverter:htmlconverter:1.0.4" javax-inject = "javax.inject:javax.inject:1" jsoup = "org.jsoup:jsoup:1.20.1" junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }