diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b4e032..a70a545 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,7 @@ jobs: ./gradlew multiplatform-markdown-renderer-m3:publishAllPublicationsToMavenCentralRepository --no-daemon --no-configure-on-demand --no-parallel ./gradlew multiplatform-markdown-renderer-coil2:publishAllPublicationsToMavenCentralRepository --no-daemon --no-configure-on-demand --no-parallel ./gradlew multiplatform-markdown-renderer-coil3:publishAllPublicationsToMavenCentralRepository --no-daemon --no-configure-on-demand --no-parallel + ./gradlew multiplatform-markdown-renderer-code:publishAllPublicationsToMavenCentralRepository --no-daemon --no-configure-on-demand --no-parallel build: name: Build diff --git a/Gemfile.lock b/Gemfile.lock index 849838b..641c286 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,8 +57,7 @@ GEM open4 (1.3.4) public_suffix (5.0.3) rchardet (1.8.0) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.3.9) ruby-ll (2.1.3) ansi ast @@ -66,7 +65,6 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - strscan (3.1.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) unicode-display_width (2.4.2) diff --git a/README.md b/README.md index e094472..3dfafec 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,42 @@ Markdown( paragraph = customParagraphComponent ) ) +``` + +Another example to of a custom component is changing the rendering of an unordered list. + +```kotlin +// Define a custom component for rendering unordered list items in Markdown +val customUnorderedListComponent: MarkdownComponent = { + // Use the MarkdownListItems composable to render the list items + MarkdownListItems(it.content, it.node, level = 0) { index, child -> + // Create a row layout for each list item with spacing between elements + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + // Render an icon for the bullet point with a green tint + Icon( + imageVector = icon, + tint = Color.Green, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + // Extract the bullet marker text from the child node + val bulletMarker: String = child.findChildOfType(MarkdownTokenTypes.LIST_BULLET)?.getTextInNode(it.content).toString() + // Extract the item text and remove the bullet marker from it + val itemText = child.getTextInNode(it.content).toString().replace(bulletMarker, "") + + // Render the item text using the MarkdownText composable + MarkdownText(content = itemText) + } + } +} +// Define the `Markdown` composable and pass in the custom unorderedList component +Markdown( + content, + components = markdownComponents( + unorderedList = customUnorderedListComponent + ) +) ```

@@ -251,7 +286,8 @@ Markdown( ``` > [!NOTE] -> 0.21.0 adds JVM support for this dependency via `HTTPUrlConnection` -> however this is expected to be removed in the future. +> 0.21.0 adds JVM support for this dependency via `HTTPUrlConnection` -> however this is expected to be removed in the +> future. > [!NOTE] > Please refer to the official coil2 documentation on how to adjust the `ImageLoader` @@ -276,6 +312,43 @@ Markdown( > [!NOTE] > The `coil3` module does depend on SNAPSHOT builds of coil3 +### Syntax Highlighting + +The library (introduced with 0.27.0) offers optional support for syntax highlighting via +the [Highlights](https://github.com/SnipMeDev/Highlights) project. +This support is not included in the core, and can be enabled by adding the `multiplatform-markdown-renderer-code` +dependency. + +```groovy +implementation("com.mikepenz:multiplatform-markdown-renderer-code:${version}") +``` + +Once added, the `Markdown` has to be configured to use the alternative code highlighter. + +```kotlin +// Use default color scheme +Markdown( + MARKDOWN, + components = markdownComponents( + codeBlock = highlightedCodeBlock, + codeFence = highlightedCodeFence, + ) +) + +// ADVANCED: Customize Highlights library by defining different theme +val isDarkTheme = isSystemInDarkTheme() +val highlightsBuilder = remember(isDarkTheme) { + Highlights.Builder().theme(SyntaxThemes.atom(darkMode = isDarkTheme)) +} +Markdown( + MARKDOWN, + components = markdownComponents( + codeBlock = { MarkdownHighlightedCodeBlock(it.content, it.node, highlightsBuilder) }, + codeFence = { MarkdownHighlightedCodeFence(it.content, it.node, highlightsBuilder) }, + ) +) +``` + ## Dependency This project uses JetBrains [markdown](https://github.com/JetBrains/markdown/) Multiplatform diff --git a/app-desktop/build.gradle.kts b/app-desktop/build.gradle.kts index 8e8521a..128de8f 100644 --- a/app-desktop/build.gradle.kts +++ b/app-desktop/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.multiplatformMarkdownRenderer) implementation(projects.multiplatformMarkdownRendererM2) implementation(projects.multiplatformMarkdownRendererCoil3) + implementation(projects.multiplatformMarkdownRendererCode) implementation(compose.desktop.currentOs) implementation(compose.foundation) diff --git a/app-desktop/src/main/kotlin/main.kt b/app-desktop/src/main/kotlin/main.kt index 99e2e47..eb102aa 100644 --- a/app-desktop/src/main/kotlin/main.kt +++ b/app-desktop/src/main/kotlin/main.kt @@ -1,3 +1,4 @@ +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -11,12 +12,17 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl +import com.mikepenz.markdown.compose.components.markdownComponents +import com.mikepenz.markdown.compose.elements.MarkdownHighlightedCodeBlock +import com.mikepenz.markdown.compose.elements.MarkdownHighlightedCodeFence import com.mikepenz.markdown.compose.extendedspans.ExtendedSpans import com.mikepenz.markdown.compose.extendedspans.RoundedCornerSpanPainter import com.mikepenz.markdown.compose.extendedspans.SquigglyUnderlineSpanPainter import com.mikepenz.markdown.compose.extendedspans.rememberSquigglyUnderlineAnimator import com.mikepenz.markdown.m2.Markdown import com.mikepenz.markdown.model.markdownExtendedSpans +import dev.snipme.highlights.Highlights +import dev.snipme.highlights.model.SyntaxThemes fun main() = application { Window(onCloseRequest = ::exitApplication, title = "Markdown Sample") { @@ -29,9 +35,17 @@ fun main() = application { } ) { val scrollState = rememberScrollState() + val isDarkTheme = isSystemInDarkTheme() + val highlightsBuilder = remember(isDarkTheme) { + Highlights.Builder().theme(SyntaxThemes.atom(darkMode = isDarkTheme)) + } Markdown( MARKDOWN, + components = markdownComponents( + codeBlock = { MarkdownHighlightedCodeBlock(it.content, it.node, highlightsBuilder) }, + codeFence = { MarkdownHighlightedCodeFence(it.content, it.node, highlightsBuilder) }, + ), imageTransformer = Coil3ImageTransformerImpl, extendedSpans = markdownExtendedSpans { val animator = rememberSquigglyUnderlineAnimator() @@ -76,7 +90,7 @@ This is a paragraph with a [link](https://www.jetbrains.com/). This is a code block: ```kotlin fun main() { -println("Hello, world!") + println("Hello, world!") } ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc8575f..677d40e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,7 +18,7 @@ android { defaultConfig { applicationId = "com.mikepenz.markdown" - minSdk = libs.versions.minSdk.get().toInt() + minSdk = 24 // anything below faces: https://github.com/mikepenz/multiplatform-markdown-renderer/issues/223 targetSdk = libs.versions.targetSdk.get().toInt() versionCode = property("VERSION_CODE").toString().toInt() diff --git a/app/src/screenshotTest/kotlin/com.mikepenz.markdown.ui/SnapshotTests.kt b/app/src/screenshotTest/kotlin/com.mikepenz.markdown.ui/SnapshotTests.kt index 1217fe5..0934c64 100644 --- a/app/src/screenshotTest/kotlin/com.mikepenz.markdown.ui/SnapshotTests.kt +++ b/app/src/screenshotTest/kotlin/com.mikepenz.markdown.ui/SnapshotTests.kt @@ -63,11 +63,11 @@ private val MARKDOWN_DEFAULT = """ ###### This is an H6 -This is a paragraph with some *italic* and **bold** text. +This is a paragraph with some *italic* and **bold** text\. -This is a paragraph with some `inline code`. +This is a paragraph with some `inline code`\. -This is a paragraph with a [link](https://www.jetbrains.com/). +This is a paragraph with a [link](https://www.jetbrains.com/)\. This is a code block: ```kotlin diff --git a/gradle.properties b/gradle.properties index 3c2dd86..a8ac6a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Maven stuff GROUP=com.mikepenz -VERSION_NAME=0.26.0 -VERSION_CODE=2600 +VERSION_NAME=0.27.0 +VERSION_CODE=2700 POM_URL=https://github.com/mikepenz/multiplatform-markdown-renderer @@ -30,6 +30,7 @@ kotlin.js.compiler=both kotlin.mpp.stability.nowarn=true kotlin.mpp.androidSourceSetLayoutVersion=2 +kotlin.suppressGradlePluginWarnings=IncorrectCompileOnlyDependencyWarning org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.compose.experimental.uikit.enabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6715e50..52b62c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,24 @@ [versions] -agp = "8.5.2" +agp = "8.7.2" compileSdk = "34" minSdk = "21" targetSdk = "34" -androidx-activityCompose = "1.9.1" +androidx-activityCompose = "1.9.3" androidx-material = "1.12.0" -compose = "1.6.8" -compose-plugin = "1.6.11" -kotlin = "2.0.20" -coroutines = "1.8.1" -dokka = "1.9.20" -coil = "3.0.0-alpha10" +compose = "1.7.5" +compose-plugin = "1.7.0" +kotlin = "2.0.21" +coroutines = "1.9.0" +dokka = "2.0.0-Beta" +coil = "3.0.0-rc02" coil2 = "2.7.0" aboutlib = "11.2.3" markdown = "0.7.3" -detekt = "1.23.6" -gradleMvnPublish = "0.29.0" -screenshot = "0.0.1-alpha05" -ktor = "3.0.0-beta-2" +detekt = "1.23.7" +gradleMvnPublish = "0.30.0" +screenshot = "0.0.1-alpha07" +ktor = "3.0.1" +highlights = "0.9.3" [libraries] androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } @@ -39,6 +40,7 @@ markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" } +highlights = { module = "dev.snipme:highlights", version.ref = "highlights" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/multiplatform-markdown-renderer-code/.gitignore b/multiplatform-markdown-renderer-code/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/multiplatform-markdown-renderer-code/.gitignore @@ -0,0 +1 @@ +/build diff --git a/multiplatform-markdown-renderer-code/build.gradle.kts b/multiplatform-markdown-renderer-code/build.gradle.kts new file mode 100644 index 0000000..cf4721c --- /dev/null +++ b/multiplatform-markdown-renderer-code/build.gradle.kts @@ -0,0 +1,174 @@ +import com.vanniktech.maven.publish.SonatypeHost +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + +plugins { + kotlin("multiplatform") + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.dokka) + alias(libs.plugins.mavenPublish) +} + +android { + namespace = "com.mikepenz.markdown.code" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + tasks.withType { + kotlinOptions.jvmTarget = "11" + + kotlinOptions { + if (project.findProperty("composeCompilerReports") == "true") { + freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.absolutePath}/compose_compiler" + ) + } + if (project.findProperty("composeCompilerMetrics") == "true") { + freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir.absolutePath}/compose_compiler" + ) + } + } + } +} + +kotlin { + applyDefaultHierarchyTemplate() + + targets.all { + compilations.all { + compilerOptions.configure { + languageVersion.set(KotlinVersion.KOTLIN_1_9) + apiVersion.set(KotlinVersion.KOTLIN_1_9) + } + } + } + androidTarget { + publishLibraryVariants("release") + } + jvm { + compilations { + all { + kotlinOptions.jvmTarget = "11" + } + } + + testRuns["test"].executionTask.configure { + useJUnit { + excludeCategories("org.intellij.markdown.ParserPerformanceTest") + } + } + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + } + js(IR) { + nodejs() + } + macosX64() + macosArm64() + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val fileBasedTest by creating { + dependsOn(commonTest) + } + val jvmTest by getting { + dependsOn(fileBasedTest) + } + val jsTest by getting { + dependsOn(fileBasedTest) + } + val nativeMain by getting { + dependsOn(commonMain) + } + val nativeTest by getting { + dependsOn(fileBasedTest) + } + val nativeSourceSets = listOf( + "macosX64", + "macosArm64", + "ios", + "iosSimulatorArm64" + ).map { "${it}Main" } + for (set in nativeSourceSets) { + getByName(set).dependsOn(nativeMain) + } + val nativeTestSourceSets = listOf( + "macosX64", + "macosArm64" + ).map { "${it}Test" } + for (set in nativeTestSourceSets) { + getByName(set).dependsOn(nativeTest) + getByName(set).dependsOn(fileBasedTest) + } + } +} + +dependencies { + commonMainApi(projects.multiplatformMarkdownRenderer) + commonMainCompileOnly(compose.runtime) + commonMainCompileOnly(compose.ui) + commonMainCompileOnly(compose.foundation) + + commonMainApi(libs.highlights) +} + +tasks.dokkaHtml.configure { + dokkaSourceSets { + configureEach { + noAndroidSdkLink.set(false) + } + } +} + +tasks.create("javadocJar") { + dependsOn("dokkaJavadoc") + archiveClassifier.set("javadoc") + from("${layout.buildDirectory}/javadoc") +} + +mavenPublishing { + publishToMavenCentral(SonatypeHost.S01) + signAllPublications() +} + +publishing { + repositories { + maven { + name = "installLocally" + setUrl("${rootProject.layout.buildDirectory}/localMaven") + } + } +} diff --git a/multiplatform-markdown-renderer-code/gradle.properties b/multiplatform-markdown-renderer-code/gradle.properties new file mode 100755 index 0000000..80bc452 --- /dev/null +++ b/multiplatform-markdown-renderer-code/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=Multiplatform Markdown Renderer - Code +POM_DESCRIPTION=Kotlin Multiplatform Markdown Renderer. (Android, Desktop, ...) powered by Compose Multiplatform +POM_ARTIFACT_ID=multiplatform-markdown-renderer-code \ No newline at end of file diff --git a/multiplatform-markdown-renderer-code/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownHighlightedCode.kt b/multiplatform-markdown-renderer-code/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownHighlightedCode.kt new file mode 100644 index 0000000..51cc73d --- /dev/null +++ b/multiplatform-markdown-renderer-code/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownHighlightedCode.kt @@ -0,0 +1,109 @@ +package com.mikepenz.markdown.compose.elements + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.mikepenz.markdown.compose.LocalMarkdownColors +import com.mikepenz.markdown.compose.LocalMarkdownDimens +import com.mikepenz.markdown.compose.LocalMarkdownPadding +import com.mikepenz.markdown.compose.LocalMarkdownTypography +import com.mikepenz.markdown.compose.components.MarkdownComponent +import com.mikepenz.markdown.compose.elements.material.MarkdownBasicText +import dev.snipme.highlights.Highlights +import dev.snipme.highlights.model.BoldHighlight +import dev.snipme.highlights.model.ColorHighlight +import dev.snipme.highlights.model.SyntaxLanguage +import org.intellij.markdown.ast.ASTNode + +/** Default definition for the [MarkdownHighlightedCodeFence]. Uses default theme, attempts to apply language from markdown. */ +val highlightedCodeFence: MarkdownComponent = { MarkdownHighlightedCodeFence(it.content, it.node) } + +/** Default definition for the [MarkdownHighlightedCodeBlock]. Uses default theme, attempts to apply language from markdown. */ +val highlightedCodeBlock: MarkdownComponent = { MarkdownHighlightedCodeBlock(it.content, it.node) } + +@Composable +fun MarkdownHighlightedCodeFence(content: String, node: ASTNode, highlights: Highlights.Builder = Highlights.Builder()) { + MarkdownCodeFence(content, node) { code, language -> + MarkdownHighlightedCode(code, language, highlights) + } +} + +@Composable +fun MarkdownHighlightedCodeBlock(content: String, node: ASTNode, highlights: Highlights.Builder = Highlights.Builder()) { + MarkdownCodeBlock(content, node) { code, language -> + MarkdownHighlightedCode(code, language, highlights) + } +} + +@Composable +fun MarkdownHighlightedCode( + code: String, + language: String?, + highlights: Highlights.Builder = Highlights.Builder(), + style: TextStyle = LocalMarkdownTypography.current.code, +) { + val backgroundCodeColor = LocalMarkdownColors.current.codeBackground + val codeBackgroundCornerSize = LocalMarkdownDimens.current.codeBackgroundCornerSize + val codeBlockPadding = LocalMarkdownPadding.current.codeBlock + val syntaxLanguage = remember(language) { language?.let { SyntaxLanguage.getByName(it) } } + + val codeHighlights by remembering(code) { + derivedStateOf { + highlights + .code(code) + .let { if (syntaxLanguage != null) it.language(syntaxLanguage) else it } + .build() + } + } + + MarkdownCodeBackground( + color = backgroundCodeColor, + shape = RoundedCornerShape(codeBackgroundCornerSize), + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) + ) { + MarkdownBasicText( + buildAnnotatedString { + text(codeHighlights.getCode()) + codeHighlights.getHighlights() + .filterIsInstance() + .forEach { + addStyle( + SpanStyle(color = Color(it.rgb).copy(alpha = 1f)), + start = it.location.start, + end = it.location.end, + ) + } + codeHighlights.getHighlights() + .filterIsInstance() + .forEach { + addStyle( + SpanStyle(fontWeight = FontWeight.Bold), + start = it.location.start, + end = it.location.end, + ) + } + }, + color = LocalMarkdownColors.current.codeText, + modifier = Modifier.horizontalScroll(rememberScrollState()).padding(codeBlockPadding), + style = style + ) + } +} + +@Composable +internal inline fun remembering( + key1: K, + crossinline calculation: @DisallowComposableCalls (K) -> T, +): T = remember(key1) { calculation(key1) } + +internal fun AnnotatedString.Builder.text(text: String, style: SpanStyle = SpanStyle()) = withStyle(style = style) { + append(text) +} \ No newline at end of file diff --git a/multiplatform-markdown-renderer-m2/src/commonMain/kotlin/com/mikepenz/markdown/m2/MarkdownTypography.kt b/multiplatform-markdown-renderer-m2/src/commonMain/kotlin/com/mikepenz/markdown/m2/MarkdownTypography.kt index 3a19667..a66bef1 100644 --- a/multiplatform-markdown-renderer-m2/src/commonMain/kotlin/com/mikepenz/markdown/m2/MarkdownTypography.kt +++ b/multiplatform-markdown-renderer-m2/src/commonMain/kotlin/com/mikepenz/markdown/m2/MarkdownTypography.kt @@ -6,6 +6,8 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import com.mikepenz.markdown.model.DefaultMarkdownTypography import com.mikepenz.markdown.model.MarkdownTypography @@ -19,13 +21,18 @@ fun markdownTypography( h6: TextStyle = MaterialTheme.typography.h6, text: TextStyle = MaterialTheme.typography.body1, code: TextStyle = MaterialTheme.typography.body2.copy(fontFamily = FontFamily.Monospace), + inlineCode: TextStyle = text.copy(fontFamily = FontFamily.Monospace), quote: TextStyle = MaterialTheme.typography.body2.plus(SpanStyle(fontStyle = FontStyle.Italic)), paragraph: TextStyle = MaterialTheme.typography.body1, ordered: TextStyle = MaterialTheme.typography.body1, bullet: TextStyle = MaterialTheme.typography.body1, - list: TextStyle = MaterialTheme.typography.body1 + list: TextStyle = MaterialTheme.typography.body1, + link: TextStyle = MaterialTheme.typography.body1.copy( + fontWeight = FontWeight.Bold, + textDecoration = TextDecoration.Underline + ), ): MarkdownTypography = DefaultMarkdownTypography( h1 = h1, h2 = h2, h3 = h3, h4 = h4, h5 = h5, h6 = h6, - text = text, quote = quote, code = code, paragraph = paragraph, - ordered = ordered, bullet = bullet, list = list + text = text, quote = quote, code = code, inlineCode = inlineCode, paragraph = paragraph, + ordered = ordered, bullet = bullet, list = list, link = link, ) diff --git a/multiplatform-markdown-renderer-m3/src/commonMain/kotlin/com/mikepenz/markdown/m3/MarkdownTypography.kt b/multiplatform-markdown-renderer-m3/src/commonMain/kotlin/com/mikepenz/markdown/m3/MarkdownTypography.kt index 70317d1..e90bca7 100644 --- a/multiplatform-markdown-renderer-m3/src/commonMain/kotlin/com/mikepenz/markdown/m3/MarkdownTypography.kt +++ b/multiplatform-markdown-renderer-m3/src/commonMain/kotlin/com/mikepenz/markdown/m3/MarkdownTypography.kt @@ -6,6 +6,8 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import com.mikepenz.markdown.model.DefaultMarkdownTypography import com.mikepenz.markdown.model.MarkdownTypography @@ -19,13 +21,18 @@ fun markdownTypography( h6: TextStyle = MaterialTheme.typography.titleLarge, text: TextStyle = MaterialTheme.typography.bodyLarge, code: TextStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace), + inlineCode: TextStyle = text.copy(fontFamily = FontFamily.Monospace), quote: TextStyle = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)), paragraph: TextStyle = MaterialTheme.typography.bodyLarge, ordered: TextStyle = MaterialTheme.typography.bodyLarge, bullet: TextStyle = MaterialTheme.typography.bodyLarge, - list: TextStyle = MaterialTheme.typography.bodyLarge + list: TextStyle = MaterialTheme.typography.bodyLarge, + link: TextStyle = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Bold, + textDecoration = TextDecoration.Underline + ), ): MarkdownTypography = DefaultMarkdownTypography( h1 = h1, h2 = h2, h3 = h3, h4 = h4, h5 = h5, h6 = h6, - text = text, quote = quote, code = code, paragraph = paragraph, - ordered = ordered, bullet = bullet, list = list + text = text, quote = quote, code = code, inlineCode = inlineCode, paragraph = paragraph, + ordered = ordered, bullet = bullet, list = list, link = link, ) diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/components/MarkdownComponents.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/components/MarkdownComponents.kt index 73e3378..9eeda11 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/components/MarkdownComponents.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/components/MarkdownComponents.kt @@ -9,12 +9,12 @@ import androidx.compose.ui.Modifier import com.mikepenz.markdown.compose.LocalReferenceLinkHandler import com.mikepenz.markdown.compose.elements.* import com.mikepenz.markdown.model.MarkdownTypography +import com.mikepenz.markdown.utils.getUnescapedTextInNode import org.intellij.markdown.IElementType import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.MarkdownTokenTypes import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.ast.findChildOfType -import org.intellij.markdown.ast.getTextInNode typealias MarkdownComponent = @Composable ColumnScope.(MarkdownComponentModel) -> Unit @@ -29,7 +29,7 @@ data class MarkdownComponentModel( val typography: MarkdownTypography, ) -private fun MarkdownComponentModel.getTextInNode() = node.getTextInNode(content) +private fun MarkdownComponentModel.getUnescapedTextInNode() = node.getUnescapedTextInNode(content) fun markdownComponents( text: MarkdownComponent = CurrentComponentsBridge.text, @@ -130,7 +130,7 @@ private class DefaultMarkdownComponents( */ object CurrentComponentsBridge { val text: MarkdownComponent = { - MarkdownText(it.getTextInNode().toString()) + MarkdownText(it.getUnescapedTextInNode()) } val eol: MarkdownComponent = { } val codeFence: MarkdownComponent = { @@ -184,11 +184,10 @@ object CurrentComponentsBridge { } val linkDefinition: MarkdownComponent = { val linkLabel = - it.node.findChildOfType(MarkdownElementTypes.LINK_LABEL)?.getTextInNode(it.content) - ?.toString() + it.node.findChildOfType(MarkdownElementTypes.LINK_LABEL)?.getUnescapedTextInNode(it.content) if (linkLabel != null) { val destination = it.node.findChildOfType(MarkdownElementTypes.LINK_DESTINATION) - ?.getTextInNode(it.content)?.toString() + ?.getUnescapedTextInNode(it.content) LocalReferenceLinkHandler.current.store(linkLabel, destination) } } diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownCode.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownCode.kt index 9210636..1013932 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownCode.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownCode.kt @@ -23,7 +23,10 @@ import com.mikepenz.markdown.compose.LocalMarkdownDimens import com.mikepenz.markdown.compose.LocalMarkdownPadding import com.mikepenz.markdown.compose.LocalMarkdownTypography import com.mikepenz.markdown.compose.elements.material.MarkdownBasicText +import org.intellij.markdown.MarkdownTokenTypes import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.findChildOfType +import org.intellij.markdown.ast.getTextInNode @Composable private fun MarkdownCode( @@ -34,15 +37,10 @@ private fun MarkdownCode( val codeBackgroundCornerSize = LocalMarkdownDimens.current.codeBackgroundCornerSize val codeBlockPadding = LocalMarkdownPadding.current.codeBlock MarkdownCodeBackground( - color = backgroundCodeColor, - shape = RoundedCornerShape(codeBackgroundCornerSize), - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) + color = backgroundCodeColor, shape = RoundedCornerShape(codeBackgroundCornerSize), modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) ) { MarkdownBasicText( - code, - color = LocalMarkdownColors.current.codeText, - modifier = Modifier.horizontalScroll(rememberScrollState()).padding(codeBlockPadding), - style = style + code, color = LocalMarkdownColors.current.codeText, modifier = Modifier.horizontalScroll(rememberScrollState()).padding(codeBlockPadding), style = style ) } } @@ -51,14 +49,17 @@ private fun MarkdownCode( fun MarkdownCodeFence( content: String, node: ASTNode, + block: @Composable (String, String?) -> Unit = { code, _ -> MarkdownCode(code) }, ) { // CODE_FENCE_START, FENCE_LANG, {content // CODE_FENCE_CONTENT // x-times}, CODE_FENCE_END // CODE_FENCE_START, EOL, {content // CODE_FENCE_CONTENT // x-times}, EOL // CODE_FENCE_START, EOL, {content // CODE_FENCE_CONTENT // x-times} + + val language = node.findChildOfType(MarkdownTokenTypes.FENCE_LANG)?.getTextInNode(content)?.toString() if (node.children.size >= 3) { val start = node.children[2].startOffset val end = node.children[(node.children.size - 2).coerceAtLeast(2)].endOffset - MarkdownCode(content.subSequence(start, end).toString().replaceIndent()) + block(content.subSequence(start, end).toString().replaceIndent(), language) } else { // invalid code block, skipping } @@ -68,15 +69,16 @@ fun MarkdownCodeFence( fun MarkdownCodeBlock( content: String, node: ASTNode, + block: @Composable (String, String?) -> Unit = { code, _ -> MarkdownCode(code) }, ) { val start = node.children[0].startOffset val end = node.children[node.children.size - 1].endOffset - MarkdownCode(content.subSequence(start, end).toString().replaceIndent()) + val language = node.findChildOfType(MarkdownTokenTypes.FENCE_LANG)?.getTextInNode(content)?.toString() + block(content.subSequence(start, end).toString().replaceIndent(), language) } - @Composable -internal fun MarkdownCodeBackground( +fun MarkdownCodeBackground( color: Color, modifier: Modifier = Modifier, shape: Shape = RectangleShape, @@ -84,17 +86,10 @@ internal fun MarkdownCodeBackground( elevation: Dp = 0.dp, content: @Composable () -> Unit, ) { - Box( - modifier = modifier - .shadow(elevation, shape, clip = false) - .then(if (border != null) Modifier.border(border, shape) else Modifier) - .background(color = color, shape = shape) - .clip(shape) - .semantics(mergeDescendants = false) { - isTraversalGroup = true - } - .pointerInput(Unit) {}, - propagateMinConstraints = true + Box(modifier = modifier.shadow(elevation, shape, clip = false).then(if (border != null) Modifier.border(border, shape) else Modifier).background(color = color, shape = shape) + .clip(shape).semantics(mergeDescendants = false) { + isTraversalGroup = true + }.pointerInput(Unit) {}, propagateMinConstraints = true ) { content() } diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownImage.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownImage.kt index 82b88a5..a414dab 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownImage.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownImage.kt @@ -4,14 +4,14 @@ import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import com.mikepenz.markdown.compose.LocalImageTransformer import com.mikepenz.markdown.utils.findChildOfTypeRecursive +import com.mikepenz.markdown.utils.getUnescapedTextInNode import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.ast.ASTNode -import org.intellij.markdown.ast.getTextInNode @Composable fun MarkdownImage(content: String, node: ASTNode) { - val link = node.findChildOfTypeRecursive(MarkdownElementTypes.LINK_DESTINATION)?.getTextInNode(content)?.toString() ?: return + val link = node.findChildOfTypeRecursive(MarkdownElementTypes.LINK_DESTINATION)?.getUnescapedTextInNode(content) ?: return LocalImageTransformer.current.transform(link)?.let { imageData -> Image( diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownList.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownList.kt index f95830d..08e7007 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownList.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownList.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import com.mikepenz.markdown.compose.* import com.mikepenz.markdown.compose.elements.material.MarkdownBasicText +import com.mikepenz.markdown.utils.getUnescapedTextInNode import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.MarkdownElementTypes.ORDERED_LIST import org.intellij.markdown.MarkdownElementTypes.UNORDERED_LIST @@ -16,7 +17,6 @@ import org.intellij.markdown.MarkdownTokenTypes.Companion.LIST_BULLET import org.intellij.markdown.MarkdownTokenTypes.Companion.LIST_NUMBER import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.ast.findChildOfType -import org.intellij.markdown.ast.getTextInNode @Composable fun MarkdownListItems( @@ -65,7 +65,7 @@ fun MarkdownOrderedList( MarkdownBasicText( text = orderedListHandler.transform( LIST_NUMBER, - child.findChildOfType(LIST_NUMBER)?.getTextInNode(content), + child.findChildOfType(LIST_NUMBER)?.getUnescapedTextInNode(content), index ), style = style, @@ -98,7 +98,7 @@ fun MarkdownBulletList( MarkdownBasicText( bulletHandler.transform( LIST_BULLET, - child.findChildOfType(LIST_BULLET)?.getTextInNode(content), + child.findChildOfType(LIST_BULLET)?.getUnescapedTextInNode(content), index ), style = style, diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownText.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownText.kt index 0019e28..2ec4758 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownText.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/MarkdownText.kt @@ -128,32 +128,36 @@ fun MarkdownText( imageState.setContainerSize(coordinates.size) } } - .animateContentSize(), + .let { + if (placeholderState.animate) it.animateContentSize() else it + }, style = style, color = LocalMarkdownColors.current.text, - inlineContent = mapOf(MARKDOWN_TAG_IMAGE_URL to InlineTextContent( - Placeholder( - width = placeholderState.size.width.sp, - height = placeholderState.size.height.sp, - placeholderVerticalAlign = placeholderState.verticalAlign - ) - ) { link -> - transformer.transform(link)?.let { imageData -> - val intrinsicSize = transformer.intrinsicSize(imageData.painter) - LaunchedEffect(intrinsicSize) { - imageState.setImageSize(intrinsicSize) - } - Image( - painter = imageData.painter, - contentDescription = imageData.contentDescription, - modifier = imageData.modifier, - alignment = imageData.alignment, - contentScale = imageData.contentScale, - alpha = imageData.alpha, - colorFilter = imageData.colorFilter + inlineContent = mapOf( + MARKDOWN_TAG_IMAGE_URL to InlineTextContent( + Placeholder( + width = placeholderState.size.width.sp, + height = placeholderState.size.height.sp, + placeholderVerticalAlign = placeholderState.verticalAlign ) + ) { link -> + transformer.transform(link)?.let { imageData -> + val intrinsicSize = transformer.intrinsicSize(imageData.painter) + LaunchedEffect(intrinsicSize) { + imageState.setImageSize(intrinsicSize) + } + Image( + painter = imageData.painter, + contentDescription = imageData.contentDescription, + modifier = imageData.modifier, + alignment = imageData.alignment, + contentScale = imageData.contentScale, + alpha = imageData.alpha, + colorFilter = imageData.colorFilter + ) + } } - }), + ), onTextLayout = { layoutResult.value = it onTextLayout.invoke(it) diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/material/TextWrapper.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/material/TextWrapper.kt index aea0ffc..c7150b0 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/material/TextWrapper.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/elements/material/TextWrapper.kt @@ -70,7 +70,7 @@ internal fun MarkdownBasicText( } @Composable -internal fun MarkdownBasicText( +fun MarkdownBasicText( text: AnnotatedString, style: TextStyle, modifier: Modifier = Modifier, diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/ImageTransformer.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/ImageTransformer.kt index 5cf6836..0a2e30c 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/ImageTransformer.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/ImageTransformer.kt @@ -55,6 +55,7 @@ interface ImageTransformer { data class PlaceholderConfig( val size: Size, val verticalAlign: PlaceholderVerticalAlign = PlaceholderVerticalAlign.Bottom, + val animate: Boolean = true, ) @Immutable diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/MarkdownTypography.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/MarkdownTypography.kt index 788f08a..bade6c8 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/MarkdownTypography.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/MarkdownTypography.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.text.TextStyle interface MarkdownTypography { val text: TextStyle val code: TextStyle + val inlineCode: TextStyle val h1: TextStyle val h2: TextStyle val h3: TextStyle @@ -17,6 +18,7 @@ interface MarkdownTypography { val ordered: TextStyle val bullet: TextStyle val list: TextStyle + val link: TextStyle } @Immutable @@ -29,9 +31,11 @@ class DefaultMarkdownTypography( override val h6: TextStyle, override val text: TextStyle, override val code: TextStyle, + override val inlineCode: TextStyle, override val quote: TextStyle, override val paragraph: TextStyle, override val ordered: TextStyle, override val bullet: TextStyle, - override val list: TextStyle + override val list: TextStyle, + override val link: TextStyle, ) : MarkdownTypography \ No newline at end of file diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/AnnotatedStringKtx.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/AnnotatedStringKtx.kt index 1ad64b5..fa0fe8d 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/AnnotatedStringKtx.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/AnnotatedStringKtx.kt @@ -4,17 +4,16 @@ import androidx.compose.foundation.text.appendInlineContent import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import com.mikepenz.markdown.compose.LocalMarkdownAnnotator import com.mikepenz.markdown.compose.LocalMarkdownColors +import com.mikepenz.markdown.compose.LocalMarkdownTypography import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.MarkdownTokenTypes import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.ast.findChildOfType -import org.intellij.markdown.ast.getTextInNode import org.intellij.markdown.flavours.gfm.GFMElementTypes import org.intellij.markdown.flavours.gfm.GFMTokenTypes @@ -22,23 +21,19 @@ import org.intellij.markdown.flavours.gfm.GFMTokenTypes internal fun AnnotatedString.Builder.appendMarkdownLink(content: String, node: ASTNode) { val linkText = node.findChildOfType(MarkdownElementTypes.LINK_TEXT)?.children?.innerList() if (linkText == null) { - append(node.getTextInNode(content).toString()) + append(node.getUnescapedTextInNode(content)) return } val destination = node.findChildOfType(MarkdownElementTypes.LINK_DESTINATION) - ?.getTextInNode(content) + ?.getUnescapedTextInNode(content) ?.toString() val linkLabel = node.findChildOfType(MarkdownElementTypes.LINK_LABEL) - ?.getTextInNode(content)?.toString() + ?.getUnescapedTextInNode(content) val annotation = destination ?: linkLabel if (annotation != null) pushStringAnnotation(MARKDOWN_TAG_URL, annotation) - pushStyle( - SpanStyle( - color = LocalMarkdownColors.current.linkText, - textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Bold - ) - ) + val linkColor = LocalMarkdownColors.current.linkText + val linkTextStyle = LocalMarkdownTypography.current.link.copy(color = linkColor).toSpanStyle() + pushStyle(linkTextStyle) buildMarkdownAnnotatedString(content, linkText) pop() if (annotation != null) pop() @@ -49,15 +44,11 @@ internal fun AnnotatedString.Builder.appendAutoLink(content: String, node: ASTNo val targetNode = node.children.firstOrNull { it.type.name == MarkdownElementTypes.AUTOLINK.name } ?: node - val destination = targetNode.getTextInNode(content).toString() + val destination = targetNode.getUnescapedTextInNode(content) pushStringAnnotation(MARKDOWN_TAG_URL, (destination)) - pushStyle( - SpanStyle( - color = LocalMarkdownColors.current.linkText, - textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Bold - ) - ) + val linkColor = LocalMarkdownColors.current.linkText + val linkTextStyle = LocalMarkdownTypography.current.link.copy(color = linkColor).toSpanStyle() + pushStyle(linkTextStyle) append(destination) pop() } @@ -100,7 +91,7 @@ fun AnnotatedString.Builder.buildMarkdownAnnotatedString(content: String, childr MarkdownElementTypes.IMAGE -> child.findChildOfTypeRecursive( MarkdownElementTypes.LINK_DESTINATION )?.let { - appendInlineContent(MARKDOWN_TAG_IMAGE_URL, it.getTextInNode(content).toString()) + appendInlineContent(MARKDOWN_TAG_IMAGE_URL, it.getUnescapedTextInNode(content)) } MarkdownElementTypes.EMPH -> { @@ -122,12 +113,12 @@ fun AnnotatedString.Builder.buildMarkdownAnnotatedString(content: String, childr } MarkdownElementTypes.CODE_SPAN -> { + val codeStyle = LocalMarkdownTypography.current.inlineCode pushStyle( - SpanStyle( - fontFamily = FontFamily.Monospace, + codeStyle.copy( color = LocalMarkdownColors.current.inlineCodeText, background = LocalMarkdownColors.current.inlineCodeBackground - ) + ).toSpanStyle() ) append(' ') buildMarkdownAnnotatedString(content, child.children.innerList()) @@ -141,9 +132,9 @@ fun AnnotatedString.Builder.buildMarkdownAnnotatedString(content: String, childr MarkdownElementTypes.FULL_REFERENCE_LINK -> appendMarkdownLink(content, child) // Token Types - MarkdownTokenTypes.TEXT -> append(child.getTextInNode(content).toString()) + MarkdownTokenTypes.TEXT -> append(child.getUnescapedTextInNode(content)) GFMTokenTypes.GFM_AUTOLINK -> if (child.parent == MarkdownElementTypes.LINK_TEXT) { - append(child.getTextInNode(content).toString()) + append(child.getUnescapedTextInNode(content)) } else appendAutoLink(content, child) MarkdownTokenTypes.SINGLE_QUOTE -> append('\'') diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/EntityConverter.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/EntityConverter.kt new file mode 100644 index 0000000..e6fed62 --- /dev/null +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/EntityConverter.kt @@ -0,0 +1,38 @@ +package com.mikepenz.markdown.utils + +import org.intellij.markdown.html.entities.Entities + +/** + * Based on: https://github.com/JetBrains/markdown/blob/master/src/commonMain/kotlin/org/intellij/markdown/html/entities/EntityConverter.kt + * Removed HTML focused escaping by https://github.com/mikepenz/multiplatform-markdown-renderer/pull/222 + */ +object EntityConverter { + private const val escapeAllowedString = """!"#\$%&'\(\)\*\+,\-.\/:;<=>\?@\[\\\]\^_`{\|}~""" + private val REGEX = Regex("""&(?:([a-zA-Z0-9]+)|#([0-9]{1,8})|#[xX]([a-fA-F0-9]{1,8}));|(["&<>])""") + private val REGEX_ESCAPES = Regex("${REGEX.pattern}|\\\\([$escapeAllowedString])") + + fun replaceEntities( + text: CharSequence, + processEntities: Boolean, + processEscapes: Boolean + ): String { + val regex = if (processEscapes) REGEX_ESCAPES else REGEX + return regex.replace(text) { match -> + val g = match.groups + when { + g.size > 5 && g[5] != null -> g[5]!!.value[0].toString() + g[4] != null -> match.value + else -> { + val code = when { + !processEntities -> null + g[1] != null -> Entities.map[match.value] + g[2] != null -> g[2]!!.value.toInt() + g[3] != null -> g[3]!!.value.toInt(16) + else -> null + } + code?.toChar()?.toString() ?: "&${match.value.substring(1)}" + } + } + } + } +} diff --git a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/Extensions.kt b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/Extensions.kt index 9fe1a47..0684649 100644 --- a/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/Extensions.kt +++ b/multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/Extensions.kt @@ -2,6 +2,7 @@ package com.mikepenz.markdown.utils import org.intellij.markdown.IElementType import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.getTextInNode /** * Tag used to indicate an url for inline content. Required for click handling. @@ -35,3 +36,11 @@ internal fun ASTNode.findChildOfTypeRecursive(type: IElementType): ASTNode? { * E.g. we don't want to render the brackets of a link */ internal fun List.innerList(): List = this.subList(1, this.size - 1) + +internal fun ASTNode.getUnescapedTextInNode(allFileText: CharSequence): String { + val escapedText = getTextInNode(allFileText).toString() + return EntityConverter.replaceEntities(escapedText, + processEntities = false, + processEscapes = true + ) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d6f095c..1c1d82c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,7 @@ include(":multiplatform-markdown-renderer-m2") include(":multiplatform-markdown-renderer-m3") include(":multiplatform-markdown-renderer-coil2") include(":multiplatform-markdown-renderer-coil3") +include(":multiplatform-markdown-renderer-code") include(":app") include(":app-desktop")