Skip to content

Commit

Permalink
Load block markdown images with Coil3
Browse files Browse the repository at this point in the history
  • Loading branch information
obask committed Nov 10, 2024
1 parent a3525ea commit 806f009
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 1 deletion.
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[versions]
coil = "3.0.2"
commonmark = "0.24.0"
composeDesktop = "1.7.0"
detekt = "1.23.6"
Expand All @@ -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" }

Expand Down
3 changes: 3 additions & 0 deletions markdown/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -110,6 +115,7 @@ public fun LazyMarkdown(
markdownStyling: MarkdownStyling = JewelTheme.markdownStyling,
blockRenderer: MarkdownBlockRenderer = JewelTheme.markdownBlockRenderer,
) {
setSingletonImageLoaderFactory(::createImageLoader)
if (selectable) {
SelectionContainer(modifier) {
LazyColumn(
Expand All @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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<ListItem>
public val isTight: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
|```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 806f009

Please sign in to comment.