From 70df5ff46e1df3df7007c894d6d2af1876017e16 Mon Sep 17 00:00:00 2001 From: Jacob Logsdon Date: Thu, 28 Mar 2024 17:17:20 -0700 Subject: [PATCH] Add an autolink extension for Markdown Exposes the autolink extension offered by the commonmark java library, to markdown rendering in Compose. Since links are already supported for processing and rendering, we only need to hookup the parserExtension to get this to work. --- gradle/libs.versions.toml | 1 + .../extensions/MarkdownProcessorExtension.kt | 17 +++++++++------- .../markdown/processing/MarkdownProcessor.kt | 2 +- markdown/extension/autolink/api/autolink.api | 8 ++++++++ markdown/extension/autolink/build.gradle.kts | 17 ++++++++++++++++ .../autolink/AutolinkProcessorExtension.kt | 20 +++++++++++++++++++ .../AutolinkProcessorExtensionTest.kt | 20 +++++++++++++++++++ samples/standalone/build.gradle.kts | 2 +- .../view/markdown/MarkdownPreview.kt | 8 ++++++-- settings.gradle.kts | 1 + 10 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 markdown/extension/autolink/api/autolink.api create mode 100644 markdown/extension/autolink/build.gradle.kts create mode 100644 markdown/extension/autolink/src/main/kotlin/org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtension.kt create mode 100644 markdown/extension/autolink/src/test/kotlin/org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtensionTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 56b05fe540..5b013282bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ poko = "0.13.1" [libraries] commonmark-core = { module = "org.commonmark:commonmark", version.ref = "commonmark" } +commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" } filePicker = { module = "com.darkrockstudios:mpfilepicker", version = "3.1.0" } diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownProcessorExtension.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownProcessorExtension.kt index f484bf8832..039f5c8952 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownProcessorExtension.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/MarkdownProcessorExtension.kt @@ -9,25 +9,28 @@ import org.jetbrains.jewel.markdown.MarkdownBlock /** An extension for the Jewel Markdown processing engine. */ @ExperimentalJewelApi public interface MarkdownProcessorExtension { - /** * A CommonMark [ParserExtension] that will be used to parse the extended - * syntax represented by this extension instance. + * syntax represented by this extension instance. Null in the case where + * parsing is already handled by an existing [org.commonmark.parser.Parser]. */ - public val parserExtension: ParserExtension + public val parserExtension: ParserExtension? /** * A CommonMark [TextContentRendererExtension] that will be used to render * the text content of the CommonMark [CustomBlock] produced by the - * [parserExtension]. + * [parserExtension]. Null in the case where rendering is already + * handled by an existing [org.commonmark.renderer.Renderer]. */ - public val textRendererExtension: TextContentRendererExtension + public val textRendererExtension: TextContentRendererExtension? /** * An extension for * [`MarkdownParser`][org.jetbrains.jewel.markdown.parsing.MarkdownParser] * that will transform a supported [CustomBlock] into the corresponding - * [MarkdownBlock.CustomBlock]. + * [MarkdownBlock.CustomBlock]. Null in the case where processing + * is already be handled by [org.jetbrains.jewel.markdown.processing.MarkdownProcessor] + * or another [org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension]. */ - public val processorExtension: MarkdownBlockProcessorExtension + public val processorExtension: MarkdownBlockProcessorExtension? } 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 626920173f..a30750bc43 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 @@ -189,7 +189,7 @@ public class MarkdownProcessor( is ThematicBreak -> MarkdownBlock.ThematicBreak is HtmlBlock -> toMarkdownHtmlBlockOrNull() is CustomBlock -> { - extensions.find { it.processorExtension.canProcess(this) } + extensions.find { it.processorExtension?.canProcess(this) == true } ?.processorExtension?.processMarkdownBlock(this, this@MarkdownProcessor) } else -> null diff --git a/markdown/extension/autolink/api/autolink.api b/markdown/extension/autolink/api/autolink.api new file mode 100644 index 0000000000..76809e0ee5 --- /dev/null +++ b/markdown/extension/autolink/api/autolink.api @@ -0,0 +1,8 @@ +public final class org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtension : org/jetbrains/jewel/markdown/extensions/MarkdownProcessorExtension { + public static final field $stable I + public fun ()V + public fun getParserExtension ()Lorg/commonmark/parser/Parser$ParserExtension; + public fun getProcessorExtension ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockProcessorExtension; + public fun getTextRendererExtension ()Lorg/commonmark/renderer/text/TextContentRenderer$TextContentRendererExtension; +} + diff --git a/markdown/extension/autolink/build.gradle.kts b/markdown/extension/autolink/build.gradle.kts new file mode 100644 index 0000000000..052dbcea83 --- /dev/null +++ b/markdown/extension/autolink/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.autolink) + 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/autolink/src/main/kotlin/org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtension.kt b/markdown/extension/autolink/src/main/kotlin/org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtension.kt new file mode 100644 index 0000000000..ad8161deaf --- /dev/null +++ b/markdown/extension/autolink/src/main/kotlin/org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtension.kt @@ -0,0 +1,20 @@ +package org.jetbrains.jewel.markdown.extension.autolink + +import org.commonmark.ext.autolink.AutolinkExtension +import org.commonmark.parser.Parser.ParserExtension +import org.commonmark.renderer.text.TextContentRenderer +import org.jetbrains.jewel.markdown.extensions.MarkdownBlockProcessorExtension +import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension + +public object AutolinkProcessorExtension : MarkdownProcessorExtension { + override val parserExtension: ParserExtension + get() = AutolinkExtension.create() as ParserExtension + + /** + * Rendering and processing is already handled by [org.jetbrains.jewel.markdown.rendering.DefaultInlineMarkdownRenderer] + */ + override val textRendererExtension: TextContentRenderer.TextContentRendererExtension? + get() = null + override val processorExtension: MarkdownBlockProcessorExtension? + get() = null +} diff --git a/markdown/extension/autolink/src/test/kotlin/org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtensionTest.kt b/markdown/extension/autolink/src/test/kotlin/org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtensionTest.kt new file mode 100644 index 0000000000..8e14e7aa5f --- /dev/null +++ b/markdown/extension/autolink/src/test/kotlin/org/jetbrains/jewel/markdown/extension/autolink/AutolinkProcessorExtensionTest.kt @@ -0,0 +1,20 @@ +package org.jetbrains.jewel.markdown.extension.autolink + +import org.jetbrains.jewel.markdown.InlineMarkdown +import org.jetbrains.jewel.markdown.MarkdownBlock +import org.jetbrains.jewel.markdown.processing.MarkdownProcessor +import org.junit.Assert.assertTrue +import org.junit.Test + +class AutolinkProcessorExtensionTest { + // testing a simple case to assure wiring up our AutolinkProcessorExtension works correctly + @Test + fun `https text gets processed into a link`() { + val processor = MarkdownProcessor(listOf(AutolinkProcessorExtension)) + val rawMarkDown = "https://commonmark.org" + val processed = processor.processMarkdownDocument(rawMarkDown) + val paragraph = processed[0] as MarkdownBlock.Paragraph + + assertTrue(paragraph.inlineContent[0] is InlineMarkdown.Link) + } +} diff --git a/samples/standalone/build.gradle.kts b/samples/standalone/build.gradle.kts index 6cff7ddff0..2d8714780d 100644 --- a/samples/standalone/build.gradle.kts +++ b/samples/standalone/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementation(projects.intUi.intUiDecoratedWindow) implementation(projects.markdown.intUiStandaloneStyling) implementation(projects.markdown.extension.gfmAlerts) - + implementation(projects.markdown.extension.autolink) implementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") } 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 8baa7ed879..f3f6aba6e2 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 @@ -31,6 +31,7 @@ import org.jetbrains.jewel.intui.markdown.styling.extension.github.alerts.dark import org.jetbrains.jewel.intui.markdown.styling.extension.github.alerts.light import org.jetbrains.jewel.intui.markdown.styling.light import org.jetbrains.jewel.markdown.MarkdownBlock +import org.jetbrains.jewel.markdown.extension.autolink.AutolinkProcessorExtension 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 @@ -43,14 +44,17 @@ import java.awt.Desktop import java.net.URI @Composable -internal fun MarkdownPreview(rawMarkdown: String, modifier: Modifier = Modifier) { +internal fun MarkdownPreview( + rawMarkdown: String, + modifier: Modifier = Modifier, +) { val isDark = JewelTheme.isDark val markdownStyling = remember(isDark) { if (isDark) MarkdownStyling.dark() else MarkdownStyling.light() } var markdownBlocks by remember { mutableStateOf(emptyList()) } - val extensions = listOf(GitHubAlertProcessorExtension) + val extensions = listOf(GitHubAlertProcessorExtension, AutolinkProcessorExtension) val processor = remember { MarkdownProcessor(extensions) } LaunchedEffect(rawMarkdown) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 7d7e33b089..69a2b6a3e4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,7 @@ include( ":int-ui:int-ui-decorated-window", ":int-ui:int-ui-standalone", ":markdown:core", + ":markdown:extension:autolink", ":markdown:extension:gfm-alerts", ":markdown:int-ui-standalone-styling", ":markdown:ide-laf-bridge-styling",