diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 645847c1e..63a601cf9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ poko = "0.15.3" [libraries] commonmark-core = { module = "org.commonmark:commonmark", version.ref = "commonmark" } commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" } +commonmark-ext-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" } filePicker = { module = "com.darkrockstudios:mpfilepicker", version = "3.1.0" } diff --git a/markdown/README.md b/markdown/README.md index c41644a86..bee6c7d63 100644 --- a/markdown/README.md +++ b/markdown/README.md @@ -12,8 +12,10 @@ Additional supported Markdown, via extensions: * Alerts ([GitHub Flavored Markdown][alerts-specs]) — see [`extension-gfm-alerts`](extension/gfm-alerts) * Autolink (standard CommonMark, but provided as extension) — see [`extension-autolink`](extension/autolink) +* Strikethrough ([GitHub Flavored Markdown][strikethrough-specs]) — see [`extension-strikethrough`](extension/strikethrough) [alerts-specs]: https://github.com/orgs/community/discussions/16925 +[strikethrough-specs]: https://github.github.com/gfm/#strikethrough-extension- On the roadmap, but not currently supported — in no particular order: diff --git a/markdown/core/api/core.api b/markdown/core/api/core.api index 14376ac74..14cb360ff 100644 --- a/markdown/core/api/core.api +++ b/markdown/core/api/core.api @@ -248,12 +248,12 @@ public abstract interface class org/jetbrains/jewel/markdown/extensions/Markdown public abstract interface class org/jetbrains/jewel/markdown/extensions/MarkdownInlineProcessorExtension { public abstract fun canProcess (Lorg/commonmark/node/CustomNode;)Z - public abstract fun processInlineMarkdown (Lorg/commonmark/node/CustomNode;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;)Lorg/jetbrains/jewel/markdown/InlineMarkdown$CustomNode; + public abstract fun processInlineMarkdown (Lorg/commonmark/node/CustomNode;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Ljava/util/List;)Lorg/jetbrains/jewel/markdown/InlineMarkdown$CustomNode; } public abstract interface class org/jetbrains/jewel/markdown/extensions/MarkdownInlineRendererExtension { public abstract fun canRender (Lorg/jetbrains/jewel/markdown/InlineMarkdown$CustomNode;)Z - public abstract fun render (Lorg/jetbrains/jewel/markdown/InlineMarkdown$CustomNode;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;Z)V + public abstract fun render (Landroidx/compose/ui/text/AnnotatedString$Builder;Lorg/jetbrains/jewel/markdown/InlineMarkdown$CustomNode;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;Z)V } public final class org/jetbrains/jewel/markdown/extensions/MarkdownKt { @@ -305,6 +305,10 @@ public final class org/jetbrains/jewel/markdown/processing/MarkdownProcessor { public final fun processMarkdownDocument (Ljava/lang/String;)Ljava/util/List; } +public final class org/jetbrains/jewel/markdown/processing/ProcessingUtilKt { + public static final fun toInlineMarkdownOrNull (Lorg/commonmark/node/Node;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Ljava/util/List;)Lorg/jetbrains/jewel/markdown/InlineMarkdown; +} + public class org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer : org/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer$Companion; diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownInlineProcessorExtension.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownInlineProcessorExtension.kt index 658671044..95898458a 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownInlineProcessorExtension.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownInlineProcessorExtension.kt @@ -17,5 +17,9 @@ public interface MarkdownInlineProcessorExtension { * [canProcess] returns true for the same [node], as implementations might throw an exception for unsupported node * types. */ - public fun processInlineMarkdown(node: CustomNode, processor: MarkdownProcessor): InlineMarkdown.CustomNode? + public fun processInlineMarkdown( + node: CustomNode, + markdownProcessor: MarkdownProcessor, + extensions: List, + ): InlineMarkdown.CustomNode? } diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownInlineRendererExtension.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownInlineRendererExtension.kt index 723d0a2c0..cbfcc2cff 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownInlineRendererExtension.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownInlineRendererExtension.kt @@ -1,5 +1,6 @@ package org.jetbrains.jewel.markdown.extensions +import androidx.compose.ui.text.AnnotatedString import org.jetbrains.jewel.markdown.InlineMarkdown import org.jetbrains.jewel.markdown.InlineMarkdown.CustomNode import org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer @@ -13,5 +14,10 @@ public interface MarkdownInlineRendererExtension { * Render a [CustomNode] as an annotated string. Note that if [canRender] returns `false` for [inline], the * implementation might throw. */ - public fun render(inline: CustomNode, inlineRenderer: InlineMarkdownRenderer, enabled: Boolean) + public fun render( + builder: AnnotatedString.Builder, + inline: CustomNode, + inlineRenderer: InlineMarkdownRenderer, + enabled: Boolean, + ) } diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/ProcessingUtil.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/ProcessingUtil.kt index b0f65dcfb..66d1ec243 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/ProcessingUtil.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/ProcessingUtil.kt @@ -32,10 +32,10 @@ internal fun Node.readInlineContent( } @VisibleForTesting -internal fun Node.toInlineMarkdownOrNull( +public fun Node.toInlineMarkdownOrNull( markdownProcessor: MarkdownProcessor, extensions: List, -) = +): InlineMarkdown? = when (this) { is CMText -> InlineMarkdown.Text(literal) is CMLink -> @@ -72,7 +72,7 @@ internal fun Node.toInlineMarkdownOrNull( extensions .find { it.inlineProcessorExtension?.canProcess(this) == true } ?.inlineProcessorExtension - ?.processInlineMarkdown(this, markdownProcessor) + ?.processInlineMarkdown(this, markdownProcessor, extensions) else -> error("Unexpected block $this") } diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer.kt index 60fc29271..651493927 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer.kt @@ -90,11 +90,12 @@ public open class DefaultInlineMarkdownRenderer(private val rendererExtensions: ) } - is InlineMarkdown.CustomNode -> + is InlineMarkdown.CustomNode -> { rendererExtensions .find { it.inlineRenderer?.canRender(child) == true } ?.inlineRenderer - ?.render(child, inlineRenderer = this@DefaultInlineMarkdownRenderer, enabled) + ?.render(builder = this, child, inlineRenderer = this@DefaultInlineMarkdownRenderer, enabled) + } } } } diff --git a/markdown/extension/strikethrough/api/strikethrough.api b/markdown/extension/strikethrough/api/strikethrough.api new file mode 100644 index 000000000..99ea91a03 --- /dev/null +++ b/markdown/extension/strikethrough/api/strikethrough.api @@ -0,0 +1,29 @@ +public final class org/jetbrains/jewel/markdown/extension/strikethrough/CustomStrikethroughNode : org/jetbrains/jewel/markdown/InlineMarkdown$CustomNode, org/jetbrains/jewel/markdown/WithInlineMarkdown { + public static final field $stable I + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V + public fun (Lorg/commonmark/ext/gfm/strikethrough/Strikethrough;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Ljava/util/List;)V + public fun contentOrNull ()Ljava/lang/String; + public fun equals (Ljava/lang/Object;)Z + public final fun getClosingDelimiter ()Ljava/lang/String; + public fun getInlineContent ()Ljava/util/List; + public final fun getOpeningDelimiter ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughProcessorExtension : org/jetbrains/jewel/markdown/extensions/MarkdownProcessorExtension { + public static final field $stable I + public static final field INSTANCE Lorg/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughProcessorExtension; + public fun getBlockProcessorExtension ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockProcessorExtension; + public fun getInlineProcessorExtension ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownInlineProcessorExtension; + public fun getParserExtension ()Lorg/commonmark/parser/Parser$ParserExtension; + public fun getTextRendererExtension ()Lorg/commonmark/renderer/text/TextContentRenderer$TextContentRendererExtension; +} + +public final class org/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughRendererExtension : org/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension { + public static final field $stable I + public fun (Lorg/jetbrains/jewel/markdown/rendering/InlinesStyling;)V + public fun getBlockRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension; + public fun getInlineRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownInlineRendererExtension; +} + diff --git a/markdown/extension/strikethrough/build.gradle.kts b/markdown/extension/strikethrough/build.gradle.kts new file mode 100644 index 000000000..3b9482557 --- /dev/null +++ b/markdown/extension/strikethrough/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + jewel + `jewel-publish` + `jewel-check-public-api` + alias(libs.plugins.composeDesktop) +} + +dependencies { + implementation(projects.markdown.core) + implementation(libs.commonmark.ext.strikethrough) + testImplementation(compose.desktop.uiTestJUnit4) +} + +publishing.publications.named("main") { + val ijpTarget = project.property("ijp.target") as String + artifactId = "jewel-markdown-extension-${project.name}-$ijpTarget" +} diff --git a/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/CustomStrikethroughNode.kt b/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/CustomStrikethroughNode.kt new file mode 100644 index 000000000..d9a5220c2 --- /dev/null +++ b/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/CustomStrikethroughNode.kt @@ -0,0 +1,36 @@ +package org.jetbrains.jewel.markdown.extension.strikethrough + +import org.commonmark.ext.gfm.strikethrough.Strikethrough +import org.commonmark.node.Node +import org.jetbrains.jewel.foundation.GenerateDataFunctions +import org.jetbrains.jewel.markdown.InlineMarkdown +import org.jetbrains.jewel.markdown.WithInlineMarkdown +import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension +import org.jetbrains.jewel.markdown.processing.MarkdownProcessor +import org.jetbrains.jewel.markdown.processing.toInlineMarkdownOrNull + +@GenerateDataFunctions +public class CustomStrikethroughNode( + public val openingDelimiter: String, + public val closingDelimiter: String, + public override val inlineContent: List, +) : InlineMarkdown.CustomNode, WithInlineMarkdown { + public constructor( + node: Strikethrough, + markdownProcessor: MarkdownProcessor, + extensions: List, + ) : this( + node.openingDelimiter, + node.closingDelimiter, + node.children().mapNotNull { it.toInlineMarkdownOrNull(markdownProcessor, extensions) }, + ) +} + +private fun Node.children() = + buildList { + var current = this@children.firstChild + while (current != null) { + this.add(current) + current = current.next + } + } diff --git a/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughProcessorExtension.kt b/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughProcessorExtension.kt new file mode 100644 index 000000000..082adade3 --- /dev/null +++ b/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughProcessorExtension.kt @@ -0,0 +1,27 @@ +package org.jetbrains.jewel.markdown.extension.strikethrough + +import org.commonmark.ext.gfm.strikethrough.Strikethrough +import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension +import org.commonmark.node.CustomNode +import org.commonmark.parser.Parser.ParserExtension +import org.jetbrains.jewel.markdown.InlineMarkdown +import org.jetbrains.jewel.markdown.extensions.MarkdownInlineProcessorExtension +import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension +import org.jetbrains.jewel.markdown.processing.MarkdownProcessor + +public object StrikethroughProcessorExtension : MarkdownProcessorExtension { + override val parserExtension: ParserExtension = StrikethroughExtension.create() as ParserExtension + + override val inlineProcessorExtension: MarkdownInlineProcessorExtension + get() = + object : MarkdownInlineProcessorExtension { + override fun canProcess(node: CustomNode): Boolean = node is Strikethrough + + override fun processInlineMarkdown( + node: CustomNode, + markdownProcessor: MarkdownProcessor, + extensions: List, + ): InlineMarkdown.CustomNode = + CustomStrikethroughNode(node as Strikethrough, markdownProcessor, extensions) + } +} diff --git a/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughRendererExtension.kt b/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughRendererExtension.kt new file mode 100644 index 000000000..cc3f597d7 --- /dev/null +++ b/markdown/extension/strikethrough/src/main/kotlin/org/jetbrains/jewel/markdown/extension/strikethrough/StrikethroughRendererExtension.kt @@ -0,0 +1,36 @@ +package org.jetbrains.jewel.markdown.extension.strikethrough + +import androidx.compose.ui.text.AnnotatedString.Builder +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import org.jetbrains.jewel.markdown.InlineMarkdown +import org.jetbrains.jewel.markdown.WithInlineMarkdown +import org.jetbrains.jewel.markdown.extensions.MarkdownInlineRendererExtension +import org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension +import org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer +import org.jetbrains.jewel.markdown.rendering.InlinesStyling + +public class StrikethroughRendererExtension(private val inlinesStyling: InlinesStyling) : MarkdownRendererExtension { + override val inlineRenderer: MarkdownInlineRendererExtension + get() = + object : MarkdownInlineRendererExtension { + override fun canRender(inline: InlineMarkdown.CustomNode): Boolean = inline is CustomStrikethroughNode + + override fun render( + builder: Builder, + inline: InlineMarkdown.CustomNode, + inlineRenderer: InlineMarkdownRenderer, + enabled: Boolean, + ) { + val style = inlinesStyling.textStyle.copy(textDecoration = TextDecoration.LineThrough).toSpanStyle() + builder.withStyle(style) { + val inlineMarkdowns = (inline as? WithInlineMarkdown)?.inlineContent ?: emptyList() + for (markdown in inlineMarkdowns) { + append( + inlineRenderer.renderAsAnnotatedString(listOf(markdown), inlinesStyling, enabled = true) + ) + } + } + } + } +} diff --git a/markdown/extension/strikethrough/src/test/kotlin/org/jetbrains/jewel/markdown/extension/autolink/StrikethroughProcessorExtensionTest.kt b/markdown/extension/strikethrough/src/test/kotlin/org/jetbrains/jewel/markdown/extension/autolink/StrikethroughProcessorExtensionTest.kt new file mode 100644 index 000000000..55cf65faf --- /dev/null +++ b/markdown/extension/strikethrough/src/test/kotlin/org/jetbrains/jewel/markdown/extension/autolink/StrikethroughProcessorExtensionTest.kt @@ -0,0 +1,20 @@ +package org.jetbrains.jewel.markdown.extension.autolink + +import org.jetbrains.jewel.markdown.MarkdownBlock +import org.jetbrains.jewel.markdown.extension.strikethrough.CustomStrikethroughNode +import org.jetbrains.jewel.markdown.extension.strikethrough.StrikethroughProcessorExtension +import org.jetbrains.jewel.markdown.processing.MarkdownProcessor +import org.junit.Assert.assertTrue +import org.junit.Test + +class StrikethroughProcessorExtensionTest { + // testing a simple case to assure wiring up our StrikethroughProcessorExtension works correctly + @Test + fun `~text~ processed into strikethrough`() { + val processor = MarkdownProcessor(listOf(StrikethroughProcessorExtension)) + val rawMarkDown = "~~text~~" + val processed = processor.processMarkdownDocument(rawMarkDown) + val paragraph = processed.first() as MarkdownBlock.Paragraph + assertTrue(paragraph.inlineContent.first() is CustomStrikethroughNode) + } +} diff --git a/samples/standalone/build.gradle.kts b/samples/standalone/build.gradle.kts index 9ef9a4abe..3fcbb591a 100644 --- a/samples/standalone/build.gradle.kts +++ b/samples/standalone/build.gradle.kts @@ -13,8 +13,9 @@ dependencies { implementation(projects.intUi.intUiStandalone) implementation(projects.intUi.intUiDecoratedWindow) implementation(projects.markdown.intUiStandaloneStyling) - implementation(projects.markdown.extension.gfmAlerts) implementation(projects.markdown.extension.autolink) + implementation(projects.markdown.extension.gfmAlerts) + implementation(projects.markdown.extension.strikethrough) implementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") } implementation(compose.components.resources) implementation(libs.intellijPlatform.icons) diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownPreview.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownPreview.kt index 40a796138..1ad9e24b9 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownPreview.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownPreview.kt @@ -28,6 +28,8 @@ import org.jetbrains.jewel.intui.markdown.standalone.styling.light import org.jetbrains.jewel.markdown.LazyMarkdown import org.jetbrains.jewel.markdown.MarkdownBlock import org.jetbrains.jewel.markdown.extension.autolink.AutolinkProcessorExtension +import org.jetbrains.jewel.markdown.extension.strikethrough.StrikethroughProcessorExtension +import org.jetbrains.jewel.markdown.extension.strikethrough.StrikethroughRendererExtension import org.jetbrains.jewel.markdown.extensions.github.alerts.AlertStyling import org.jetbrains.jewel.markdown.extensions.github.alerts.GitHubAlertProcessorExtension import org.jetbrains.jewel.markdown.extensions.github.alerts.GitHubAlertRendererExtension @@ -44,7 +46,9 @@ internal fun MarkdownPreview(modifier: Modifier = Modifier, rawMarkdown: CharSeq val markdownStyling = remember(isDark) { if (isDark) MarkdownStyling.dark() else MarkdownStyling.light() } var markdownBlocks by remember { mutableStateOf(emptyList()) } - val extensions = remember { listOf(GitHubAlertProcessorExtension, AutolinkProcessorExtension) } + val extensions = remember { + listOf(GitHubAlertProcessorExtension, AutolinkProcessorExtension, StrikethroughProcessorExtension) + } // We are doing this here for the sake of simplicity. // In a real-world scenario you would be doing this outside your Composables, @@ -64,12 +68,20 @@ internal fun MarkdownPreview(modifier: Modifier = Modifier, rawMarkdown: CharSeq if (isDark) { MarkdownBlockRenderer.dark( styling = markdownStyling, - rendererExtensions = listOf(GitHubAlertRendererExtension(AlertStyling.dark(), markdownStyling)), + rendererExtensions = + listOf( + GitHubAlertRendererExtension(AlertStyling.dark(), markdownStyling), + StrikethroughRendererExtension(markdownStyling.paragraph.inlinesStyling), + ), ) } else { MarkdownBlockRenderer.light( styling = markdownStyling, - rendererExtensions = listOf(GitHubAlertRendererExtension(AlertStyling.light(), markdownStyling)), + rendererExtensions = + listOf( + GitHubAlertRendererExtension(AlertStyling.light(), markdownStyling), + StrikethroughRendererExtension(markdownStyling.paragraph.inlinesStyling), + ), ) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 608a13983..ad95660c7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,7 @@ include( ":markdown:core", ":markdown:extension:autolink", ":markdown:extension:gfm-alerts", + ":markdown:extension:strikethrough", ":markdown:int-ui-standalone-styling", ":markdown:ide-laf-bridge-styling", ":samples:ide-plugin",