diff --git a/android-sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt b/android-sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt index b185d380..37fabea1 100644 --- a/android-sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt +++ b/android-sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em -import com.halilibo.richtext.markdown.Markdown -import com.halilibo.richtext.markdown.MarkdownParseOptions +import com.halilibo.richtext.commonmark.Markdown +import com.halilibo.richtext.commonmark.MarkdownParseOptions import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.material.RichText import com.halilibo.richtext.ui.resolveDefaults diff --git a/desktop-sample/build.gradle.kts b/desktop-sample/build.gradle.kts index 6b424ea9..4f5b0b6c 100644 --- a/desktop-sample/build.gradle.kts +++ b/desktop-sample/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.compose.compose import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { diff --git a/desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/Main.kt b/desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/Main.kt index 58b649fb..1594bebe 100644 --- a/desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/Main.kt +++ b/desktop-sample/src/main/kotlin/com/halilibo/richtext/desktop/Main.kt @@ -27,12 +27,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import androidx.compose.ui.window.singleWindowApplication -import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.commonmark.Markdown import com.halilibo.richtext.ui.CodeBlockStyle import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.material.RichText diff --git a/docs/index.md b/docs/index.md index ff479660..896282d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ Compose Richtext is a collection of Compose libraries for working with rich text formatting and documents. -`richtext-ui`, `richtext-commonmark`, and `richtext-ui-material`|`richtext-ui-material3` are Kotlin Multiplatform(KMP) Compose Libraries. +`richtext-ui`, `richtext-markdown`, `richtext-commonmark`, and `richtext-ui-material`|`richtext-ui-material3` are Kotlin Multiplatform(KMP) Compose Libraries. All these modules can be used in Android and Desktop Compose apps. Each library is documented separately, see the navigation menu for the list. This site also includes diff --git a/docs/richtext-commonmark.md b/docs/richtext-commonmark.md index c8814dec..082f4557 100644 --- a/docs/richtext-commonmark.md +++ b/docs/richtext-commonmark.md @@ -3,7 +3,7 @@ [![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies) [![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html) -Library for rendering Markdown in Compose using [CommonMark](https://github.com/commonmark/commonmark-java) +Library for parsing and rendering Markdown in Compose using [CommonMark](https://github.com/commonmark/commonmark-java) library/spec to parse, and `richtext-ui` to render. ## Gradle @@ -14,7 +14,37 @@ dependencies { } ``` -## Usage +## Parsing + +`richtext-markdown` module renders a given Markdown Abstract Syntax Tree. It accepts a root +`AstNode`. This library gives you a parser called `CommonmarkAstNodeParser` to easily convert any +String to an `AstNode` that represents the Markdown tree. + +```kotlin + val parser = CommonmarkAstNodeParser() + val astNode = parser.parse( + """ + # Demo + + Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. [Links with two blocks, text in square-brackets, destination is in parentheses.](https://www.example.com). Inline `code` has `back-ticks around` it. + + 1. First ordered list item + 2. Another item + * Unordered sub-list. + 3. And another item. + You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). + + * Unordered list can use asterisks + - Or minuses + + Or pluses + """.trimIndent() + ) + // ... + + RichTextScope.Markdown(astNode) +``` + +## Rendering The simplest way to render markdown is just pass a string to the [`Markdown`](../api/richtext-commonmark/com.halilibo.richtext.markdown/-markdown.html) composable under RichText scope: @@ -61,12 +91,12 @@ Which produces something like this: ![markdown demo](img/markdown-demo.png) -## MarkdownParseOptions +## [`MarkdownParseOptions`](../api/richtext-commonmark/com.halilibo.richtext.commonmark/-markdown-parse-options.html) -Passing `MarkdownParseOptions` into `Markdown` provides the ability to control some aspects of the markdown parser: +Passing `MarkdownParseOptions` into either `Markdown` composable or `CommonmarkAstNodeParser.parse` method provides the ability to control some aspects of the markdown parser: ```kotlin -val markdownParseOptions = MarkdownParseOptions.Default.copy( +val markdownParseOptions = MarkdownParseOptions( autolink = false ) diff --git a/docs/richtext-markdown.md b/docs/richtext-markdown.md new file mode 100644 index 00000000..e77eb84d --- /dev/null +++ b/docs/richtext-markdown.md @@ -0,0 +1,54 @@ +# Markdown + +[![Android Library](https://img.shields.io/badge/Platform-Android-green.svg?style=for-the-badge)](https://developer.android.com/studio/build/dependencies) +[![JVM Library](https://img.shields.io/badge/Platform-JVM-red.svg?style=for-the-badge)](https://kotlinlang.org/docs/mpp-intro.html) + +Library for rendering Markdown tree defined as an `AstNode`. + +## Gradle + +```kotlin +dependencies { + implementation("com.halilibo.compose-richtext:richtext-markdown:${richtext_version}") +} +``` + +## Usage + +`richtext-markdown` module renders a given Markdown Abstract Syntax Tree. It accepts a root +`AstNode`. However, this library does not include a parser to convert a regular Markdown String into +an `AstNode`. Please refer to `richtext-commonmark` for a sample implementation or a quick way to +render Markdown. + +## Rendering + +The simplest way to render markdown is just pass an `AstNode` to the [`Markdown`](../api/richtext-commonmark/com.halilibo.richtext.markdown/-markdown.html) +composable under RichText scope: + +~~~kotlin +RichText( + modifier = Modifier.padding(16.dp) +) { + val parser = remember(options) { CommonmarkAstNodeParser(options) } + val astNode = remember(parser) { + parser.parse( + """ + # Demo + + Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. [Links with two blocks, text in square-brackets, destination is in parentheses.](https://www.example.com). Inline `code` has `back-ticks around` it. + + 1. First ordered list item + 2. Another item + * Unordered sub-list. + 3. And another item. + You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). + + * Unordered list can use asterisks + - Or minuses + + Or pluses + """.trimIndent() + ) + } + Markdown(astNode) +} +~~~ diff --git a/mkdocs.yml b/mkdocs.yml index db4d2808..9a33b31f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,6 +29,7 @@ nav: - richtext-ui-material.md - richtext-ui-material3.md - richtext-ui.md + - richtext-markdown.md - richtext-commonmark.md - printing.md - slideshow.md diff --git a/richtext-commonmark/build.gradle.kts b/richtext-commonmark/build.gradle.kts index 33d6a5a1..44cd6e9f 100644 --- a/richtext-commonmark/build.gradle.kts +++ b/richtext-commonmark/build.gradle.kts @@ -9,7 +9,7 @@ repositories { } android { - namespace = "com.halilibo.richtext.markdown" + namespace = "com.halilibo.richtext.commonmark" } kotlin { @@ -17,8 +17,8 @@ kotlin { val commonMain by getting { dependencies { implementation(compose.runtime) - implementation(compose.foundation) api(project(":richtext-ui")) + api(project(":richtext-markdown")) } } val commonTest by getting @@ -26,8 +26,6 @@ kotlin { val androidMain by getting { kotlin.srcDir("src/commonJvmAndroid/kotlin") dependencies { - implementation(Compose.coil) - implementation(Commonmark.core) implementation(Commonmark.tables) implementation(Commonmark.strikethrough) @@ -38,9 +36,6 @@ kotlin { val jvmMain by getting { kotlin.srcDir("src/commonJvmAndroid/kotlin") dependencies { - implementation(compose.desktop.currentOs) - implementation(Network.okHttp) - implementation(Commonmark.core) implementation(Commonmark.tables) implementation(Commonmark.strikethrough) diff --git a/richtext-commonmark/src/commonJvmAndroid/kotlin/com/halilibo/richtext/markdown/AstNodeConvert.kt b/richtext-commonmark/src/commonJvmAndroid/kotlin/com/halilibo/richtext/commonmark/AstNodeConvert.kt similarity index 87% rename from richtext-commonmark/src/commonJvmAndroid/kotlin/com/halilibo/richtext/markdown/AstNodeConvert.kt rename to richtext-commonmark/src/commonJvmAndroid/kotlin/com/halilibo/richtext/commonmark/AstNodeConvert.kt index a9f3fa0a..7877270b 100644 --- a/richtext-commonmark/src/commonJvmAndroid/kotlin/com/halilibo/richtext/markdown/AstNodeConvert.kt +++ b/richtext-commonmark/src/commonJvmAndroid/kotlin/com/halilibo/richtext/commonmark/AstNodeConvert.kt @@ -1,11 +1,6 @@ -package com.halilibo.richtext.markdown +package com.halilibo.richtext.commonmark -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember import com.halilibo.richtext.markdown.node.AstBlockQuote -import com.halilibo.richtext.markdown.node.AstBulletList import com.halilibo.richtext.markdown.node.AstCode import com.halilibo.richtext.markdown.node.AstDocument import com.halilibo.richtext.markdown.node.AstEmphasis @@ -35,6 +30,7 @@ import com.halilibo.richtext.markdown.node.AstTableRoot import com.halilibo.richtext.markdown.node.AstTableRow import com.halilibo.richtext.markdown.node.AstText import com.halilibo.richtext.markdown.node.AstThematicBreak +import com.halilibo.richtext.markdown.node.AstUnorderedList import org.commonmark.ext.autolink.AutolinkExtension import org.commonmark.ext.gfm.strikethrough.Strikethrough import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension @@ -90,7 +86,7 @@ internal fun convert( val newNodeType: AstNodeType? = when (node) { is BlockQuote -> AstBlockQuote - is BulletList -> AstBulletList(bulletMarker = node.bulletMarker) + is BulletList -> AstUnorderedList(bulletMarker = node.bulletMarker) is Code -> AstCode(literal = node.literal) is Document -> AstDocument is Emphasis -> AstEmphasis(delimiter = node.openingDelimiter) @@ -186,26 +182,30 @@ internal fun convert( return newNode } -internal fun Node.convert() = convert(this) +public actual class CommonmarkAstNodeParser actual constructor( + options: MarkdownParseOptions +) { -@Composable -internal actual fun parsedMarkdownAst(text: String, options: MarkdownParseOptions): AstNode? { - val parser = remember(options) { - Parser.builder() - .extensions( - listOfNotNull( - TablesExtension.create(), - StrikethroughExtension.create(), - if (options.autolink) AutolinkExtension.create() else null - ) + private val parser = Parser.builder() + .extensions( + listOfNotNull( + TablesExtension.create(), + StrikethroughExtension.create(), + if (options.autolink) AutolinkExtension.create() else null ) - .build() - } + ) + .build() - val astRootNode by produceState(null, text, parser) { - value = parser.parse(text).convert() - } + public actual fun parse(text: String): AstNode { + val commonmarkNode = parser.parse(text) + ?: throw IllegalArgumentException( + "Could not parse the given text content into a meaningful Markdown representation!" + ) - return astRootNode + return convert(commonmarkNode) + ?: throw IllegalArgumentException( + "Could not convert the generated Commonmark Node into an ASTNode!" + ) + } } diff --git a/richtext-commonmark/src/commonJvmAndroidTest/kotlin/com/halilibo/richtext/markdown/AstNodeConvertKtTest.kt b/richtext-commonmark/src/commonJvmAndroidTest/kotlin/com/halilibo/richtext/commonmark/AstNodeConvertKtTest.kt similarity index 93% rename from richtext-commonmark/src/commonJvmAndroidTest/kotlin/com/halilibo/richtext/markdown/AstNodeConvertKtTest.kt rename to richtext-commonmark/src/commonJvmAndroidTest/kotlin/com/halilibo/richtext/commonmark/AstNodeConvertKtTest.kt index 1917865f..9247b567 100644 --- a/richtext-commonmark/src/commonJvmAndroidTest/kotlin/com/halilibo/richtext/markdown/AstNodeConvertKtTest.kt +++ b/richtext-commonmark/src/commonJvmAndroidTest/kotlin/com/halilibo/richtext/commonmark/AstNodeConvertKtTest.kt @@ -1,5 +1,6 @@ package com.halilibo.richtext.markdown +import com.halilibo.richtext.commonmark.convert import com.halilibo.richtext.markdown.node.AstImage import com.halilibo.richtext.markdown.node.AstNode import com.halilibo.richtext.markdown.node.AstNodeLinks diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/Markdown.kt b/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/Markdown.kt new file mode 100644 index 00000000..5caeb695 --- /dev/null +++ b/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/Markdown.kt @@ -0,0 +1,52 @@ +package com.halilibo.richtext.commonmark + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.commonmark.MarkdownParseOptions.Companion +import com.halilibo.richtext.markdown.node.AstNode +import com.halilibo.richtext.ui.RichTextScope + +/** + * A composable that renders Markdown content according to Commonmark specification using RichText. + * + * @param content Markdown text. No restriction on length. + * @param markdownParseOptions Options for the Markdown parser. + * @param onLinkClicked A function to invoke when a link is clicked from rendered content. + */ +@Composable +public fun RichTextScope.Markdown( + content: String, + markdownParseOptions: MarkdownParseOptions = Companion.Default, + onLinkClicked: ((String) -> Unit)? = null +) { + val commonmarkAstNodeParser = remember(markdownParseOptions) { + CommonmarkAstNodeParser(markdownParseOptions) + } + + val astRootNode by produceState(null, commonmarkAstNodeParser) { + value = commonmarkAstNodeParser.parse(content) + } + + astRootNode?.let { + Markdown(astNode = it, onLinkClicked = onLinkClicked) + } +} + +/** + * A helper class that can convert any text content into an ASTNode tree and return its root. + */ +public expect class CommonmarkAstNodeParser( + options: MarkdownParseOptions = MarkdownParseOptions.Default +) { + + /** + * Parse markdown content and return Abstract Syntax Tree(AST). + * + * @param text Markdown text to be parsed. + * @param options Options for the Commonmark Markdown parser. + */ + public fun parse(text: String): AstNode +} \ No newline at end of file diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownParseOptions.kt b/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/MarkdownParseOptions.kt similarity index 89% rename from richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownParseOptions.kt rename to richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/MarkdownParseOptions.kt index c09af285..30756823 100644 --- a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownParseOptions.kt +++ b/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/commonmark/MarkdownParseOptions.kt @@ -1,4 +1,4 @@ -package com.halilibo.richtext.markdown +package com.halilibo.richtext.commonmark /** * Allows configuration of the Markdown parser diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNode.kt b/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNode.kt deleted file mode 100644 index c0a7a087..00000000 --- a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNode.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.halilibo.richtext.markdown.node - -/** - * Generic AstNode implementation that can define any node in Abstract Syntax Tree. - * - * @param type A sealed class which is categorized into block, container, and leaf nodes. - * @param links Pointers to parent, sibling, child nodes. - */ -internal data class AstNode( - val type: AstNodeType, - val links: AstNodeLinks -) diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeLinks.kt b/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeLinks.kt deleted file mode 100644 index 3c23665a..00000000 --- a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeLinks.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.halilibo.richtext.markdown.node - -import androidx.compose.runtime.Immutable - -/** - * All the pointers that can exist for a node in an AST. - */ -@Immutable -internal data class AstNodeLinks( - var parent: AstNode? = null, - var firstChild: AstNode? = null, - var lastChild: AstNode? = null, - var previous: AstNode? = null, - var next: AstNode? = null -) { - - /** - * Stop infinite loop and only check towards bottom-right direction - */ - override fun equals(other: Any?): Boolean { - if (other !is AstNodeLinks) return false - - return firstChild == other.firstChild && next == other.next - } - - /** - * Stop infinite loop and only calculate towards bottom-right direction - */ - override fun hashCode(): Int { - return (firstChild ?: 0).hashCode() * 11 + (next ?: 0).hashCode() * 7 - } -} \ No newline at end of file diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstStrikethrough.kt b/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstStrikethrough.kt deleted file mode 100644 index 1b9a2d3d..00000000 --- a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstStrikethrough.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.halilibo.richtext.markdown.node - -import androidx.compose.runtime.Immutable - -@Immutable -internal data class AstStrikethrough( - val delimiter: String -) : AstInlineNodeType() diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstTable.kt b/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstTable.kt deleted file mode 100644 index 90d08a7e..00000000 --- a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstTable.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.halilibo.richtext.markdown.node - -import androidx.compose.runtime.Immutable - -@Immutable -internal object AstTableRoot: AstContainerBlockNodeType() - -@Immutable -internal object AstTableBody: AstContainerBlockNodeType() - -@Immutable -internal object AstTableHeader: AstContainerBlockNodeType() - -@Immutable -internal object AstTableRow: AstContainerBlockNodeType() - -@Immutable -internal data class AstTableCell( - val header: Boolean, - val alignment: AstTableCellAlignment -) : AstContainerBlockNodeType() - -internal enum class AstTableCellAlignment { - LEFT, - CENTER, - RIGHT -} diff --git a/richtext-markdown/build.gradle.kts b/richtext-markdown/build.gradle.kts new file mode 100644 index 00000000..2a6d3412 --- /dev/null +++ b/richtext-markdown/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + id("richtext-kmp-library") + id("org.jetbrains.compose") version Compose.desktopVersion + id("org.jetbrains.dokka") +} + +repositories { + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") +} + +android { + namespace = "com.halilibo.richtext.markdown" +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + api(project(":richtext-ui")) + } + } + val commonTest by getting + + val androidMain by getting { + dependencies { + implementation(Compose.coil) + + implementation(Commonmark.core) + implementation(Commonmark.tables) + implementation(Commonmark.strikethrough) + implementation(Commonmark.autolink) + } + } + + val jvmMain by getting { + dependencies { + implementation(compose.desktop.currentOs) + implementation(Network.okHttp) + + implementation(Commonmark.core) + implementation(Commonmark.tables) + implementation(Commonmark.strikethrough) + implementation(Commonmark.autolink) + } + } + + val jvmTest by getting { + dependencies { + implementation(Kotlin.Test.jdk) + } + } + } +} diff --git a/richtext-markdown/gradle.properties b/richtext-markdown/gradle.properties new file mode 100644 index 00000000..572446d8 --- /dev/null +++ b/richtext-markdown/gradle.properties @@ -0,0 +1,2 @@ +POM_NAME=Compose Richtext Commonmark +POM_DESCRIPTION=A library for rendering markdown in Compose using the Commonmark library. \ No newline at end of file diff --git a/richtext-commonmark/src/androidMain/AndroidManifest.xml b/richtext-markdown/src/androidMain/AndroidManifest.xml similarity index 100% rename from richtext-commonmark/src/androidMain/AndroidManifest.xml rename to richtext-markdown/src/androidMain/AndroidManifest.xml diff --git a/richtext-commonmark/src/androidMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt b/richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt similarity index 100% rename from richtext-commonmark/src/androidMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt rename to richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt diff --git a/richtext-commonmark/src/androidMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt b/richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt similarity index 100% rename from richtext-commonmark/src/androidMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt rename to richtext-markdown/src/androidMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt similarity index 100% rename from richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt rename to richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/Markdown.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/Markdown.kt similarity index 88% rename from richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/Markdown.kt rename to richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/Markdown.kt index 4386cca7..5f244951 100644 --- a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/Markdown.kt +++ b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/Markdown.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import com.halilibo.richtext.markdown.node.AstBlockQuote -import com.halilibo.richtext.markdown.node.AstBulletList +import com.halilibo.richtext.markdown.node.AstUnorderedList import com.halilibo.richtext.markdown.node.AstDocument import com.halilibo.richtext.markdown.node.AstFencedCodeBlock import com.halilibo.richtext.markdown.node.AstHeading @@ -42,42 +42,27 @@ import com.halilibo.richtext.ui.string.Text import com.halilibo.richtext.ui.string.richTextString /** - * A composable that renders Markdown content using RichText. + * A composable that renders Markdown content pointed by [astNode] into this [RichTextScope]. * - * @param content Markdown text. No restriction on length. - * @param markdownParseOptions Options for the Markdown parser. + * @param astNode Root node of Markdown tree. This can be obtained via a parser. * @param onLinkClicked A function to invoke when a link is clicked from rendered content. */ @Composable public fun RichTextScope.Markdown( - content: String, - markdownParseOptions: MarkdownParseOptions = MarkdownParseOptions.Default, + astNode: AstNode, onLinkClicked: ((String) -> Unit)? = null ) { val onLinkClickedState = rememberUpdatedState(onLinkClicked) - // Can't use UriHandlerAmbient.current::openUri here, - // see https://issuetracker.google.com/issues/172366483 val realLinkClickedHandler = onLinkClickedState.value ?: LocalUriHandler.current.let { remember { { url -> it.openUri(url) } } } CompositionLocalProvider(LocalOnLinkClicked provides realLinkClickedHandler) { - val markdownAst = parsedMarkdownAst(text = content, options = markdownParseOptions) - RecursiveRenderMarkdownAst(astNode = markdownAst) + RecursiveRenderMarkdownAst(astNode = astNode) } } -/** - * Parse markdown content and return Abstract Syntax Tree(AST). - * Composable is efficient thanks to remember construct. - * - * @param text Markdown text to be parsed. - * @param options Options for the Markdown parser. - */ -@Composable -internal expect fun parsedMarkdownAst(text: String, options: MarkdownParseOptions): AstNode? - /** * When parsed, markdown content or any other rich text can be represented as a tree. * The default markdown parser that is used in this project `common-markdown` also @@ -115,7 +100,7 @@ internal fun RichTextScope.RecursiveRenderMarkdownAst(astNode: AstNode?) { visitChildren(astNode) } } - is AstBulletList -> { + is AstUnorderedList -> { FormattedList( listType = Unordered, items = astNode.filterChildrenType().toList() diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownRichText.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownRichText.kt similarity index 100% rename from richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownRichText.kt rename to richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/MarkdownRichText.kt diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt similarity index 100% rename from richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt rename to richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/RenderTable.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/RenderTable.kt similarity index 100% rename from richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/RenderTable.kt rename to richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/RenderTable.kt diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/TraverseUtils.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/TraverseUtils.kt similarity index 100% rename from richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/TraverseUtils.kt rename to richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/TraverseUtils.kt diff --git a/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNode.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNode.kt new file mode 100644 index 00000000..f83d32a4 --- /dev/null +++ b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNode.kt @@ -0,0 +1,28 @@ +package com.halilibo.richtext.markdown.node + +/** + * Generic AstNode implementation that can define any node in Abstract Syntax Tree. + * + * @param type A sealed class which is categorized into block and inline nodes. + * @param links Pointers to parent, sibling, child nodes. + */ +public class AstNode( + public val type: AstNodeType, + public val links: AstNodeLinks +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AstNode) return false + + if (type != other.type) return false + if (links != other.links) return false + + return true + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + links.hashCode() + return result + } +} diff --git a/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeLinks.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeLinks.kt new file mode 100644 index 00000000..be5bbcce --- /dev/null +++ b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeLinks.kt @@ -0,0 +1,37 @@ +package com.halilibo.richtext.markdown.node + +import androidx.compose.runtime.Immutable + +/** + * All the pointers that can exist for a node in an AST. + * + * Links are mutable to make it possible to instantiate a Node which can then reconfigure its + * children and siblings. Please do not modify the links after an ASTNode is created and the scope + * is finished. + */ +@Immutable +public class AstNodeLinks( + public var parent: AstNode? = null, + public var firstChild: AstNode? = null, + public var lastChild: AstNode? = null, + public var previous: AstNode? = null, + public var next: AstNode? = null +) { + + override fun equals(other: Any?): Boolean { + if (other !is AstNodeLinks) return false + + return parent === other.parent && + firstChild === other.firstChild && + lastChild === other.lastChild && + previous === other.previous && + next === other.next + } + + /** + * Stop infinite loop and only calculate towards bottom-right direction + */ + override fun hashCode(): Int { + return (firstChild ?: 0).hashCode() * 11 + (next ?: 0).hashCode() * 7 + } +} \ No newline at end of file diff --git a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeType.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeType.kt similarity index 55% rename from richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeType.kt rename to richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeType.kt index 3d53e6d3..e1851279 100644 --- a/richtext-commonmark/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeType.kt +++ b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstNodeType.kt @@ -23,32 +23,50 @@ import com.halilibo.richtext.ui.string.RichTextString * nodes are about styling(bold, italic, strikethrough, code). The rest contains links, images, * html content, and of course raw text. */ -internal sealed class AstNodeType +public sealed class AstNodeType //region AstBlockNodeType -internal sealed class AstBlockNodeType: AstNodeType() +public sealed class AstBlockNodeType: AstNodeType() //region AstContainerBlockNodeType -internal sealed class AstContainerBlockNodeType: AstBlockNodeType() +/** + * Defines a subtype of Block Node that can contain other nodes. + */ +public sealed class AstContainerBlockNodeType: AstBlockNodeType() +/** + * Usually defines the root of a markdown document. + */ @Immutable -internal object AstDocument : AstContainerBlockNodeType() +public object AstDocument : AstContainerBlockNodeType() +/** + * A block quote container that will indent its contents relative to its own indentation. + */ @Immutable -internal object AstBlockQuote : AstContainerBlockNodeType() +public object AstBlockQuote : AstContainerBlockNodeType() +/** + * Ordered or Unordered list item. + */ @Immutable -internal object AstListItem : AstContainerBlockNodeType() +public object AstListItem : AstContainerBlockNodeType() +/** + * A list type that marks its items with bullets to signify a lack of order. + */ @Immutable -internal data class AstBulletList( +public data class AstUnorderedList( val bulletMarker: Char ) : AstContainerBlockNodeType() +/** + * A list type that uses numbers to mark its items. + */ @Immutable -internal data class AstOrderedList( +public data class AstOrderedList( val startNumber: Int, val delimiter: Char ) : AstContainerBlockNodeType() @@ -57,23 +75,26 @@ internal data class AstOrderedList( //region AstLeafBlockNodeType -internal sealed class AstLeafBlockNodeType: AstBlockNodeType() +/** + * Defines a subtype of Block Node that can only contain plain text and full-length annotations. + */ +public sealed class AstLeafBlockNodeType: AstBlockNodeType() @Immutable -internal object AstThematicBreak : AstLeafBlockNodeType() +public object AstThematicBreak : AstLeafBlockNodeType() @Immutable -internal data class AstHeading( +public data class AstHeading( val level: Int ) : AstLeafBlockNodeType() @Immutable -internal data class AstIndentedCodeBlock( +public data class AstIndentedCodeBlock( val literal: String ) : AstLeafBlockNodeType() @Immutable -internal data class AstFencedCodeBlock( +public data class AstFencedCodeBlock( val fenceChar: Char, val fenceLength: Int, val fenceIndent: Int, @@ -82,19 +103,19 @@ internal data class AstFencedCodeBlock( ) : AstLeafBlockNodeType() @Immutable -internal data class AstHtmlBlock( +public data class AstHtmlBlock( val literal: String ) : AstLeafBlockNodeType() @Immutable -internal data class AstLinkReferenceDefinition( +public data class AstLinkReferenceDefinition( val label: String, val destination: String, val title: String ) : AstLeafBlockNodeType() @Immutable -internal object AstParagraph : AstLeafBlockNodeType() +public object AstParagraph : AstLeafBlockNodeType() //endregion @@ -102,48 +123,56 @@ internal object AstParagraph : AstLeafBlockNodeType() //region AstInlineNodeType -internal sealed class AstInlineNodeType: AstNodeType() +/** + * Defines a node type that can only apply to inline content. + */ +public sealed class AstInlineNodeType: AstNodeType() @Immutable -internal data class AstCode( +public data class AstCode( val literal: String ) : AstInlineNodeType() @Immutable -internal data class AstEmphasis( +public data class AstEmphasis( private val delimiter: String ) : AstInlineNodeType() @Immutable -internal data class AstStrongEmphasis( +public data class AstStrongEmphasis( private val delimiter: String ) : AstInlineNodeType() @Immutable -internal data class AstLink( +public data class AstStrikethrough( + val delimiter: String +) : AstInlineNodeType() + +@Immutable +public data class AstLink( val destination: String, val title: String ) : AstInlineNodeType() @Immutable -internal data class AstImage( +public data class AstImage( val title: String, val destination: String ) : AstInlineNodeType() @Immutable -internal data class AstHtmlInline( +public data class AstHtmlInline( val literal: String ) : AstInlineNodeType() @Immutable -internal object AstHardLineBreak : AstInlineNodeType() +public object AstHardLineBreak : AstInlineNodeType() @Immutable -internal object AstSoftLineBreak : AstInlineNodeType() +public object AstSoftLineBreak : AstInlineNodeType() @Immutable -internal data class AstText( +public data class AstText( val literal: String ) : AstInlineNodeType() diff --git a/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstTable.kt b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstTable.kt new file mode 100644 index 00000000..f2addc4f --- /dev/null +++ b/richtext-markdown/src/commonMain/kotlin/com/halilibo/richtext/markdown/node/AstTable.kt @@ -0,0 +1,27 @@ +package com.halilibo.richtext.markdown.node + +import androidx.compose.runtime.Immutable + +@Immutable +public object AstTableRoot: AstContainerBlockNodeType() + +@Immutable +public object AstTableBody: AstContainerBlockNodeType() + +@Immutable +public object AstTableHeader: AstContainerBlockNodeType() + +@Immutable +public object AstTableRow: AstContainerBlockNodeType() + +@Immutable +public data class AstTableCell( + val header: Boolean, + val alignment: AstTableCellAlignment +) : AstContainerBlockNodeType() + +public enum class AstTableCellAlignment { + LEFT, + CENTER, + RIGHT +} diff --git a/richtext-commonmark/src/jvmMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt b/richtext-markdown/src/jvmMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt similarity index 100% rename from richtext-commonmark/src/jvmMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt rename to richtext-markdown/src/jvmMain/kotlin/com/halilibo/richtext/markdown/HtmlBlock.kt diff --git a/richtext-commonmark/src/jvmMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt b/richtext-markdown/src/jvmMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt similarity index 100% rename from richtext-commonmark/src/jvmMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt rename to richtext-markdown/src/jvmMain/kotlin/com/halilibo/richtext/markdown/RemoteImage.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f7f9a90..2f7eeb59 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,7 @@ include(":richtext-ui") include(":richtext-ui-material") include(":richtext-ui-material3") include(":richtext-commonmark") +include(":richtext-markdown") include(":android-sample") include(":desktop-sample") include(":slideshow")