From edddbe113fa9b95e098a07bb689a9bcde77fadcf Mon Sep 17 00:00:00 2001 From: Diego Beraldin Date: Sun, 18 Feb 2024 10:04:03 +0100 Subject: [PATCH] refactor: new markdown rendering (#530) closes #305 --- CONTRIBUTING.md | 3 +- core/commonui/lemmyui/build.gradle.kts | 2 +- .../core/commonui/lemmyui/PostCardBody.kt | 16 +- .../core/commonui/lemmyui/PostCardTitle.kt | 14 +- core/{md => markdown}/build.gradle.kts | 24 +- .../core/markdown/CustomMarkdownImage.kt | 77 +++++++ .../core/markdown/CustomMarkdownSpoiler.kt | 114 +++++++++ .../core/markdown/CustomMarkdownWrapper.kt | 109 +++++++++ .../raccoonforlemmy/core/markdown/Regexes.kt | 17 ++ .../raccoonforlemmy/core/markdown/Utils.kt | 65 ++++++ .../core/markdown/compose/CustomMarkdown.kt | 214 ----------------- .../core/markdown/di/MarkwonModule.kt | 19 -- .../markdown/plugins/ClickableImagesPlugin.kt | 39 ---- .../markdown/plugins/MarkwonLemmyPlugin.kt | 75 ------ .../markdown/plugins/MarkwonSpoilerPlugin.kt | 179 -------------- .../provider/DefaultMarkwonProvider.kt | 98 -------- .../core/markdown/provider/MarkwonProvider.kt | 11 - .../core/markdown/compose/ComposeLocal.kt | 51 ---- .../core/markdown/compose/CustomMarkdown.kt | 41 ---- .../core/markdown/model/BulletHandler.kt | 6 - .../core/markdown/model/MarkdownColors.kt | 37 --- .../core/markdown/model/MarkdownPadding.kt | 30 --- .../core/markdown/model/MarkdownTypography.kt | 63 ----- .../markdown/model/ReferenceLinkHandler.kt | 26 --- .../core/markdown/compose/CustomMarkdown.kt | 218 ------------------ .../compose/elements/MarkdownBlockQuote.kt | 46 ---- .../markdown/compose/elements/MarkdownCode.kt | 57 ----- .../compose/elements/MarkdownHeader.kt | 30 --- .../compose/elements/MarkdownImage.kt | 65 ------ .../markdown/compose/elements/MarkdownList.kt | 177 -------------- .../compose/elements/MarkdownParagraph.kt | 40 ---- .../markdown/compose/elements/MarkdownText.kt | 214 ----------------- .../core/markdown/utils/AnnotatedStringKtx.kt | 154 ------------- .../core/markdown/utils/Extensions.kt | 47 ---- docs/index.md | 11 +- docs/tech_manual/module_structure.md | 6 +- docs/tech_manual/tech_stack.md | 21 +- gradle/libs.versions.toml | 11 +- settings.gradle.kts | 2 +- shared/build.gradle.kts | 1 - .../raccoonforlemmy/di/DiHelper.kt | 2 - 41 files changed, 431 insertions(+), 2001 deletions(-) rename core/{md => markdown}/build.gradle.kts (65%) create mode 100644 core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownImage.kt create mode 100644 core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownSpoiler.kt create mode 100644 core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownWrapper.kt create mode 100644 core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/Regexes.kt create mode 100644 core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/Utils.kt delete mode 100755 core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt delete mode 100644 core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/di/MarkwonModule.kt delete mode 100644 core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/ClickableImagesPlugin.kt delete mode 100644 core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/MarkwonLemmyPlugin.kt delete mode 100644 core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/MarkwonSpoilerPlugin.kt delete mode 100644 core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/DefaultMarkwonProvider.kt delete mode 100644 core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/MarkwonProvider.kt delete mode 100755 core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/ComposeLocal.kt delete mode 100755 core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt delete mode 100755 core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/BulletHandler.kt delete mode 100755 core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownColors.kt delete mode 100755 core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownPadding.kt delete mode 100755 core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownTypography.kt delete mode 100755 core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/ReferenceLinkHandler.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownBlockQuote.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownCode.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownHeader.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownImage.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownList.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownParagraph.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownText.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/utils/AnnotatedStringKtx.kt delete mode 100755 core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/utils/Extensions.kt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38ddeec68..24decea72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -268,7 +268,8 @@ The modules can be grouped into five groups: - **core modules** are the foundational layer of the application. They are included by unit modules, feature modules, domain modules and top-level modules. They should not include anything except in some rare occasions, other core modules (but never cyclically!). A notable example of this is the - `:core:md module` (Markdown rendering) that includes `:core:commonui:components` because Markdown + `:core:markdown` module (Markdown rendering) that includes `:core:commonui:components` because + Markdown requires some custom UI components to be rendered. For more detailed information about the contents of each group and the purpose of each module, diff --git a/core/commonui/lemmyui/build.gradle.kts b/core/commonui/lemmyui/build.gradle.kts index 5b1fea560..b34310289 100644 --- a/core/commonui/lemmyui/build.gradle.kts +++ b/core/commonui/lemmyui/build.gradle.kts @@ -43,7 +43,7 @@ kotlin { implementation(projects.core.appearance) implementation(projects.core.commonui.components) - implementation(projects.core.md) + implementation(projects.core.markdown) implementation(projects.core.l10n) implementation(projects.core.navigation) implementation(projects.core.persistence) diff --git a/core/commonui/lemmyui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/lemmyui/PostCardBody.kt b/core/commonui/lemmyui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/lemmyui/PostCardBody.kt index 67de912ae..e9a0960c3 100644 --- a/core/commonui/lemmyui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/lemmyui/PostCardBody.kt +++ b/core/commonui/lemmyui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/lemmyui/PostCardBody.kt @@ -1,5 +1,6 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -8,14 +9,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.toTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.CustomMarkdown -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.markdownTypography +import com.github.diegoberaldin.raccoonforlemmy.core.markdown.CustomMarkdownWrapper import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel +import com.mikepenz.markdown.model.markdownColor +import com.mikepenz.markdown.model.markdownTypography @Composable fun PostCardBody( @@ -40,11 +42,10 @@ fun PostCardBody( val typography = fontFamily.toTypography() if (text.isNotEmpty()) { - CustomMarkdown( + CustomMarkdownWrapper( modifier = modifier, content = text, maxLines = maxLines, - inlineImages = false, autoLoadImages = autoLoadImages, typography = markdownTypography( h1 = typography.titleLarge, @@ -56,6 +57,13 @@ fun PostCardBody( text = typography.bodyMedium, paragraph = typography.bodyMedium, ), + colors = markdownColor( + text = MaterialTheme.colorScheme.onBackground, + linkText = MaterialTheme.colorScheme.primary, + codeText = MaterialTheme.colorScheme.onBackground, + codeBackground = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.1f), + dividerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), + ), onOpenUrl = rememberCallbackArgs { url -> navigationCoordinator.handleUrl( url = url, diff --git a/core/commonui/lemmyui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/lemmyui/PostCardTitle.kt b/core/commonui/lemmyui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/lemmyui/PostCardTitle.kt index 1ee61c5fe..34cf7c8eb 100644 --- a/core/commonui/lemmyui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/lemmyui/PostCardTitle.kt +++ b/core/commonui/lemmyui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/lemmyui/PostCardTitle.kt @@ -1,5 +1,6 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -9,14 +10,15 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.toTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.CustomMarkdown -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.markdownTypography +import com.github.diegoberaldin.raccoonforlemmy.core.markdown.CustomMarkdownWrapper import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel +import com.mikepenz.markdown.model.markdownColor +import com.mikepenz.markdown.model.markdownTypography @Composable fun PostCardTitle( @@ -39,7 +41,7 @@ fun PostCardTitle( val fontFamily by themeRepository.contentFontFamily.collectAsState() val typography = fontFamily.toTypography() - CustomMarkdown( + CustomMarkdownWrapper( modifier = modifier, content = text, autoLoadImages = autoLoadImages, @@ -53,6 +55,12 @@ fun PostCardTitle( text = typography.bodyMedium.copy(fontWeight = FontWeight.Medium), paragraph = typography.bodyMedium.copy(fontWeight = FontWeight.Medium), ), + colors = markdownColor( + text = MaterialTheme.colorScheme.onBackground, + codeText = MaterialTheme.colorScheme.onBackground, + codeBackground = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.1f), + dividerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), + ), onOpenUrl = rememberCallbackArgs { url -> navigationCoordinator.handleUrl( url = url, diff --git a/core/md/build.gradle.kts b/core/markdown/build.gradle.kts similarity index 65% rename from core/md/build.gradle.kts rename to core/markdown/build.gradle.kts index 01c65505c..e08e8a7a3 100644 --- a/core/md/build.gradle.kts +++ b/core/markdown/build.gradle.kts @@ -21,41 +21,25 @@ kotlin { iosSimulatorArm64() ).forEach { it.binaries.framework { - baseName = "md" + baseName = "markdown" } } sourceSets { - val androidMain by getting { - dependencies { - implementation(libs.markwon.core) - implementation(libs.markwon.strikethrough) - implementation(libs.markwon.tables) - implementation(libs.markwon.html) - implementation(libs.markwon.linkify) - implementation(libs.markwon.image.coil) - implementation(libs.coil) - implementation(libs.coil.gif) - implementation(libs.android.gif.drawable) - } - } val commonMain by getting { dependencies { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) - implementation(libs.koin.core) + implementation(libs.markdown) + api(libs.multiplatform.markdown.renderer) + implementation(projects.core.l10n) implementation(projects.core.commonui.components) implementation(projects.core.utils) } } - val iosMain by getting { - dependencies { - implementation(libs.markdown) - } - } val commonTest by getting { dependencies { implementation(kotlin("test")) diff --git a/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownImage.kt b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownImage.kt new file mode 100644 index 000000000..4e01d7e16 --- /dev/null +++ b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownImage.kt @@ -0,0 +1,77 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.markdown + +import androidx.compose.animation.core.InfiniteRepeatableSpec +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomImage +import com.github.diegoberaldin.raccoonforlemmy.core.l10n.LocalXmlStrings +import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick +import com.mikepenz.markdown.compose.LocalMarkdownTypography +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.getTextInNode + +@Composable +internal fun CustomMarkdownImage( + node: ASTNode, + content: String, + onOpenImage: ((String) -> Unit)?, + autoLoadImages: Boolean, +) { + val link = node.findChildOfTypeRecursive(MarkdownElementTypes.LINK_DESTINATION) + ?.getTextInNode(content) + ?.toString().orEmpty() + if (link.isNotEmpty()) { + CustomImage( + modifier = Modifier + .fillMaxWidth() + .onClick( + onClick = { + onOpenImage?.invoke(link) + }, + ), + url = link, + autoload = autoLoadImages, + quality = FilterQuality.Low, + contentScale = ContentScale.FillWidth, + onFailure = { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = LocalXmlStrings.current.messageImageLoadingError, + style = LocalMarkdownTypography.current.text, + ) + }, + onLoading = { progress -> + val prog = if (progress != null) { + progress + } else { + val transition = rememberInfiniteTransition() + val res by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = InfiniteRepeatableSpec( + animation = tween(1000) + ), + ) + res + } + CircularProgressIndicator( + progress = prog, + color = MaterialTheme.colorScheme.primary, + ) + }, + ) + } +} diff --git a/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownSpoiler.kt b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownSpoiler.kt new file mode 100644 index 000000000..bd2fddb53 --- /dev/null +++ b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownSpoiler.kt @@ -0,0 +1,114 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.markdown + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick +import com.mikepenz.markdown.compose.elements.MarkdownText + +@Composable +internal fun CustomMarkdownSpoiler( + content: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + var lastIndex = 0 + val matches = SpoilerRegex.spoilerOpenRegex.findAll(content) + for (match in matches) { + val openingStart = match.range.first + val openingEnd = match.range.last + if (openingStart > lastIndex) { + val subcontent = content.substring( + startIndex = lastIndex, + endIndex = openingStart, + ) + MarkdownText(content = subcontent) + } + val spoilerTitle = match.groups["title"]?.value.orEmpty() + val closeMatch = SpoilerRegex.spoilerCloseRegex.find( + input = content, + startIndex = openingEnd, + ) + val spoilerContent = closeMatch?.let { + content.substring( + startIndex = openingEnd + 1, + endIndex = it.range.last - 3, + ) + } ?: content.substring( + startIndex = openingEnd + 1, + ) + + InnerSpoilerElement( + title = spoilerTitle, + content = spoilerContent, + ) + + lastIndex = closeMatch?.range?.last ?: content.lastIndex + } + if (lastIndex < content.lastIndex) { + val subcontent = content.substring( + startIndex = lastIndex, + ) + MarkdownText(content = subcontent) + } + } +} + +@Composable +private fun InnerSpoilerElement( + title: String, + content: String, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + + Column( + modifier = modifier, + ) { + if (!expanded) { + MarkdownText( + modifier = Modifier + .fillMaxWidth() + .onClick(onClick = { expanded = !expanded }), + content = buildAnnotatedString { + withStyle(SpanStyle(fontSize = 20.sp)) { + append("▶︎ ") + } + append(title) + }, + ) + } else { + MarkdownText( + modifier = Modifier + .fillMaxWidth() + .onClick(onClick = { expanded = !expanded }), + content = buildAnnotatedString { + withStyle(SpanStyle(fontSize = 20.sp)) { + append("▼︎ ") + } + append(title) + }, + ) + MarkdownText( + modifier = Modifier.padding( + start = 18.dp, + bottom = 10.dp, + ), + content = content, + ) + } + } +} \ No newline at end of file diff --git a/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownWrapper.kt b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownWrapper.kt new file mode 100644 index 000000000..e49a25b05 --- /dev/null +++ b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/CustomMarkdownWrapper.kt @@ -0,0 +1,109 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.markdown + +import androidx.compose.foundation.layout.heightIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler +import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick +import com.mikepenz.markdown.compose.LocalMarkdownTypography +import com.mikepenz.markdown.compose.Markdown +import com.mikepenz.markdown.compose.components.markdownComponents +import com.mikepenz.markdown.compose.elements.MarkdownParagraph +import com.mikepenz.markdown.model.MarkdownColors +import com.mikepenz.markdown.model.MarkdownPadding +import com.mikepenz.markdown.model.MarkdownTypography +import com.mikepenz.markdown.model.markdownColor +import com.mikepenz.markdown.model.markdownPadding +import com.mikepenz.markdown.model.markdownTypography + +private val String.containsSpoiler: Boolean + get() = SpoilerRegex.spoilerOpenRegex.containsMatchIn(this) + +@Composable +fun CustomMarkdownWrapper( + content: String, + modifier: Modifier, + colors: MarkdownColors = markdownColor(), + typography: MarkdownTypography = markdownTypography(), + padding: MarkdownPadding = markdownPadding(), + autoLoadImages: Boolean, + maxLines: Int? = null, + onOpenUrl: ((String) -> Unit)?, + onOpenImage: ((String) -> Unit)?, + onClick: (() -> Unit)?, + onDoubleClick: (() -> Unit)?, + onLongClick: (() -> Unit)?, +) { + val customUriHandler = remember { + object : UriHandler { + override fun openUri(uri: String) { + onOpenUrl?.invoke(uri) + } + } + } + val components = markdownComponents( + paragraph = { model -> + val substring = model.content.substring( + startIndex = model.node.startOffset, + endIndex = model.node.endOffset, + ) + when { + substring.containsSpoiler -> { + CustomMarkdownSpoiler(content = substring) + } + + else -> { + MarkdownParagraph( + modifier = if (maxLines != null) { + val maxHeightSp = + LocalMarkdownTypography.current.paragraph.lineHeight * maxLines + val maxHeightDp = with(LocalDensity.current) { + maxHeightSp.toDp() + } + Modifier.heightIn(max = maxHeightDp) + } else { + Modifier + }, + content = model.content, + node = model.node + ) + } + } + }, + image = { model -> + CustomMarkdownImage( + node = model.node, + content = content, + onOpenImage = onOpenImage, + autoLoadImages = autoLoadImages, + ) + }, + ) + + CompositionLocalProvider( + LocalUriHandler provides customUriHandler + ) { + Markdown( + modifier = modifier.onClick( + onClick = { + onClick?.invoke() + }, + onLongClick = { + onLongClick?.invoke() + }, + onDoubleClick = { + onDoubleClick?.invoke() + }, + ), + content = content.sanitize(), + colors = colors, + typography = typography, + padding = padding, + components = components, + ) + } +} diff --git a/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/Regexes.kt b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/Regexes.kt new file mode 100644 index 000000000..d71de288f --- /dev/null +++ b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/Regexes.kt @@ -0,0 +1,17 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.markdown + +internal object SpoilerRegex { + val spoilerOpenRegex = Regex("(:::\\s+spoiler\\s+)(?.*)") + val spoilerCloseRegex = Regex(":::") +} + +internal object LemmyLinkRegex { + private const val DETAIL_FRAGMENT: String = """[a-zA-Z0-9_]{3,}""" + + private const val INSTANCE_FRAGMENT: String = + """([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+[a-zA-Z]{2,}""" + + val lemmyHandle: Regex = + Regex("(?<!\\S)!(?<detail>$DETAIL_FRAGMENT)(?:@(?<instance>$INSTANCE_FRAGMENT))?\\b") +} + diff --git a/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/Utils.kt b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/Utils.kt new file mode 100644 index 000000000..84e1ad554 --- /dev/null +++ b/core/markdown/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/Utils.kt @@ -0,0 +1,65 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.markdown + +import org.intellij.markdown.IElementType +import org.intellij.markdown.ast.ASTNode + +internal fun ASTNode.findChildOfTypeRecursive(type: IElementType): ASTNode? { + children.forEach { + if (it.type == type) { + return it + } else { + val found = it.findChildOfTypeRecursive(type) + if (found != null) { + return found + } + } + } + return null +} + +internal fun String.sanitize(): String = this + .replace("&", "&") + .replace(" ", " ") + .fixBlankLinesForSpoilers() + .expandLemmyHandles() + +private fun String.fixBlankLinesForSpoilers(): String = run { + val finalLines = mutableListOf<String>() + var finalLinesSizeAtLastSpoiler = 0 + lines().forEach { line -> + val isSpoilerOnTopOfStack = finalLinesSizeAtLastSpoiler == finalLines.size + if (line.contains(SpoilerRegex.spoilerOpenRegex)) { + if (finalLines.lastOrNull()?.isEmpty() == false) { + finalLines += "" + } + finalLines += line + finalLinesSizeAtLastSpoiler = finalLines.size + } else if (line.isNotBlank()) { + finalLines += line + } else if (!isSpoilerOnTopOfStack) { + finalLines += "" + } + } + finalLines.joinToString("\n") +} + +private fun String.expandLemmyHandles(): String = let { content -> + buildString { + val matches = LemmyLinkRegex.lemmyHandle.findAll(content) + var lastIndex = 0 + for (match in matches) { + val start = match.range.first + val end = match.range.last + if (start > lastIndex) { + append(content.substring(startIndex = lastIndex, endIndex = start)) + } + val detail = match.groups["detail"]?.value.orEmpty() + val instance = match.groups["instance"]?.value.orEmpty() + append("[$detail@$instance](!$detail@$instance)") + lastIndex = end + 1 + } + if (lastIndex < content.lastIndex) { + append(content.substring(startIndex = lastIndex)) + } + } +} diff --git a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt b/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt deleted file mode 100755 index 03046087e..000000000 --- a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose - -import android.content.Context -import android.graphics.Typeface -import android.util.TypedValue -import android.view.GestureDetector -import android.view.MotionEvent -import android.view.View -import android.widget.TextView -import androidx.annotation.IdRes -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFontFamilyResolver -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontSynthesis -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.viewinterop.AndroidView -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownColors -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownPadding -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.ReferenceLinkHandlerImpl -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.di.getMarkwonProvider -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.provider.MarkwonProvider -import com.github.diegoberaldin.raccoonforlemmy.core.utils.datetime.epochMillis -import io.noties.markwon.image.AsyncDrawableSpan -import kotlinx.coroutines.delay - -/* - * CREDITS: - * https://github.com/dessalines/jerboa/blob/main/app/src/main/java/com/jerboa/ui/components/common/MarkdownHelper.kt - */ -@Composable -actual fun CustomMarkdown( - content: String, - modifier: Modifier, - colors: MarkdownColors, - typography: MarkdownTypography, - padding: MarkdownPadding, - maxLines: Int?, - onOpenUrl: ((String) -> Unit)?, - inlineImages: Boolean, - autoLoadImages: Boolean, - onOpenImage: ((String) -> Unit)?, - onClick: (() -> Unit)?, - onDoubleClick: (() -> Unit)?, - onLongClick: (() -> Unit)?, -) { - CompositionLocalProvider( - LocalReferenceLinkHandler provides ReferenceLinkHandlerImpl(), - LocalMarkdownPadding provides padding, - LocalMarkdownColors provides colors, - LocalMarkdownTypography provides typography, - ) { - var imageRecompositionTrigger by remember { mutableStateOf(false) } - var hasImages by remember { mutableStateOf(false) } - val markwonProvider: MarkwonProvider = remember { getMarkwonProvider() } - markwonProvider.onOpenUrl = onOpenUrl - markwonProvider.onOpenImage = onOpenImage - - BoxWithConstraints( - modifier = modifier - ) { - val style = LocalMarkdownTypography.current.text - val fontScale = LocalDensity.current.fontScale * 1.3f - val canvasWidthMaybe = with(LocalDensity.current) { maxWidth.toPx() }.toInt() - val textSizeMaybe = with(LocalDensity.current) { (style.fontSize * fontScale).toPx() } - val defaultColor = LocalMarkdownColors.current.text - val resolver: FontFamily.Resolver = LocalFontFamilyResolver.current - val typeface: Typeface = remember(resolver, style) { - resolver.resolve( - fontFamily = style.fontFamily, - fontWeight = style.fontWeight ?: FontWeight.Normal, - fontStyle = style.fontStyle ?: FontStyle.Normal, - fontSynthesis = style.fontSynthesis ?: FontSynthesis.All, - ) - }.value as Typeface - - key(imageRecompositionTrigger) { - AndroidView( - factory = { ctx -> - createTextView( - context = ctx, - textColor = defaultColor, - style = style, - typeface = typeface, - maxLines = maxLines, - fontSize = style.fontSize * fontScale, - ).apply { - val gestureDetector = - GestureDetector( - ctx, - object : GestureDetector.SimpleOnGestureListener() { - - private var lastClickTime = 0L - - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - val currentTime = epochMillis() - if ((currentTime - lastClickTime) < 300) return false - lastClickTime = currentTime - if (!markwonProvider.blockClickPropagation.value) { - cancelPendingInputEvents() - onClick?.invoke() - } - return true - } - - override fun onDoubleTap(e: MotionEvent): Boolean { - val currentTime = epochMillis() - if ((currentTime - lastClickTime) < 300) return false - lastClickTime = currentTime - if (!markwonProvider.blockClickPropagation.value) { - cancelPendingInputEvents() - onDoubleClick?.invoke() - } - return true - } - - override fun onLongPress(e: MotionEvent) { - if (!markwonProvider.blockClickPropagation.value) { - cancelPendingInputEvents() - onLongClick?.invoke() - } - } - - override fun onDown(e: MotionEvent): Boolean { - return true - } - } - ) - setOnTouchListener { v, evt -> - if (evt.action == MotionEvent.ACTION_UP) { - v.performClick() - } - gestureDetector.onTouchEvent(evt) - } - } - }, - update = { textView -> - val md = markwonProvider.markwon.toMarkdown(content) - val imageSpans = md.getSpans(0, md.length, AsyncDrawableSpan::class.java) - for (img in imageSpans) { - img.drawable.initWithKnownDimensions(canvasWidthMaybe, textSizeMaybe) - } - markwonProvider.markwon.setParsedMarkdown(textView, md) - if (imageSpans.isNotEmpty()) { - hasImages = true - } - }, - ) - } - - LaunchedEffect(hasImages) { - if (hasImages && !imageRecompositionTrigger) { - delay(500) - imageRecompositionTrigger = true - } - } - } - } -} - -private fun createTextView( - context: Context, - textColor: Color, - maxLines: Int? = null, - fontSize: TextUnit = TextUnit.Unspecified, - textAlign: TextAlign? = null, - typeface: Typeface? = null, - style: TextStyle, - @IdRes viewId: Int? = null, -): TextView { - val mergedStyle = style.merge( - TextStyle( - color = textColor, - fontSize = if (fontSize != TextUnit.Unspecified) fontSize else style.fontSize, - textAlign = textAlign ?: TextAlign.Start, - ), - ) - return TextView(context).apply { - setTextColor(textColor.toArgb()) - setTextSize(TypedValue.COMPLEX_UNIT_SP, mergedStyle.fontSize.value) - width = maxWidth - if (maxLines != null) { - this.maxLines = maxLines - } - - viewId?.let { id = viewId } - textAlign?.let { align -> - textAlignment = when (align) { - TextAlign.Left, TextAlign.Start -> View.TEXT_ALIGNMENT_TEXT_START - TextAlign.Right, TextAlign.End -> View.TEXT_ALIGNMENT_TEXT_END - TextAlign.Center -> View.TEXT_ALIGNMENT_CENTER - else -> View.TEXT_ALIGNMENT_TEXT_START - } - } - - this.typeface = typeface - } -} diff --git a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/di/MarkwonModule.kt b/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/di/MarkwonModule.kt deleted file mode 100644 index 99056018f..000000000 --- a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/di/MarkwonModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.di - -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.provider.DefaultMarkwonProvider -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.provider.MarkwonProvider -import org.koin.dsl.module -import org.koin.java.KoinJavaComponent.inject - -val markwonModule = module { - single<MarkwonProvider> { - DefaultMarkwonProvider( - context = get(), - ) - } -} - -internal fun getMarkwonProvider(): MarkwonProvider { - val res: MarkwonProvider by inject(MarkwonProvider::class.java) - return res -} diff --git a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/ClickableImagesPlugin.kt b/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/ClickableImagesPlugin.kt deleted file mode 100644 index 054ffbdd7..000000000 --- a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/ClickableImagesPlugin.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.plugins - -import android.content.Context -import io.noties.markwon.AbstractMarkwonPlugin -import io.noties.markwon.MarkwonSpansFactory -import io.noties.markwon.core.MarkwonTheme -import io.noties.markwon.core.spans.LinkSpan -import io.noties.markwon.image.ImageProps -import org.commonmark.node.Image - -class ClickableImagesPlugin private constructor( - private val context: Context, - private val onOpenImage: (String) -> Unit, -) : AbstractMarkwonPlugin() { - - companion object { - fun create(context: Context, onOpenImage: (String) -> Unit) = - ClickableImagesPlugin(context, onOpenImage) - } - - override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { - val origin = builder.getFactory(Image::class.java) - if (origin != null) { - builder.setFactory(Image::class.java) { configuration, props -> - val url = ImageProps.DESTINATION.require(props) - val linkSpan = LinkSpan( - MarkwonTheme.create(context), url, - ) { view, link -> - view.cancelPendingInputEvents() - onOpenImage(link) - } - arrayOf( - origin.getSpans(configuration, props), - linkSpan - ) - } - } - } -} \ No newline at end of file diff --git a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/MarkwonLemmyPlugin.kt b/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/MarkwonLemmyPlugin.kt deleted file mode 100644 index fb56d0d6a..000000000 --- a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/MarkwonLemmyPlugin.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.plugins - - -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.style.URLSpan -import android.text.util.Linkify -import io.noties.markwon.AbstractMarkwonPlugin -import io.noties.markwon.MarkwonPlugin -import io.noties.markwon.MarkwonVisitor -import io.noties.markwon.SpannableBuilder -import io.noties.markwon.core.CorePlugin -import io.noties.markwon.core.CoreProps -import org.commonmark.node.Link -import java.util.regex.Pattern - - -private const val COMMUNITY_FRAGMENT: String = """[a-zA-Z0-9_]{3,}""" - - -private const val INSTANCE_FRAGMENT: String = - """([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+[a-zA-Z]{2,}""" - -private const val USER_FRAGMENT: String = """[a-zA-Z0-9_]{3,}""" - -private val lemmyCommunityPattern: Pattern = - Pattern.compile("(?<!\\S)!($COMMUNITY_FRAGMENT)(?:@($INSTANCE_FRAGMENT))?\\b") - - -private val lemmyUserPattern: Pattern = - Pattern.compile("(?<!\\S)@($USER_FRAGMENT)(?:@($INSTANCE_FRAGMENT))?\\b") - -class MarkwonLemmyLinkPlugin private constructor() : AbstractMarkwonPlugin() { - - companion object { - fun create() = MarkwonLemmyLinkPlugin() - } - - override fun configure(registry: MarkwonPlugin.Registry) { - registry.require(CorePlugin::class.java) { it.addOnTextAddedListener(LemmyTextAddedListener()) } - } - - private class LemmyTextAddedListener : CorePlugin.OnTextAddedListener { - override fun onTextAdded(visitor: MarkwonVisitor, text: String, start: Int) { - val spanFactory = visitor.configuration().spansFactory().get( - Link::class.java, - ) ?: return - - val builder = SpannableStringBuilder(text) - if (addLinks(builder)) { - // target URL span specifically - val spans = builder.getSpans(0, builder.length, URLSpan::class.java) - if (!spans.isNullOrEmpty()) { - val renderProps = visitor.renderProps() - val spannableBuilder = visitor.builder() - for (span in spans) { - CoreProps.LINK_DESTINATION[renderProps] = span.url - SpannableBuilder.setSpans( - spannableBuilder, - spanFactory.getSpans(visitor.configuration(), renderProps), - start + builder.getSpanStart(span), - start + builder.getSpanEnd(span), - ) - } - } - } - } - - fun addLinks(text: Spannable): Boolean { - val communityLinkAdded = Linkify.addLinks(text, lemmyCommunityPattern, null) - val userLinkAdded = Linkify.addLinks(text, lemmyUserPattern, null) - return communityLinkAdded || userLinkAdded - } - } -} \ No newline at end of file diff --git a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/MarkwonSpoilerPlugin.kt b/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/MarkwonSpoilerPlugin.kt deleted file mode 100644 index af61dade9..000000000 --- a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/plugins/MarkwonSpoilerPlugin.kt +++ /dev/null @@ -1,179 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.plugins - -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.TextPaint -import android.text.style.ClickableSpan -import android.view.View -import android.widget.TextView -import io.noties.markwon.AbstractMarkwonPlugin -import io.noties.markwon.MarkwonPlugin -import io.noties.markwon.MarkwonVisitor -import io.noties.markwon.core.CorePlugin -import io.noties.markwon.image.AsyncDrawableScheduler - -data class SpoilerTitleSpan(val title: CharSequence) -class SpoilerCloseSpan - -/* - * Originally inspired by: - * https://github.com/dessalines/jerboa/blob/main/app/src/main/java/com/jerboa/util/markwon/MarkwonSpoilerPlugin.kt - */ -class MarkwonSpoilerPlugin private constructor( - private val enableInteraction: Boolean, - private val onInteraction: () -> Unit, -) : - AbstractMarkwonPlugin() { - - companion object { - fun create( - enableInteraction: Boolean = true, - onInteraction: () -> Unit = {}, - ): MarkwonSpoilerPlugin { - return MarkwonSpoilerPlugin( - enableInteraction = enableInteraction, - onInteraction = onInteraction, - ) - } - } - - override fun configure(registry: MarkwonPlugin.Registry) { - registry.require(CorePlugin::class.java) { - it.addOnTextAddedListener( - SpoilerTextAddedListener(), - ) - } - } - - override fun afterSetText(textView: TextView) { - runCatching { - val spanned = SpannableStringBuilder(textView.text) - val startSpans = - spanned.getSpans(0, spanned.length, SpoilerTitleSpan::class.java) - .sortedBy { spanned.getSpanStart(it) } - val closeSpans = - spanned.getSpans(0, spanned.length, SpoilerCloseSpan::class.java) - .sortedBy { spanned.getSpanStart(it) } - - startSpans - .zip(closeSpans) - .forEach { (startSpan, closeSpan) -> - val spoilerStart = spanned.getSpanStart(startSpan) - val spoilerEnd = spanned.getSpanEnd(closeSpan) - - val spoilerTitle = getSpoilerTitle(false, startSpan.title.toString()) - - val spoilerContent = spanned.subSequence( - spanned.getSpanEnd(startSpan) + 1, - spoilerEnd - 3, - ) - - // Remove spoiler content from span - spanned.replace(spoilerStart, spoilerEnd, spoilerTitle) - - // Set span block title - spanned.setSpan( - spoilerTitle, - spoilerStart, - spoilerStart + spoilerTitle.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, - ) - val wrapper = SpoilerClickableSpan( - enableInteraction = enableInteraction, - textView = textView, - spoilerTitle = startSpan.title.toString(), - spoilerContent = spoilerContent.toString(), - onInteraction = onInteraction, - ) - - // Set spoiler block type as ClickableSpan - spanned.setSpan( - wrapper, - spoilerStart, - spoilerStart + spoilerTitle.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, - ) - - textView.text = spanned - } - } - } -} - -private fun getSpoilerTitle(openParam: Boolean, title: String): String = if (openParam) { - "▼ ${title}\n" -} else { - // The space at the end is necessary for the lengths to be the same - // This reduces complexity as else it would need complex logic to determine the replacement length - "▶ ${title}\u200B" -} - -private class SpoilerClickableSpan( - private val enableInteraction: Boolean, - private val textView: TextView, - private val spoilerTitle: String, - private val spoilerContent: String, - private val onInteraction: () -> Unit, -) : ClickableSpan() { - - private var open = false - - override fun onClick(view: View) { - if (enableInteraction) { - onInteraction() - textView.cancelPendingInputEvents() - val spanned = SpannableStringBuilder(textView.text) - val title = getSpoilerTitle(open, spoilerTitle) - val start = spanned.indexOf(title).coerceAtLeast(0) - - open = !open - - spanned.replace( - start, - start + title.length, - getSpoilerTitle(open, spoilerTitle), - ) - if (open) { - spanned.insert(start + title.length, spoilerContent) - } else { - spanned.replace( - start + title.length, - start + title.length + spoilerContent.length, - "", - ) - } - - textView.text = spanned - AsyncDrawableScheduler.schedule(textView) - } - } - - override fun updateDrawState(ds: TextPaint) { - } -} - -private class SpoilerTextAddedListener : CorePlugin.OnTextAddedListener { - override fun onTextAdded(visitor: MarkwonVisitor, text: String, start: Int) { - val spoilerTitleRegex = Regex("(:::\\s+spoiler\\s+)(.*)") - // Find all spoiler "start" lines - val spoilerTitles = spoilerTitleRegex.findAll(text) - - for (match in spoilerTitles) { - val spoilerTitle = match.groups[2]!!.value - visitor.builder().setSpan( - SpoilerTitleSpan(spoilerTitle), - start, - start + match.groups[2]!!.range.last, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, - ) - } - - val spoilerCloseRegex = Regex("^(?!.*spoiler).*:::") - // Find all spoiler "end" lines - val spoilerCloses = spoilerCloseRegex.findAll(text) - for (match in spoilerCloses) { - visitor.builder() - .setSpan(SpoilerCloseSpan(), start, start + 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } -} \ No newline at end of file diff --git a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/DefaultMarkwonProvider.kt b/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/DefaultMarkwonProvider.kt deleted file mode 100644 index 887150073..000000000 --- a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/DefaultMarkwonProvider.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.provider - -import android.content.Context -import android.text.Spanned -import android.text.util.Linkify -import android.widget.TextView -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.plugins.ClickableImagesPlugin -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.plugins.MarkwonLemmyLinkPlugin -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.plugins.MarkwonSpoilerPlugin -import com.github.diegoberaldin.raccoonforlemmy.core.utils.imagepreload.getCoilImageLoader -import io.noties.markwon.AbstractMarkwonPlugin -import io.noties.markwon.Markwon -import io.noties.markwon.MarkwonConfiguration -import io.noties.markwon.ext.strikethrough.StrikethroughPlugin -import io.noties.markwon.ext.tables.TablePlugin -import io.noties.markwon.html.HtmlPlugin -import io.noties.markwon.image.AsyncDrawableScheduler -import io.noties.markwon.image.coil.CoilImagesPlugin -import io.noties.markwon.linkify.LinkifyPlugin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch - -private const val OPEN_LINK_DELAY = 300L - -class DefaultMarkwonProvider( - context: Context, - override var onOpenUrl: ((String) -> Unit)? = null, - override var onOpenImage: ((String) -> Unit)? = null, -) : MarkwonProvider { - - override val markwon: Markwon - override val blockClickPropagation = MutableStateFlow(false) - private val scope = CoroutineScope(SupervisorJob()) - - init { - markwon = Markwon.builder(context) - .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) - .usePlugin(MarkwonLemmyLinkPlugin.create()) - .usePlugin(StrikethroughPlugin.create()) - .usePlugin(TablePlugin.create(context)) - .usePlugin(HtmlPlugin.create()) - .usePlugin( - MarkwonSpoilerPlugin.create( - onInteraction = { - blockClickPropagation.value = true - scope.launch { - delay(OPEN_LINK_DELAY) - blockClickPropagation.value = false - } - } - ) - ) - .run { - val imageLoader = getCoilImageLoader(context) - usePlugin(CoilImagesPlugin.create(context, imageLoader)) - } - .usePlugin(object : AbstractMarkwonPlugin() { - override fun beforeSetText(textView: TextView, markdown: Spanned) { - AsyncDrawableScheduler.unschedule(textView) - } - - override fun afterSetText(textView: TextView) { - AsyncDrawableScheduler.schedule(textView) - } - }) - .usePlugin( - ClickableImagesPlugin.create( - context = context, - onOpenImage = { url -> - blockClickPropagation.value = true - onOpenImage?.invoke(url) - scope.launch { - delay(OPEN_LINK_DELAY) - blockClickPropagation.value = false - } - }, - ) - ).usePlugin( - object : AbstractMarkwonPlugin() { - override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { - builder.linkResolver { view, link -> - view.cancelPendingInputEvents() - blockClickPropagation.value = true - onOpenUrl?.invoke(link) - scope.launch { - delay(OPEN_LINK_DELAY) - blockClickPropagation.value = false - } - } - } - }, - ) - .build() - } -} \ No newline at end of file diff --git a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/MarkwonProvider.kt b/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/MarkwonProvider.kt deleted file mode 100644 index e3ee6af10..000000000 --- a/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/MarkwonProvider.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.provider - -import io.noties.markwon.Markwon -import kotlinx.coroutines.flow.StateFlow - -interface MarkwonProvider { - val markwon: Markwon - val blockClickPropagation: StateFlow<Boolean> - var onOpenUrl: ((String) -> Unit)? - var onOpenImage: ((String) -> Unit)? -} \ No newline at end of file diff --git a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/ComposeLocal.kt b/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/ComposeLocal.kt deleted file mode 100755 index 0ff9094ed..000000000 --- a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/ComposeLocal.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose - -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.staticCompositionLocalOf -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.BulletHandler -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownColors -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownPadding -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.ReferenceLinkHandler - -/** - * The CompositionLocal to provide functionality related to transforming the bullet of an ordered list - */ -val LocalBulletListHandler = staticCompositionLocalOf { - return@staticCompositionLocalOf BulletHandler { "• " } -} - -/** - * The CompositionLocal to provide functionality related to transforming the bullet of an ordered list - */ -val LocalOrderedListHandler = staticCompositionLocalOf { - return@staticCompositionLocalOf BulletHandler { "$it " } -} - -/** - * Local [ReferenceLinkHandler] provider - */ -val LocalReferenceLinkHandler = staticCompositionLocalOf<ReferenceLinkHandler> { - error("CompositionLocal ReferenceLinkHandler not present") -} - -/** - * Local [MarkdownColors] provider - */ -val LocalMarkdownColors = compositionLocalOf<MarkdownColors> { - error("No local MarkdownColors") -} - -/** - * Local [MarkdownTypography] provider - */ -val LocalMarkdownTypography = compositionLocalOf<MarkdownTypography> { - error("No local MarkdownTypography") -} - -/** - * Local [MarkdownPadding] provider - */ -val LocalMarkdownPadding = staticCompositionLocalOf<MarkdownPadding> { - error("No local Padding") -} diff --git a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt b/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt deleted file mode 100755 index 6d1c73523..000000000 --- a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownColors -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownPadding -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.markdownColor -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.markdownPadding -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.markdownTypography - -@Composable -expect fun CustomMarkdown( - content: String, - modifier: Modifier = Modifier.fillMaxSize(), - colors: MarkdownColors = markdownColor( - text = MaterialTheme.colorScheme.onBackground, - backgroundCode = MaterialTheme.colorScheme.background, - ), - typography: MarkdownTypography = markdownTypography( - h1 = MaterialTheme.typography.titleLarge, - h2 = MaterialTheme.typography.titleLarge, - h3 = MaterialTheme.typography.titleMedium, - h4 = MaterialTheme.typography.titleMedium, - h5 = MaterialTheme.typography.titleSmall, - h6 = MaterialTheme.typography.titleSmall, - text = MaterialTheme.typography.bodyMedium, - paragraph = MaterialTheme.typography.bodyMedium, - ), - padding: MarkdownPadding = markdownPadding(), - maxLines: Int? = null, - onOpenUrl: ((String) -> Unit)? = null, - inlineImages: Boolean = true, - autoLoadImages: Boolean = true, - onOpenImage: ((String) -> Unit)? = null, - onClick: (() -> Unit)? = null, - onDoubleClick: (() -> Unit)? = null, - onLongClick: (() -> Unit)? = null, -) \ No newline at end of file diff --git a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/BulletHandler.kt b/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/BulletHandler.kt deleted file mode 100755 index 4e3a92e57..000000000 --- a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/BulletHandler.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model - -/** An interface of providing use case specific un/ordered list handling.*/ -fun interface BulletHandler { - fun transform(bullet: CharSequence?): String -} diff --git a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownColors.kt b/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownColors.kt deleted file mode 100755 index b2a6fe7ad..000000000 --- a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownColors.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import androidx.compose.ui.graphics.Color - -@Stable -interface MarkdownColors { - /** Represents the color used for the text of this [Markdown] component. */ - val text: Color - - /** Represents the background color for this [Markdown] component. */ - val backgroundCode: Color - - /** Represents the linl color for this [Markdown] component. */ - val linkColor: Color -} - -@Immutable -private class DefaultMarkdownColors( - override val text: Color, - override val backgroundCode: Color, - override val linkColor: Color, -) : MarkdownColors - -@Composable -fun markdownColor( - text: Color = MaterialTheme.colorScheme.onBackground, - backgroundCode: Color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.1f), - linkColor: Color = MaterialTheme.colorScheme.primary, -): MarkdownColors = DefaultMarkdownColors( - text = text, - backgroundCode = backgroundCode, - linkColor = linkColor, -) diff --git a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownPadding.kt b/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownPadding.kt deleted file mode 100755 index e91af8999..000000000 --- a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownPadding.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -interface MarkdownPadding { - val block: Dp - val list: Dp - val indentList: Dp -} - -@Immutable -private class DefaultMarkdownPadding( - override val block: Dp, - override val list: Dp, - override val indentList: Dp, -) : MarkdownPadding - -@Composable -fun markdownPadding( - block: Dp = 2.dp, - list: Dp = 8.dp, - indentList: Dp = 8.dp, -): MarkdownPadding = DefaultMarkdownPadding( - block = block, - list = list, - indentList = indentList, -) diff --git a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownTypography.kt b/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownTypography.kt deleted file mode 100755 index dd3fe201d..000000000 --- a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/MarkdownTypography.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle - -interface MarkdownTypography { - val text: TextStyle - val code: TextStyle - val h1: TextStyle - val h2: TextStyle - val h3: TextStyle - val h4: TextStyle - val h5: TextStyle - val h6: TextStyle - val quote: TextStyle - val paragraph: TextStyle - val ordered: TextStyle - val bullet: TextStyle - val list: TextStyle -} - -@Immutable -private class DefaultMarkdownTypography( - override val h1: TextStyle, - override val h2: TextStyle, - override val h3: TextStyle, - override val h4: TextStyle, - override val h5: TextStyle, - override val h6: TextStyle, - override val text: TextStyle, - override val code: TextStyle, - override val quote: TextStyle, - override val paragraph: TextStyle, - override val ordered: TextStyle, - override val bullet: TextStyle, - override val list: TextStyle, -) : MarkdownTypography - -@Composable -fun markdownTypography( - h1: TextStyle = MaterialTheme.typography.titleLarge, - h2: TextStyle = MaterialTheme.typography.titleLarge, - h3: TextStyle = MaterialTheme.typography.titleMedium, - h4: TextStyle = MaterialTheme.typography.titleMedium, - h5: TextStyle = MaterialTheme.typography.titleSmall, - h6: TextStyle = MaterialTheme.typography.titleSmall, - text: TextStyle = MaterialTheme.typography.bodyMedium, - code: TextStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace), - quote: TextStyle = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)), - paragraph: TextStyle = MaterialTheme.typography.bodyMedium, - ordered: TextStyle = MaterialTheme.typography.bodyMedium, - bullet: TextStyle = MaterialTheme.typography.bodyMedium, - list: TextStyle = MaterialTheme.typography.bodyMedium, -): MarkdownTypography = DefaultMarkdownTypography( - h1 = h1, h2 = h2, h3 = h3, h4 = h4, h5 = h5, h6 = h6, - text = text, quote = quote, code = code, paragraph = paragraph, - ordered = ordered, bullet = bullet, list = list, -) diff --git a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/ReferenceLinkHandler.kt b/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/ReferenceLinkHandler.kt deleted file mode 100755 index 925089f7f..000000000 --- a/core/md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/model/ReferenceLinkHandler.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model - -/** - * Interface to describe the [ReferenceLinkHandler] - */ -interface ReferenceLinkHandler { - /** Keep the provided link */ - fun store(label: String, destination: String?) - - /** Returns the link for the provided label if it exists */ - fun find(label: String): String -} - -/** - * Implementation for [ReferenceLinkHandler] to resolve referenced link within the Markdown - */ -class ReferenceLinkHandlerImpl : ReferenceLinkHandler { - private val stored = mutableMapOf<String, String?>() - override fun store(label: String, destination: String?) { - stored[label] = destination - } - - override fun find(label: String): String { - return stored[label] ?: label - } -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt deleted file mode 100755 index cd1b51901..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/CustomMarkdown.kt +++ /dev/null @@ -1,218 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Modifier -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownBlockQuote -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownBulletList -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownCodeBlock -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownCodeFence -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownHeader -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownImage -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownOrderedList -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownParagraph -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements.MarkdownText -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownColors -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownPadding -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.MarkdownTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.model.ReferenceLinkHandlerImpl -import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick -import org.intellij.markdown.MarkdownElementTypes -import org.intellij.markdown.MarkdownElementTypes.ATX_1 -import org.intellij.markdown.MarkdownElementTypes.ATX_2 -import org.intellij.markdown.MarkdownElementTypes.ATX_3 -import org.intellij.markdown.MarkdownElementTypes.ATX_4 -import org.intellij.markdown.MarkdownElementTypes.ATX_5 -import org.intellij.markdown.MarkdownElementTypes.ATX_6 -import org.intellij.markdown.MarkdownElementTypes.BLOCK_QUOTE -import org.intellij.markdown.MarkdownElementTypes.CODE_BLOCK -import org.intellij.markdown.MarkdownElementTypes.CODE_FENCE -import org.intellij.markdown.MarkdownElementTypes.IMAGE -import org.intellij.markdown.MarkdownElementTypes.LINK_DEFINITION -import org.intellij.markdown.MarkdownElementTypes.ORDERED_LIST -import org.intellij.markdown.MarkdownElementTypes.PARAGRAPH -import org.intellij.markdown.MarkdownElementTypes.UNORDERED_LIST -import org.intellij.markdown.MarkdownTokenTypes.Companion.EOL -import org.intellij.markdown.MarkdownTokenTypes.Companion.TEXT -import org.intellij.markdown.ast.ASTNode -import org.intellij.markdown.ast.findChildOfType -import org.intellij.markdown.ast.getTextInNode -import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor -import org.intellij.markdown.parser.MarkdownParser - -/* - * CREDITS: - * https://github.com/mikepenz/multiplatform-markdown-renderer - */ -@Composable -actual fun CustomMarkdown( - content: String, - modifier: Modifier, - colors: MarkdownColors, - typography: MarkdownTypography, - padding: MarkdownPadding, - maxLines: Int?, - onOpenUrl: ((String) -> Unit)?, - inlineImages: Boolean, - autoLoadImages: Boolean, - onOpenImage: ((String) -> Unit)?, - onClick: (() -> Unit)?, - onDoubleClick: (() -> Unit)?, - onLongClick: (() -> Unit)?, -) { - val matches = Regex("::: spoiler (?<title>.*?)\\n(?<content>.*?)\\n:::\\n").findAll(content) - val mangledContent = buildString { - var lastIndex = -1 - for (match in matches) { - val (start, end) = match.range.first to match.range.last - if (lastIndex == -1) { - append(content.substring(0, start)) - } else { - append(content.substring(lastIndex, start)) - } - val title = match.groups["title"]?.value.orEmpty() - val spoilerContent = match.groups["content"]?.value.orEmpty() - val replacement = - "<details>\\n<summary>\\n$title\\n</summary>\\n\\n$spoilerContent\\n</details>\\n" - append(replacement) - lastIndex = end - } - if (lastIndex >= 0) { - if (lastIndex < content.length) { - append(content.substring(lastIndex)) - } - } else { - append(content) - } - } - .replace("&", "&") - .replace(" ", " ") - - CompositionLocalProvider( - LocalReferenceLinkHandler provides ReferenceLinkHandlerImpl(), - LocalMarkdownPadding provides padding, - LocalMarkdownColors provides colors, - LocalMarkdownTypography provides typography, - ) { - Column( - modifier = modifier.onClick( - onClick = onClick ?: {}, - onDoubleClick = onDoubleClick ?: {}, - onLongClick = onLongClick ?: {}, - ) - ) { - val parsedTree = - MarkdownParser(GFMFlavourDescriptor()).buildMarkdownTreeFromString(mangledContent) - parsedTree.children.forEach { node -> - if (!node.handleElement( - content = mangledContent, - maxLines = maxLines, - onOpenUrl = onOpenUrl, - inlineImages = inlineImages, - autoLoadImages = autoLoadImages, - onOpenImage = onOpenImage, - ) - ) { - node.children.forEach { child -> - child.handleElement( - content = mangledContent, - maxLines = maxLines, - onOpenUrl = onOpenUrl, - inlineImages = inlineImages, - autoLoadImages = autoLoadImages, - onOpenImage = onOpenImage, - ) - } - } - } - } - } -} - -@Composable -private fun ASTNode.handleElement( - content: String, - maxLines: Int? = null, - onOpenUrl: ((String) -> Unit)? = null, - inlineImages: Boolean = true, - autoLoadImages: Boolean = true, - onOpenImage: ((String) -> Unit)? = null, -): Boolean { - val typography = LocalMarkdownTypography.current - var handled = true - Spacer(Modifier.height(LocalMarkdownPadding.current.block)) - when (type) { - TEXT -> { - val text = getTextInNode(content).toString() - MarkdownText( - content = text, - maxLines = maxLines, - onOpenUrl = onOpenUrl, - inlineImages = inlineImages, - onOpenImage = onOpenImage, - ) - } - - EOL -> {} - CODE_FENCE -> MarkdownCodeFence(content, this) - CODE_BLOCK -> MarkdownCodeBlock(content, this) - ATX_1 -> MarkdownHeader(content, this, typography.h1) - ATX_2 -> MarkdownHeader(content, this, typography.h2) - ATX_3 -> MarkdownHeader(content, this, typography.h3) - ATX_4 -> MarkdownHeader(content, this, typography.h4) - ATX_5 -> MarkdownHeader(content, this, typography.h5) - ATX_6 -> MarkdownHeader(content, this, typography.h6) - BLOCK_QUOTE -> MarkdownBlockQuote(content, this) - PARAGRAPH -> MarkdownParagraph( - content = content, - maxLines = maxLines, - node = this, - style = typography.paragraph, - onOpenUrl = onOpenUrl, - inlineImages = inlineImages, - autoLoadImages = autoLoadImages, - onOpenImage = onOpenImage, - ) - - ORDERED_LIST -> Column(modifier = Modifier) { - MarkdownOrderedList( - content = content, - node = this@handleElement, - style = typography.ordered, - onOpenUrl = onOpenUrl, - ) - } - - UNORDERED_LIST -> Column(modifier = Modifier) { - MarkdownBulletList( - content = content, - node = this@handleElement, - style = typography.bullet, - onOpenUrl = onOpenUrl - ) - } - - IMAGE -> MarkdownImage( - content = content, - node = this, - autoLoadImages = autoLoadImages - ) - - LINK_DEFINITION -> { - val linkLabel = - findChildOfType(MarkdownElementTypes.LINK_LABEL)?.getTextInNode(content)?.toString() - if (linkLabel != null) { - val destination = - findChildOfType(MarkdownElementTypes.LINK_DESTINATION)?.getTextInNode(content) - ?.toString() - LocalReferenceLinkHandler.current.store(linkLabel, destination) - } - } - - else -> handled = false - } - return handled -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownBlockQuote.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownBlockQuote.kt deleted file mode 100755 index cbcbc9883..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownBlockQuote.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.unit.dp -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownTypography -import org.intellij.markdown.ast.ASTNode -import org.intellij.markdown.ast.getTextInNode - -@Composable -internal fun MarkdownBlockQuote( - content: String, - node: ASTNode, - style: TextStyle = LocalMarkdownTypography.current.quote, -) { - Box( - modifier = Modifier - .drawBehind { - drawLine( - color = style.color, - strokeWidth = 2f, - start = Offset(12.dp.value, 0f), - end = Offset(12.dp.value, size.height), - ) - } - .padding(start = 16.dp, top = 16.dp, bottom = 16.dp), - ) { - val text = buildAnnotatedString { - pushStyle(style.toSpanStyle()) - val text = node.getTextInNode(content).toString() - .replace(Regex("^\\s*?>"), "") - .replace(Regex("\n\\s*?>"), "\n") - .trim() - append(text) - pop() - } - Text(text) - } -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownCode.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownCode.kt deleted file mode 100755 index bd6fce45a..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownCode.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements - -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownColors -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownTypography -import org.intellij.markdown.ast.ASTNode - -@Composable -private fun MarkdownCode( - code: String, - style: TextStyle = LocalMarkdownTypography.current.code, -) { - val backgroundCodeColor = LocalMarkdownColors.current.backgroundCode - Surface( - color = backgroundCodeColor, - shape = RoundedCornerShape(8.dp), - modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), - ) { - Text( - code, - modifier = Modifier.horizontalScroll(rememberScrollState()).padding(8.dp), - style = style, - color = LocalMarkdownColors.current.text, - ) - } -} - -@Composable -internal fun MarkdownCodeFence( - content: String, - node: ASTNode, -) { - // CODE_FENCE_START, FENCE_LANG, {content}, CODE_FENCE_END - val start = node.children[2].startOffset - val end = node.children[node.children.size - 2].endOffset - MarkdownCode(content.subSequence(start, end).toString().replaceIndent()) -} - -@Composable -internal fun MarkdownCodeBlock( - content: String, - node: ASTNode, -) { - val start = node.children[0].startOffset - val end = node.children[node.children.size - 1].endOffset - MarkdownCode(content.subSequence(start, end).toString().replaceIndent()) -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownHeader.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownHeader.kt deleted file mode 100755 index a79f8105e..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownHeader.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownColors -import org.intellij.markdown.MarkdownTokenTypes -import org.intellij.markdown.ast.ASTNode -import org.intellij.markdown.ast.findChildOfType -import org.intellij.markdown.ast.getTextInNode - -@Composable -internal fun MarkdownHeader( - content: String, - node: ASTNode, - style: TextStyle, -) { - node.findChildOfType(MarkdownTokenTypes.ATX_CONTENT)?.let { - Text( - text = it.getTextInNode(content).trim().toString(), - modifier = Modifier.fillMaxWidth().padding(top = 16.dp), - style = style, - color = LocalMarkdownColors.current.text, - ) - } -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownImage.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownImage.kt deleted file mode 100755 index 0014228fc..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownImage.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements - -import androidx.compose.animation.core.InfiniteRepeatableSpec -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.style.TextAlign -import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomImage -import com.github.diegoberaldin.raccoonforlemmy.core.l10n.LocalXmlStrings -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.utils.findChildOfTypeRecursive -import org.intellij.markdown.MarkdownElementTypes -import org.intellij.markdown.ast.ASTNode -import org.intellij.markdown.ast.getTextInNode - -@Composable -internal fun MarkdownImage(content: String, node: ASTNode, autoLoadImages: Boolean = true) { - val link = - node.findChildOfTypeRecursive(MarkdownElementTypes.LINK_DESTINATION)?.getTextInNode(content) - ?.toString() ?: return - - CustomImage( - url = link, - autoload = autoLoadImages, - quality = FilterQuality.Low, - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth(), - onFailure = { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = LocalXmlStrings.current.messageImageLoadingError, - style = LocalMarkdownTypography.current.text, - ) - }, - onLoading = { progress -> - val prog = if (progress != null) { - progress - } else { - val transition = rememberInfiniteTransition() - val res by transition.animateFloat( - initialValue = 0f, - targetValue = 1f, - animationSpec = InfiniteRepeatableSpec( - animation = tween(1000) - ) - ) - res - } - CircularProgressIndicator( - progress = prog, - color = MaterialTheme.colorScheme.primary, - ) - }, - ) -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownList.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownList.kt deleted file mode 100755 index 8a785decf..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownList.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.unit.dp -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalBulletListHandler -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownColors -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownPadding -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalOrderedListHandler -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.utils.buildMarkdownAnnotatedString -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.utils.filterNonListTypes -import org.intellij.markdown.MarkdownElementTypes -import org.intellij.markdown.MarkdownElementTypes.ORDERED_LIST -import org.intellij.markdown.MarkdownElementTypes.UNORDERED_LIST -import org.intellij.markdown.MarkdownTokenTypes -import org.intellij.markdown.ast.ASTNode -import org.intellij.markdown.ast.findChildOfType -import org.intellij.markdown.ast.getTextInNode - -@Composable -private fun MarkdownListItems( - content: String, - node: ASTNode, - style: TextStyle = LocalMarkdownTypography.current.list, - level: Int = 0, - autoLoadImages: Boolean = true, - item: @Composable (child: ASTNode) -> Unit, -) { - val listDp = LocalMarkdownPadding.current.list - val indentListDp = LocalMarkdownPadding.current.indentList - Column( - modifier = Modifier.padding( - start = (indentListDp) * level, - top = listDp, - bottom = listDp, - ), - ) { - node.children.forEach { child -> - when (child.type) { - MarkdownElementTypes.LIST_ITEM -> { - item(child) - when (child.children.last().type) { - ORDERED_LIST -> MarkdownOrderedList( - content = content, - node = child, - style = style, - level = level + 1, - autoLoadImages = autoLoadImages, - ) - - UNORDERED_LIST -> MarkdownBulletList( - content = content, - node = child, - style = style, - level = level + 1, - autoLoadImages = autoLoadImages, - ) - } - } - - ORDERED_LIST -> MarkdownOrderedList( - content = content, - node = child, - style = style, - level = level + 1, - autoLoadImages = autoLoadImages, - ) - - UNORDERED_LIST -> MarkdownBulletList( - content = content, - node = child, - style = style, - level = level + 1, - autoLoadImages = autoLoadImages, - ) - } - } - } -} - -@Composable -internal fun MarkdownOrderedList( - content: String, - node: ASTNode, - style: TextStyle = LocalMarkdownTypography.current.ordered, - level: Int = 0, - autoLoadImages: Boolean = true, - onOpenUrl: ((String) -> Unit)? = null, -) { - val orderedListHandler = LocalOrderedListHandler.current - MarkdownListItems( - content = content, - node = node, - style = style, - level = level, - autoLoadImages = autoLoadImages, - ) { child -> - Row(Modifier.fillMaxWidth()) { - Text( - text = orderedListHandler.transform( - child.findChildOfType(MarkdownTokenTypes.LIST_NUMBER)?.getTextInNode(content), - ), - style = style, - color = LocalMarkdownColors.current.text, - ) - val text = buildAnnotatedString { - pushStyle(style.toSpanStyle()) - buildMarkdownAnnotatedString( - content = content, - children = child.children.filterNonListTypes(), - linkColor = LocalMarkdownColors.current.linkColor, - ) - pop() - } - MarkdownText( - text, - Modifier.padding(bottom = 4.dp), - style = style, - onOpenUrl = onOpenUrl, - autoLoadImages = autoLoadImages, - ) - } - } -} - -@Composable -internal fun MarkdownBulletList( - content: String, - node: ASTNode, - style: TextStyle = LocalMarkdownTypography.current.bullet, - level: Int = 0, - autoLoadImages: Boolean = true, - onOpenUrl: ((String) -> Unit)? = null, -) { - val bulletHandler = LocalBulletListHandler.current - MarkdownListItems( - content = content, - node = node, - style = style, - level = level, - autoLoadImages = autoLoadImages, - ) { child -> - Row(Modifier.fillMaxWidth()) { - Text( - bulletHandler.transform( - child.findChildOfType(MarkdownTokenTypes.LIST_BULLET)?.getTextInNode(content), - ), - style = style, - color = LocalMarkdownColors.current.text, - ) - val text = buildAnnotatedString { - pushStyle(style.toSpanStyle()) - buildMarkdownAnnotatedString( - content = content, - children = child.children.filterNonListTypes(), - linkColor = LocalMarkdownColors.current.linkColor - ) - pop() - } - MarkdownText( - text, - Modifier.padding(bottom = 4.dp), - style = style, - onOpenUrl = onOpenUrl, - autoLoadImages = autoLoadImages, - ) - } - } -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownParagraph.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownParagraph.kt deleted file mode 100755 index 31a2c130f..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownParagraph.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements - -import androidx.compose.runtime.Composable -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownColors -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.utils.buildMarkdownAnnotatedString -import org.intellij.markdown.ast.ASTNode - -@Composable -internal fun MarkdownParagraph( - content: String, - node: ASTNode, - maxLines: Int? = null, - style: TextStyle = LocalMarkdownTypography.current.paragraph, - onOpenUrl: ((String) -> Unit)? = null, - inlineImages: Boolean = true, - autoLoadImages: Boolean = true, - onOpenImage: ((String) -> Unit)? = null, -) { - val styledText = buildAnnotatedString { - pushStyle(style.toSpanStyle()) - buildMarkdownAnnotatedString( - content = content, - node = node, - linkColor = LocalMarkdownColors.current.linkColor - ) - pop() - } - MarkdownText( - content = styledText, - maxLines = maxLines, - style = style, - onOpenUrl = onOpenUrl, - inlineImages = inlineImages, - onOpenImage = onOpenImage, - autoLoadImages = autoLoadImages, - ) -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownText.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownText.kt deleted file mode 100755 index 7ac8c8de8..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownText.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.elements - -import androidx.compose.animation.core.InfiniteRepeatableSpec -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.Placeholder -import androidx.compose.ui.text.PlaceholderVerticalAlign -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomImage -import com.github.diegoberaldin.raccoonforlemmy.core.l10n.LocalXmlStrings -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownColors -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalMarkdownTypography -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.LocalReferenceLinkHandler -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.utils.TAG_IMAGE_URL -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.utils.TAG_URL -import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick - -@Composable -internal fun MarkdownText( - content: String, - maxLines: Int?, - modifier: Modifier = Modifier, - style: TextStyle = LocalMarkdownTypography.current.text, - onOpenUrl: ((String) -> Unit)? = null, - inlineImages: Boolean = true, - autoLoadImages: Boolean = true, - onOpenImage: ((String) -> Unit)? = null, -) { - MarkdownText( - content = AnnotatedString(content), - maxLines = maxLines, - modifier = modifier, - style = style, - onOpenUrl = onOpenUrl, - inlineImages = inlineImages, - autoLoadImages = autoLoadImages, - onOpenImage = onOpenImage, - ) -} - -@Composable -internal fun MarkdownText( - content: AnnotatedString, - maxLines: Int? = null, - modifier: Modifier = Modifier, - style: TextStyle = LocalMarkdownTypography.current.text, - onOpenUrl: ((String) -> Unit)? = null, - inlineImages: Boolean = true, - autoLoadImages: Boolean = true, - onOpenImage: ((String) -> Unit)? = null, -) { - val referenceLinkHandler = LocalReferenceLinkHandler.current - val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) } - - val hasUrl = content.getStringAnnotations(TAG_URL, 0, content.length).any() - val textModifier = if (hasUrl) { - modifier.pointerInput(Unit) { - detectTapGestures { pos -> - layoutResult.value?.let { layoutResult -> - val position = layoutResult.getOffsetForPosition(pos) - content.getStringAnnotations(TAG_URL, position, position) - .firstOrNull() - ?.let { - val url = referenceLinkHandler.find(it.item) - onOpenUrl?.invoke(url) - } - } - } - } - } else { - modifier - } - - var imageUrl by remember { mutableStateOf("") } - Column( - modifier = Modifier.fillMaxWidth(), - ) { - if (inlineImages || content.text != imageUrl) { - Text( - maxLines = maxLines ?: Int.MAX_VALUE, - text = content, - modifier = textModifier, - style = style, - inlineContent = mapOf( - TAG_IMAGE_URL to InlineTextContent( - if (inlineImages) { - Placeholder( - 180.sp, - 180.sp, - PlaceholderVerticalAlign.Bottom, - ) // TODO: identify flexible scaling! - } else { - Placeholder(1.sp, 1.sp, PlaceholderVerticalAlign.Bottom) - } - ) { link -> - if (inlineImages) { - CustomImage( - modifier = Modifier - .fillMaxWidth() - .onClick( - onClick = { onOpenImage?.invoke(imageUrl) }, - ), - url = link, - autoload = autoLoadImages, - quality = FilterQuality.Low, - contentScale = ContentScale.FillWidth, - onFailure = { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = LocalXmlStrings.current.messageImageLoadingError, - style = LocalMarkdownTypography.current.text - ) - }, - onLoading = { progress -> - val prog = if (progress != null) { - progress - } else { - val transition = rememberInfiniteTransition() - val res by transition.animateFloat( - initialValue = 0f, - targetValue = 1f, - animationSpec = InfiniteRepeatableSpec( - animation = tween(1000) - ) - ) - res - } - CircularProgressIndicator( - progress = prog, - color = MaterialTheme.colorScheme.primary, - ) - }, - ) - } else { - imageUrl = link - } - }, - ), - color = LocalMarkdownColors.current.text, - onTextLayout = { layoutResult.value = it }, - ) - } - if (!inlineImages && imageUrl.isNotEmpty()) { - CustomImage( - modifier = modifier.fillMaxWidth() - // TODO: improve fixed values - .heightIn(min = 200.dp, max = Dp.Unspecified) - .clip(RoundedCornerShape(20.dp)) - .onClick( - onClick = { onOpenImage?.invoke(imageUrl) }, - onDoubleClick = {}, - ), - url = imageUrl, - autoload = autoLoadImages, - quality = FilterQuality.Low, - contentScale = ContentScale.FillWidth, - onFailure = { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = LocalXmlStrings.current.messageImageLoadingError, - style = LocalMarkdownTypography.current.text, - ) - }, - onLoading = { progress -> - val prog = if (progress != null) { - progress - } else { - val transition = rememberInfiniteTransition() - val res by transition.animateFloat( - initialValue = 0f, - targetValue = 1f, - animationSpec = InfiniteRepeatableSpec( - animation = tween(1000) - ) - ) - res - } - CircularProgressIndicator( - progress = prog, - color = MaterialTheme.colorScheme.primary, - ) - }, - ) - } - } -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/utils/AnnotatedStringKtx.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/utils/AnnotatedStringKtx.kt deleted file mode 100755 index cd9be5927..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/utils/AnnotatedStringKtx.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.utils - -import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration -import org.intellij.markdown.MarkdownElementTypes -import org.intellij.markdown.MarkdownTokenTypes -import org.intellij.markdown.MarkdownTokenTypes.Companion.TEXT -import org.intellij.markdown.ast.ASTNode -import org.intellij.markdown.ast.findChildOfType -import org.intellij.markdown.ast.getTextInNode -import org.intellij.markdown.flavours.gfm.GFMTokenTypes - -internal fun AnnotatedString.Builder.appendMarkdownLink( - content: String, - node: ASTNode, - linkColor: Color, -) { - val linkText = node.findChildOfType(MarkdownElementTypes.LINK_TEXT)?.children?.innerList() - if (linkText == null) { - append(node.getTextInNode(content).toString()) - return - } - val destination = - node.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)?.getTextInNode(content) - ?.toString() - val linkLabel = - node.findChildOfType(MarkdownElementTypes.LINK_LABEL)?.getTextInNode(content)?.toString() - val label = (destination ?: linkLabel) - if (label != null) { - pushStringAnnotation(TAG_URL, label) - } - pushStyle( - SpanStyle( - textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Bold, - color = linkColor, - ) - ) - buildMarkdownAnnotatedString(content, linkText, linkColor) - pop() - if (label != null) { - pop() - } -} - -internal fun AnnotatedString.Builder.appendAutoLink( - content: String, - node: ASTNode, - linkColor: Color, -) { - val destination = node.getTextInNode(content).toString() - pushStringAnnotation(TAG_URL, (destination)) - pushStyle( - SpanStyle( - textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Bold, - color = linkColor, - ) - ) - append(destination) - pop() - pop() -} - -internal fun AnnotatedString.Builder.buildMarkdownAnnotatedString( - content: String, - node: ASTNode, - linkColor: Color, -) { - buildMarkdownAnnotatedString(content, node.children, linkColor) -} - -internal fun AnnotatedString.Builder.buildMarkdownAnnotatedString( - content: String, - children: List<ASTNode>, - linkColor: Color, -) { - children.forEach { child -> - when (child.type) { - MarkdownElementTypes.PARAGRAPH -> buildMarkdownAnnotatedString( - content, - child, - linkColor - ) - - MarkdownElementTypes.IMAGE -> child.findChildOfTypeRecursive(MarkdownElementTypes.LINK_DESTINATION) - ?.let { - appendInlineContent(TAG_IMAGE_URL, it.getTextInNode(content).toString()) - } - - MarkdownElementTypes.EMPH -> { - pushStyle(SpanStyle(fontStyle = FontStyle.Italic)) - buildMarkdownAnnotatedString(content, child, linkColor) - pop() - } - - MarkdownElementTypes.STRONG -> { - pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) - buildMarkdownAnnotatedString(content, child, linkColor) - pop() - } - - MarkdownElementTypes.CODE_SPAN -> { - pushStyle(SpanStyle(fontFamily = FontFamily.Monospace)) - append(' ') - buildMarkdownAnnotatedString(content, child.children.innerList(), linkColor) - append(' ') - pop() - } - - MarkdownElementTypes.AUTOLINK -> appendAutoLink(content, child, linkColor) - MarkdownElementTypes.INLINE_LINK -> appendMarkdownLink(content, child, linkColor) - MarkdownElementTypes.SHORT_REFERENCE_LINK -> appendMarkdownLink( - content, - child, - linkColor - ) - - MarkdownElementTypes.FULL_REFERENCE_LINK -> appendMarkdownLink( - content, - child, - linkColor - ) - - TEXT -> append(child.getTextInNode(content).toString()) - GFMTokenTypes.GFM_AUTOLINK -> if (child.parent == MarkdownElementTypes.LINK_TEXT) { - append(child.getTextInNode(content).toString()) - } else { - appendAutoLink(content, child, linkColor) - } - - MarkdownTokenTypes.SINGLE_QUOTE -> append('\'') - MarkdownTokenTypes.DOUBLE_QUOTE -> append('\"') - MarkdownTokenTypes.LPAREN -> append('(') - MarkdownTokenTypes.RPAREN -> append(')') - MarkdownTokenTypes.LBRACKET -> append('[') - MarkdownTokenTypes.RBRACKET -> append(']') - MarkdownTokenTypes.LT -> append('<') - MarkdownTokenTypes.GT -> append('>') - MarkdownTokenTypes.COLON -> append(':') - MarkdownTokenTypes.EXCLAMATION_MARK -> append('!') - MarkdownTokenTypes.BACKTICK -> append('`') - MarkdownTokenTypes.HARD_LINE_BREAK -> append("\n\n") - MarkdownTokenTypes.EOL -> append('\n') - MarkdownTokenTypes.WHITE_SPACE -> append(' ') - } - } -} diff --git a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/utils/Extensions.kt b/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/utils/Extensions.kt deleted file mode 100755 index d62582f9f..000000000 --- a/core/md/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/utils/Extensions.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.diegoberaldin.raccoonforlemmy.core.markdown.utils - -import org.intellij.markdown.IElementType -import org.intellij.markdown.MarkdownElementTypes -import org.intellij.markdown.MarkdownTokenTypes -import org.intellij.markdown.ast.ASTNode - -/** - * Tag used to indicate an url for inline content. Required for click handling. - */ -internal const val TAG_URL = "MARKDOWN_URL" - -/** - * Tag used to indicate an image url for inline content. Required for rendering. - */ -internal const val TAG_IMAGE_URL = "MARKDOWN_IMAGE_URL" - -/** - * Find a child node recursive - */ -internal fun ASTNode.findChildOfTypeRecursive(type: IElementType): ASTNode? { - children.forEach { - if (it.type == type) { - return it - } else { - val found = it.findChildOfTypeRecursive(type) - if (found != null) { - return found - } - } - } - return null -} - -/** - * Helper function to drop the first and last element in the children list. - * E.g. we don't want to render the brackets of a link - */ -internal fun List<ASTNode>.innerList(): List<ASTNode> = this.subList(1, this.size - 1) - -/** - * Helper function to filter out items within a list of nodes, not of interest for the bullet list. - */ -internal fun List<ASTNode>.filterNonListTypes(): List<ASTNode> = this.filter { n -> - n.type != MarkdownElementTypes.ORDERED_LIST && n.type != MarkdownElementTypes.UNORDERED_LIST && n.type != MarkdownTokenTypes.EOL -} - diff --git a/docs/index.md b/docs/index.md index 97fb246fc..ae356fdea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -112,14 +112,11 @@ comment, remove post/comment, ban users and the ability to revert any of these a ## Credits -A saying from the original developer: «whenever in doubt, anguish or uncertainty, look at the code +A saying from the original developer: «Whenever in doubt, anguish or uncertainty, look at the code of Jerboa for Lemmy». It is without any doubt that this project has a gratitude debt -towards [Jerboa](https://github.com/dessalines/jerboa), mainly in two crucial aspects of the app: - -- markdown processing and rendering, where `MarkwonProvider` and its implementation are inspired by - Jerboa for Lemmy; -- comment processing to reconstruct the tree with missing nodes, where the memoized algorithm is, - too, inspired by Jerboa's. +towards [Jerboa](https://github.com/dessalines/jerboa), mainly in two crucial aspects of the app +such as comment processing to reconstruct the tree with missing nodes, where the memoized algorithm +is inspired by Jerboa's. The UI is inspired (in principle, rather than in actual implementation) on the great [Thunder](https://github.com/thunder-app/thunder) app. diff --git a/docs/tech_manual/module_structure.md b/docs/tech_manual/module_structure.md index 35af9041d..450698e7f 100644 --- a/docs/tech_manual/module_structure.md +++ b/docs/tech_manual/module_structure.md @@ -24,9 +24,9 @@ Here is a description of the dependency flow: by `:shared` (which does the binding between `:detailopener-api` and `:detailopener-impl`) and it includes some unit modules but the fact of a unit module included by a core module in general should never happen (instead, the reverse is perfectly ok); -- `:core` modules can sometimes include each other (but without cycles, e.g. `:core:md` +- `:core` modules can sometimes include each other (but without cycles, e.g. `:core:markdown` includes `:core:commonui:components` / `:core:utils` because it is a mid-level module and - something similar happens with :core:persistence which + something similar happens with `:core:persistecnce` which uses `:core:preferences` / `:core:appearance`) and nothing else; they are in turn used by all the other types of modules. @@ -143,7 +143,7 @@ are called throughout the whole project. Here is a short description of them: - `:modals`: definition of modal bottom sheets and dialogs that have no presentation logic. This module was historically much bigger and over time components were migrated to separate units modules; -- `:core:md` contains Markdown rendering components; +- `:core:markdown` contains Markdown rendering logic; - `core:l10n` contains all the localization messages and the `L10nManager` interface which acts as a wrapper around Lyricist to load the internationalized messages; - `:core:navigation` contains the navigation manager used for stack navigation, bottom sheet diff --git a/docs/tech_manual/tech_stack.md b/docs/tech_manual/tech_stack.md index dc9da6ca2..c523f0f99 100644 --- a/docs/tech_manual/tech_stack.md +++ b/docs/tech_manual/tech_stack.md @@ -75,19 +75,14 @@ formerly <a href="https://github.com/sqlcipher/android-database-sqlcipher">Andro <dt>Markdown rendering</dt> <dd> -This was another part, like image loading, where KMP is still lacking and things are far more -complicated than it should be. The first approach that was used in the project, and which still survives -in the iOS platform (being it "no man's land" currently) involved using JetBrain's <a href="https://github.com/JetBrains/markdown">Markdown</a> -library for parsing in conjunction with custom Compose rendering inspired by -<a href="https://github.com/mikepenz/multiplatform-markdown-renderer">Multiplatform Markdown Renderer</a>. -This approach was promising in the beginning but it has proven to grow more and more difficult to -support custom Markdown features, such as Lemmy spoilers. For this reason, the Android counterpart -has been completely refactored and migrated to the <a href="https://github.com/noties/Markwon">Markwon</a> -library which is more flexible/extensible albeit more complicated to use, especially if called from -a multiplatform environment with <code>expect</code>/<code>actual</code> functions (and image opening/URL opening/custom links -like Lemmy URL references have to be managed). The big star here is <code>MarkwonProvider</code> and its implementation -<a href="https://github.com/diegoberaldin/RaccoonForLemmy/blob/master/core/md/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/provider/DefaultMarkwonProvider.kt">DefaultMarkwonProvider.kt</a>. -Parts of the Markwon configuration and usage is inspired by <a href="https://github.com/dessalines/jerboa">Jerboa for Lemmy</a>. +This was another part, like image loading, where KMP was at the beginning quite lacky. After having given +up for some time and used Markwon (Java + Views) on the Android part of the app, I decided to give a +second chance to <a href="https://github.com/mikepenz/multiplatform-markdown-renderer">Multiplatform Markdown Renderer</a> +which was initially user for the multiplatform source set. The project grew and matured over time and +it made it possible to add custom handlers (like modular plug-ins) which made possible to support +Lemmy's custom features like spoilers. The migration from multiplatform renderer to Markwon and back to +multiplatform renderer was not easy, but this project is about KMP so, as a consequence, a pure Kotlin and +pure Compose solution <em>had to</em> be preferred. Even if it implies to sacrifice some functionality. </dd> <dt>Video playback</dt> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 45a2399d5..f77d8ce45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,8 +20,8 @@ ktor = "2.3.8" ktorfit_gradle = "1.12.0" ktorfit_lib = "1.12.0" lyricist = "1.6.2-1.8.20" -markdown = "0.5.0" -markwon = "4.6.2" +markdown = "0.6.1" +multiplatform_markdown_renderer = "0.12.0" materialKolor = "1.3.0" multiplatform_settings = "1.1.1" reorderable = "1.3.1" @@ -46,12 +46,7 @@ coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } android_gif_drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "android.gif.drawable" } markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } -markwon_core = { module = "io.noties.markwon:core", version.ref = "markwon" } -markwon_strikethrough = { module = "io.noties.markwon:ext-strikethrough", version.ref = "markwon" } -markwon_tables = { module = "io.noties.markwon:ext-tables", version.ref = "markwon" } -markwon_html = { module = "io.noties.markwon:html", version.ref = "markwon" } -markwon_linkify = { module = "io.noties.markwon:linkify", version.ref = "markwon" } -markwon_image_coil = { module = "io.noties.markwon:image-coil", version.ref = "markwon" } +multiplatform_markdown_renderer = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "multiplatform.markdown.renderer" } multiplatform_settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform.settings" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 099da690b..ea545fad9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,7 +29,7 @@ include(":core:commonui:detailopener-impl") include(":core:commonui:lemmyui") include(":core:commonui:modals") include(":core:l10n") -include(":core:md") +include(":core:markdown") include(":core:navigation") include(":core:notifications") include(":core:persistence") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 73858f821..2c3adc749 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -49,7 +49,6 @@ kotlin { implementation(projects.core.commonui.detailopenerImpl) implementation(projects.core.commonui.lemmyui) implementation(projects.core.l10n) - implementation(projects.core.md) implementation(projects.core.navigation) implementation(projects.core.notifications) implementation(projects.core.persistence) diff --git a/shared/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/di/DiHelper.kt b/shared/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/di/DiHelper.kt index 0badf52da..2dfa38c3b 100644 --- a/shared/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/di/DiHelper.kt +++ b/shared/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/di/DiHelper.kt @@ -4,7 +4,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.di.coreApiModule import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.coreAppearanceModule import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.di.lemmyUiModule import com.github.diegoberaldin.raccoonforlemmy.core.l10n.di.coreL10nModule -import com.github.diegoberaldin.raccoonforlemmy.core.markdown.di.markwonModule import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.navigationModule import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.coreNotificationModule import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.corePersistenceModule @@ -54,7 +53,6 @@ val sharedHelperModule = module { coreAppearanceModule, corePreferencesModule, coreApiModule, - markwonModule, coreIdentityModule, coreL10nModule, coreNotificationModule,