From 806f0094cfe4f01221d94c3f3b8690e72ae5526f Mon Sep 17 00:00:00 2001 From: Oleg Bask Date: Sun, 10 Nov 2024 21:55:23 +0000 Subject: [PATCH] Load block markdown images with Coil3 --- gradle/libs.versions.toml | 3 +++ markdown/core/build.gradle.kts | 3 +++ .../org/jetbrains/jewel/markdown/Markdown.kt | 20 +++++++++++++++++++ .../jetbrains/jewel/markdown/MarkdownBlock.kt | 2 ++ .../markdown/processing/MarkdownProcessor.kt | 17 +++++++++++++++- .../rendering/DefaultMarkdownBlockRenderer.kt | 7 +++++++ .../rendering/MarkdownBlockRenderer.kt | 2 ++ .../samples/ideplugin/ComponentShowcaseTab.kt | 2 ++ .../standalone/view/markdown/JewelReadme.kt | 2 ++ 9 files changed, 57 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4089fe4a0..c7ad89c44b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +coil = "3.0.2" commonmark = "0.24.0" composeDesktop = "1.7.0" detekt = "1.23.6" @@ -17,6 +18,8 @@ ktfmtGradlePlugin = "0.20.1" poko = "0.17.1" [libraries] +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } commonmark-core = { module = "org.commonmark:commonmark", version.ref = "commonmark" } commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" } diff --git a/markdown/core/build.gradle.kts b/markdown/core/build.gradle.kts index 151e991cb7..85cdfb428f 100644 --- a/markdown/core/build.gradle.kts +++ b/markdown/core/build.gradle.kts @@ -9,6 +9,9 @@ plugins { dependencies { api(projects.ui) api(libs.commonmark.core) + implementation(libs.coil.compose) + // TODO: figure out why ktor implementation crashes the IDE sample + implementation(libs.coil.network.okhttp) testImplementation(compose.desktop.uiTestJUnit4) testImplementation(projects.ui) diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/Markdown.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/Markdown.kt index 03f253dadd..a757d3b06e 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/Markdown.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/Markdown.kt @@ -17,6 +17,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.compose.setSingletonImageLoaderFactory +import coil3.memory.MemoryCache import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -76,6 +80,7 @@ public fun Markdown( markdownStyling: MarkdownStyling = JewelTheme.markdownStyling, blockRenderer: MarkdownBlockRenderer = DefaultMarkdownBlockRenderer(markdownStyling), ) { + setSingletonImageLoaderFactory(::createImageLoader) if (selectable) { SelectionContainer(modifier.semantics { rawMarkdown = markdown }) { Column(verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing)) { @@ -110,6 +115,7 @@ public fun LazyMarkdown( markdownStyling: MarkdownStyling = JewelTheme.markdownStyling, blockRenderer: MarkdownBlockRenderer = JewelTheme.markdownBlockRenderer, ) { + setSingletonImageLoaderFactory(::createImageLoader) if (selectable) { SelectionContainer(modifier) { LazyColumn( @@ -131,3 +137,17 @@ public fun LazyMarkdown( } } } + +private const val IMAGES_MEMORY_CACHE_SIZE = 20100500L // 20mb + +/** + * This method sets up an image loader with a memory cache but disables the disk cache. Disabling the disk cache is + * necessary because Coil crashes when attempting to use the file system cache with IDEA platform. + * + * Otherwise Coil3 will throw java.lang.NoSuchMethodError: kotlinx.coroutines.CoroutineDispatcher.limitedParallelism + */ +private fun createImageLoader(context: PlatformContext) = + ImageLoader.Builder(context) + .memoryCache { MemoryCache.Builder().maxSizeBytes(IMAGES_MEMORY_CACHE_SIZE).build() } + .diskCache(null) + .build() diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/MarkdownBlock.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/MarkdownBlock.kt index 892f760113..e002053bed 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/MarkdownBlock.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/MarkdownBlock.kt @@ -28,6 +28,8 @@ public sealed interface MarkdownBlock { @GenerateDataFunctions public class HtmlBlock(public val content: String) : MarkdownBlock + @GenerateDataFunctions public class Image(public val destination: String, public val title: String) : MarkdownBlock + public sealed interface ListBlock : MarkdownBlock { public val children: List public val isTight: Boolean diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt index cd526d0ae9..3605e557f5 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt @@ -8,6 +8,7 @@ import org.commonmark.node.Document import org.commonmark.node.FencedCodeBlock import org.commonmark.node.Heading import org.commonmark.node.HtmlBlock +import org.commonmark.node.Image import org.commonmark.node.IndentedCodeBlock import org.commonmark.node.ListBlock as CMListBlock import org.commonmark.node.ListItem @@ -169,7 +170,15 @@ public class MarkdownProcessor( private fun Node.tryProcessMarkdownBlock(): MarkdownBlock? = // Non-Block children are ignored when (this) { - is Paragraph -> toMarkdownParagraph() + is Paragraph -> { + val child = firstChild + if (child is Image && child === lastChild) { + // only render standalone images as blocks + child.toMarkdownImageOrNull() + } else { + toMarkdownParagraph() + } + } is Heading -> toMarkdownHeadingOrNull() is BulletList -> toMarkdownListOrNull() is OrderedList -> toMarkdownListOrNull() @@ -208,6 +217,12 @@ public class MarkdownProcessor( private fun IndentedCodeBlock.toMarkdownCodeBlockOrNull(): CodeBlock.IndentedCodeBlock = CodeBlock.IndentedCodeBlock(literal.trimEnd('\n')) + private fun Image.toMarkdownImageOrNull(): MarkdownBlock.Image? { + if (destination.isBlank()) return null + + return MarkdownBlock.Image(destination.trim(), (title ?: "").trim()) + } + private fun BulletList.toMarkdownListOrNull(): ListBlock.UnorderedList? { val children = processListItems() if (children.isEmpty()) return null diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt index ecb8f107b4..f0c14c83b9 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection.Ltr import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage import org.jetbrains.jewel.foundation.ExperimentalJewelApi import org.jetbrains.jewel.foundation.code.MimeType import org.jetbrains.jewel.foundation.code.highlighting.LocalCodeHighlighter @@ -89,6 +90,7 @@ public open class DefaultMarkdownBlockRenderer( is IndentedCodeBlock -> render(block, rootStyling.code.indented) is Heading -> render(block, rootStyling.heading, enabled, onUrlClick, onTextClick) is HtmlBlock -> render(block, rootStyling.htmlBlock) + is MarkdownBlock.Image -> render(block, rootStyling.image) is OrderedList -> render(block, rootStyling.list.ordered, enabled, onUrlClick, onTextClick) is UnorderedList -> render(block, rootStyling.list.unordered, enabled, onUrlClick, onTextClick) is ListItem -> render(block, enabled, onUrlClick, onTextClick) @@ -423,6 +425,11 @@ public open class DefaultMarkdownBlockRenderer( // HTML blocks are intentionally not rendered } + @Composable + override fun render(block: MarkdownBlock.Image, styling: MarkdownStyling.Image) { + AsyncImage(model = block.destination, contentDescription = block.title) + } + @Composable private fun rememberRenderedContent( block: WithInlineMarkdown, diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer.kt index b66ebc4979..72bc4640e4 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer.kt @@ -103,5 +103,7 @@ public interface MarkdownBlockRenderer { @Composable public fun render(block: HtmlBlock, styling: MarkdownStyling.HtmlBlock) + @Composable public fun render(block: MarkdownBlock.Image, styling: MarkdownStyling.Image) + public companion object } diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt index 62f0193114..029c0c2dd5 100644 --- a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt @@ -347,6 +347,8 @@ private fun MarkdownExample(project: Project) { | * Tables | * And more — I am running out of random things to say 😆 | + |![logo](https://avatars.githubusercontent.com/u/878437?s=42) + | |```kotlin |fun hello() = "World" |``` diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/JewelReadme.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/JewelReadme.kt index 5ddc0d580d..03c05aa25c 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/JewelReadme.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/JewelReadme.kt @@ -21,6 +21,8 @@ desktop-optimized theme and set of components. > > Use at your own risk! +![logo](https://avatars.githubusercontent.com/u/878437?s=42) + Jewel provides an implementation of the IntelliJ Platform themes that can be used in any Compose for Desktop application. Additionally, it has a Swing LaF Bridge that only works in the IntelliJ Platform (i.e., used to create IDE plugins), but automatically mirrors the current Swing LaF into Compose for a native-looking, consistent UI.