Skip to content

Commit

Permalink
Add strikethrough markdown support
Browse files Browse the repository at this point in the history
  • Loading branch information
obask committed Oct 11, 2024
1 parent deffc2b commit 5859872
Show file tree
Hide file tree
Showing 16 changed files with 210 additions and 13 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }

Expand Down
2 changes: 2 additions & 0 deletions markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
8 changes: 6 additions & 2 deletions markdown/core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MarkdownProcessorExtension>,
): InlineMarkdown.CustomNode?
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ internal fun Node.readInlineContent(
}

@VisibleForTesting
internal fun Node.toInlineMarkdownOrNull(
public fun Node.toInlineMarkdownOrNull(
markdownProcessor: MarkdownProcessor,
extensions: List<MarkdownProcessorExtension>,
) =
): InlineMarkdown? =
when (this) {
is CMText -> InlineMarkdown.Text(literal)
is CMLink ->
Expand Down Expand Up @@ -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")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions markdown/extension/strikethrough/api/strikethrough.api
Original file line number Diff line number Diff line change
@@ -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 <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
public fun <init> (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 <init> (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;
}

17 changes: 17 additions & 0 deletions markdown/extension/strikethrough/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<MavenPublication>("main") {
val ijpTarget = project.property("ijp.target") as String
artifactId = "jewel-markdown-extension-${project.name}-$ijpTarget"
}
Original file line number Diff line number Diff line change
@@ -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>,
) : InlineMarkdown.CustomNode, WithInlineMarkdown {
public constructor(
node: Strikethrough,
markdownProcessor: MarkdownProcessor,
extensions: List<MarkdownProcessorExtension>,
) : this(
node.openingDelimiter,
node.closingDelimiter,
node.children().mapNotNull { it.toInlineMarkdownOrNull(markdownProcessor, extensions) },
)
}

private fun Node.children() =
buildList<Node> {
var current = this@children.firstChild
while (current != null) {
this.add(current)
current = current.next
}
}
Original file line number Diff line number Diff line change
@@ -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<MarkdownProcessorExtension>,
): InlineMarkdown.CustomNode =
CustomStrikethroughNode(node as Strikethrough, markdownProcessor, extensions)
}
}
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 2 additions & 1 deletion samples/standalone/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MarkdownBlock>()) }
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,
Expand All @@ -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),
),
)
}
}
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 5859872

Please sign in to comment.