Skip to content

Commit 6695a03

Browse files
author
Oleg Baskakov
committed
[JEWEL-746] Load markdown images using Coil3
It supports every image as an inline node; Using built-in coroutine library and ktor3 from the platform; Added SVG support using a coil dependency. Moved images into an extension so it can be loaded on demand without pushin coil3 dependencies to everyone. Add Coil3ImagesRendererExtension to rendering extensions to render images.
1 parent 78b9543 commit 6695a03

File tree

27 files changed

+465
-3
lines changed

27 files changed

+465
-3
lines changed

.idea/modules.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

platform/jewel/gradle/libs.versions.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[versions]
2+
coil = "3.1.0"
23
commonmark = "0.24.0"
34
composeDesktop = "1.8.1"
45
detekt = "1.23.6"
@@ -17,6 +18,11 @@ kotlinxBinaryCompat = "0.17.0"
1718
ktfmtGradlePlugin = "0.22.0"
1819

1920
[libraries]
21+
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
22+
# network is only needed in a standalone non-ide version
23+
coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
24+
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
25+
2026
commonmark-core = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
2127
commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" }
2228
commonmark-ext-gfm-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" }
@@ -27,6 +33,7 @@ filePicker = { module = "com.darkrockstudios:mpfilepicker", version.ref = "filep
2733
kotlinSarif = { module = "io.github.detekt.sarif4k:sarif4k", version.ref = "kotlinSarif" }
2834
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
2935
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
36+
ktor-client-java = { module = "io.ktor:ktor-client-java", version = "3.0.3" }
3037

3138
jna-core = { module = "net.java.dev.jna:jna", version.ref = "jna" }
3239

platform/jewel/markdown/core/api/core.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ public abstract interface class org/jetbrains/jewel/markdown/WithTextContent {
259259
public abstract fun getContent ()Ljava/lang/String;
260260
}
261261

262+
public abstract interface class org/jetbrains/jewel/markdown/extensions/ImageRendererExtension {
263+
public abstract fun imageContent (Lorg/jetbrains/jewel/markdown/InlineMarkdown$Image;Landroidx/compose/runtime/Composer;I)Landroidx/compose/foundation/text/InlineTextContent;
264+
}
265+
262266
public abstract interface class org/jetbrains/jewel/markdown/extensions/MarkdownBlockProcessorExtension {
263267
public abstract fun canProcess (Lorg/commonmark/node/CustomBlock;)Z
264268
public abstract fun processMarkdownBlock (Lorg/commonmark/node/CustomBlock;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;)Lorg/jetbrains/jewel/markdown/MarkdownBlock$CustomBlock;
@@ -307,11 +311,13 @@ public final class org/jetbrains/jewel/markdown/extensions/MarkdownProcessorExte
307311
public abstract interface class org/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension {
308312
public abstract fun getBlockRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension;
309313
public abstract fun getDelimitedInlineRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownDelimitedInlineRendererExtension;
314+
public abstract fun getImageRendererExtension ()Lorg/jetbrains/jewel/markdown/extensions/ImageRendererExtension;
310315
}
311316

312317
public final class org/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension$DefaultImpls {
313318
public static fun getBlockRenderer (Lorg/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension;)Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension;
314319
public static fun getDelimitedInlineRenderer (Lorg/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension;)Lorg/jetbrains/jewel/markdown/extensions/MarkdownDelimitedInlineRendererExtension;
320+
public static fun getImageRendererExtension (Lorg/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension;)Lorg/jetbrains/jewel/markdown/extensions/ImageRendererExtension;
315321
}
316322

317323
public final class org/jetbrains/jewel/markdown/processing/MarkdownParserFactory {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.jetbrains.jewel.markdown.extensions
2+
3+
import androidx.compose.foundation.text.InlineTextContent
4+
import androidx.compose.runtime.Composable
5+
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
6+
import org.jetbrains.jewel.markdown.InlineMarkdown
7+
8+
/** An extension for the Jewel Markdown rendering engine. */
9+
@ExperimentalJewelApi
10+
public interface ImageRendererExtension {
11+
@Composable public fun imageContent(image: InlineMarkdown.Image): InlineTextContent
12+
}

platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,14 @@ public interface MarkdownRendererExtension {
2424
*/
2525
public val delimitedInlineRenderer: MarkdownDelimitedInlineRendererExtension?
2626
get() = null
27+
28+
/**
29+
* An extension for [`InlineMarkdownRenderer`][org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer] that
30+
* will render supported [org.jetbrains.jewel.markdown.InlineMarkdown.CustomDelimitedNode]s into an annotated
31+
* string.
32+
*
33+
* Can be null if this extension doesn't support rendering delimited inline nodes.
34+
*/
35+
public val imageRendererExtension: ImageRendererExtension?
36+
get() = null
2737
}

platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.jetbrains.jewel.markdown.rendering
22

3+
import androidx.compose.foundation.text.appendInlineContent
34
import androidx.compose.ui.graphics.Color
45
import androidx.compose.ui.graphics.takeOrElse
56
import androidx.compose.ui.text.AnnotatedString
@@ -166,7 +167,8 @@ public open class DefaultInlineMarkdownRenderer(rendererExtensions: List<Markdow
166167
enabled: Boolean,
167168
currentTextStyle: TextStyle,
168169
) {
169-
// Not supported yet — see JEWEL-746
170+
// Each image source corresponds to one rendered image.
171+
appendInlineContent(node.source, "![${node.title}](${node.source})")
170172
}
171173

172174
// The T type parameter is needed to avoid issues with capturing lambdas

platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.height
1414
import androidx.compose.foundation.layout.padding
1515
import androidx.compose.foundation.layout.width
1616
import androidx.compose.foundation.layout.widthIn
17+
import androidx.compose.foundation.text.InlineTextContent
1718
import androidx.compose.foundation.text.selection.DisableSelection
1819
import androidx.compose.runtime.Composable
1920
import androidx.compose.runtime.CompositionLocalProvider
@@ -41,6 +42,7 @@ import org.jetbrains.jewel.foundation.code.MimeType
4142
import org.jetbrains.jewel.foundation.code.highlighting.LocalCodeHighlighter
4243
import org.jetbrains.jewel.foundation.modifier.thenIf
4344
import org.jetbrains.jewel.foundation.theme.LocalContentColor
45+
import org.jetbrains.jewel.markdown.InlineMarkdown
4446
import org.jetbrains.jewel.markdown.MarkdownBlock
4547
import org.jetbrains.jewel.markdown.MarkdownBlock.BlockQuote
4648
import org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock
@@ -147,6 +149,7 @@ public open class DefaultMarkdownBlockRenderer(
147149
.clickable(interactionSource = interactionSource, indication = null, onClick = onTextClick),
148150
text = renderedContent,
149151
style = mergedStyle,
152+
inlineContent = renderedImages(block),
150153
)
151154
}
152155

@@ -497,6 +500,15 @@ public open class DefaultMarkdownBlockRenderer(
497500
inlineRenderer.renderAsAnnotatedString(block.inlineContent, styling, enabled, onUrlClick)
498501
}
499502

503+
@Composable
504+
private fun renderedImages(blockInlineContent: WithInlineMarkdown): Map<String, InlineTextContent> {
505+
return rendererExtensions
506+
.firstNotNullOfOrNull { it.imageRendererExtension }
507+
?.let { imagesRenderer ->
508+
getImages(blockInlineContent).associate { image -> image.source to imagesRenderer.imageContent(image) }
509+
} ?: emptyMap()
510+
}
511+
500512
@Composable
501513
protected fun MaybeScrollingContainer(
502514
isScrollable: Boolean,
@@ -525,3 +537,21 @@ public open class DefaultMarkdownBlockRenderer(
525537
override operator fun plus(extension: MarkdownRendererExtension): MarkdownBlockRenderer =
526538
DefaultMarkdownBlockRenderer(rootStyling, rendererExtensions = rendererExtensions + extension, inlineRenderer)
527539
}
540+
541+
private fun getImages(input: WithInlineMarkdown): List<InlineMarkdown.Image> = buildList {
542+
fun collectImagesRecursively(items: List<InlineMarkdown>) {
543+
for (item in items) {
544+
when (item) {
545+
is InlineMarkdown.Image -> {
546+
if (item.source.isNotBlank()) add(item)
547+
}
548+
is WithInlineMarkdown -> {
549+
collectImagesRecursively(item.inlineContent)
550+
}
551+
552+
else -> {}
553+
}
554+
}
555+
}
556+
collectImagesRecursively(input.inlineContent)
557+
}

platform/jewel/markdown/extensions/gfm-alerts/api/gfm-alerts.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ public final class org/jetbrains/jewel/markdown/extensions/github/alerts/GitHubA
143143
public fun <init> (Lorg/jetbrains/jewel/markdown/extensions/github/alerts/AlertStyling;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;)V
144144
public fun getBlockRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension;
145145
public fun getDelimitedInlineRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownDelimitedInlineRendererExtension;
146+
public fun getImageRendererExtension ()Lorg/jetbrains/jewel/markdown/extensions/ImageRendererExtension;
146147
}
147148

148149
public final class org/jetbrains/jewel/markdown/extensions/github/alerts/ImportantAlertStyling : org/jetbrains/jewel/markdown/extensions/github/alerts/BaseAlertStyling {

platform/jewel/markdown/extensions/gfm-strikethrough/api/gfm-strikethrough.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ public final class org/jetbrains/jewel/markdown/extensions/github/strikethrough/
4444
public static final field INSTANCE Lorg/jetbrains/jewel/markdown/extensions/github/strikethrough/GitHubStrikethroughRendererExtension;
4545
public fun getBlockRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension;
4646
public fun getDelimitedInlineRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownDelimitedInlineRendererExtension;
47+
public fun getImageRendererExtension ()Lorg/jetbrains/jewel/markdown/extensions/ImageRendererExtension;
4748
}
4849

platform/jewel/markdown/extensions/gfm-tables/api/gfm-tables.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public final class org/jetbrains/jewel/markdown/extensions/github/tables/GitHubT
6767
public fun <init> (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;)V
6868
public fun getBlockRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension;
6969
public fun getDelimitedInlineRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownDelimitedInlineRendererExtension;
70+
public fun getImageRendererExtension ()Lorg/jetbrains/jewel/markdown/extensions/ImageRendererExtension;
7071
}
7172

7273
public final class org/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle : java/lang/Enum {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# GitHub Flavored Markdown — strikethrough extension
2+
3+
This extension adds support for strikethrough,
4+
a [GFM extension](https://github.github.com/gfm/#strikethrough-extension-) over
5+
CommonMark. Strikethrough nodes are parsed by the `commonmark-ext-images` library, and rendered simply by
6+
wrapping their content in a `SpanStyle` that applies the `LineThrough` decoration.
7+
8+
![Screenshot of a strikethrough](../../../art/docs/images.png)
9+
10+
## Usage
11+
12+
To use the strikethrough extension, you need to add the `GitHubStrikethroughProcessorExtension` to your
13+
`MarkdownProcessor`, and the
14+
`GitHubStrikethroughRendererExtension` to the `MarkdownBlockRenderer`. For example, in standalone mode:
15+
16+
```kotlin
17+
val isDark = JewelTheme.isDark
18+
19+
val markdownStyling = remember(isDark) { if (isDark) MarkdownStyling.dark() else MarkdownStyling.light() }
20+
21+
val processor = remember { MarkdownProcessor(listOf(GitHubStrikethroughProcessorExtension)) }
22+
23+
val blockRenderer =
24+
remember(markdownStyling) {
25+
if (isDark) {
26+
MarkdownBlockRenderer.dark(
27+
styling = markdownStyling,
28+
rendererExtensions = listOf(GitHubStrikethroughRendererExtension),
29+
)
30+
} else {
31+
MarkdownBlockRenderer.light(
32+
styling = markdownStyling,
33+
rendererExtensions = listOf(GitHubStrikethroughRendererExtension),
34+
)
35+
}
36+
}
37+
38+
ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) {
39+
// Your UI that renders Markdown goes here
40+
}
41+
```
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
f:org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughInlineProcessorExtension
2+
- org.jetbrains.jewel.markdown.extensions.MarkdownDelimitedInlineProcessorExtension
3+
- sf:$stable:I
4+
- sf:INSTANCE:org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughInlineProcessorExtension
5+
- canProcess(org.commonmark.node.Delimited):Z
6+
- processDelimitedInline(org.commonmark.node.Delimited,org.jetbrains.jewel.markdown.processing.MarkdownProcessor):org.jetbrains.jewel.markdown.InlineMarkdown$CustomDelimitedNode
7+
f:org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughInlineRendererExtension
8+
- org.jetbrains.jewel.markdown.extensions.MarkdownDelimitedInlineRendererExtension
9+
- sf:$stable:I
10+
- sf:INSTANCE:org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughInlineRendererExtension
11+
- canRender(org.jetbrains.jewel.markdown.InlineMarkdown$CustomDelimitedNode):Z
12+
- render(org.jetbrains.jewel.markdown.InlineMarkdown$CustomDelimitedNode,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer,org.jetbrains.jewel.markdown.rendering.InlinesStyling,Z,kotlin.jvm.functions.Function1):androidx.compose.ui.text.AnnotatedString
13+
f:org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughNode
14+
- org.jetbrains.jewel.markdown.InlineMarkdown$CustomDelimitedNode
15+
- org.jetbrains.jewel.markdown.WithInlineMarkdown
16+
- sf:$stable:I
17+
- <init>(java.lang.String,java.util.List):V
18+
- f:component1():java.lang.String
19+
- f:component2():java.util.List
20+
- f:copy(java.lang.String,java.util.List):org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughNode
21+
- bs:copy$default(org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughNode,java.lang.String,java.util.List,I,java.lang.Object):org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughNode
22+
- equals(java.lang.Object):Z
23+
- f:getDelimiter():java.lang.String
24+
- getInlineContent():java.util.List
25+
- getOpeningDelimiter():java.lang.String
26+
- hashCode():I
27+
f:org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughProcessorExtension
28+
- org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension
29+
- sf:$stable:I
30+
- <init>():V
31+
- <init>(Z):V
32+
- b:<init>(Z,I,kotlin.jvm.internal.DefaultConstructorMarker):V
33+
- getDelimitedInlineProcessorExtension():org.jetbrains.jewel.markdown.extensions.MarkdownDelimitedInlineProcessorExtension
34+
- getParserExtension():org.commonmark.parser.Parser$ParserExtension
35+
- getTextRendererExtension():org.commonmark.renderer.text.TextContentRenderer$TextContentRendererExtension
36+
f:org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughRendererExtension
37+
- org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension
38+
- sf:$stable:I
39+
- sf:INSTANCE:org.jetbrains.jewel.markdown.extensions.github.strikethrough.GitHubStrikethroughRendererExtension
40+
- getDelimitedInlineRenderer():org.jetbrains.jewel.markdown.extensions.MarkdownDelimitedInlineRendererExtension
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
public final class org/jetbrains/jewel/markdown/extensions/images/Coil3ImagesRendererExtension : org/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension {
2+
public static final field $stable I
3+
public static final field INSTANCE Lorg/jetbrains/jewel/markdown/extensions/images/Coil3ImagesRendererExtension;
4+
public fun getBlockRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension;
5+
public fun getDelimitedInlineRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownDelimitedInlineRendererExtension;
6+
public fun getImageRendererExtension ()Lorg/jetbrains/jewel/markdown/extensions/ImageRendererExtension;
7+
}
8+
9+
public final class org/jetbrains/jewel/markdown/extensions/images/Coil3ImagesRendererExtensionImpl : org/jetbrains/jewel/markdown/extensions/ImageRendererExtension {
10+
public static final field $stable I
11+
public static final field INSTANCE Lorg/jetbrains/jewel/markdown/extensions/images/Coil3ImagesRendererExtensionImpl;
12+
public fun imageContent (Lorg/jetbrains/jewel/markdown/InlineMarkdown$Image;Landroidx/compose/runtime/Composer;I)Landroidx/compose/foundation/text/InlineTextContent;
13+
}
14+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
plugins {
2+
jewel
3+
`jewel-publish`
4+
`jewel-check-public-api`
5+
alias(libs.plugins.composeDesktop)
6+
alias(libs.plugins.compose.compiler)
7+
}
8+
9+
dependencies {
10+
implementation(projects.markdown.core)
11+
runtimeOnly(libs.ktor.client.java)
12+
implementation(libs.coil.compose)
13+
implementation(libs.coil.network.ktor3)
14+
implementation(libs.coil.svg)
15+
testImplementation(compose.desktop.uiTestJUnit4)
16+
}
17+
18+
publishing.publications.named<MavenPublication>("main") {
19+
val ijpTarget = project.property("ijp.target") as String
20+
artifactId = "jewel-markdown-extension-${project.name}-$ijpTarget"
21+
}
22+
23+
publicApiValidation { excludedClassRegexes = setOf("org.jetbrains.jewel.markdown.extensions.github.strikethrough.*") }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
androidx/compose/**
2+
kotlin/jvm/internal/DefaultConstructorMarker
3+
org/commonmark/**

0 commit comments

Comments
 (0)