├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── se │ │ └── hellsoft │ │ └── markdowncomposer │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── serios.jpg │ ├── java │ │ └── se │ │ │ └── hellsoft │ │ │ └── markdowncomposer │ │ │ ├── MainActivity.kt │ │ │ ├── MarkdownComposer.kt │ │ │ ├── Sample.kt │ │ │ └── ui │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── se │ └── hellsoft │ └── markdowncomposer │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Markdown Composer -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 20 | 24 | 25 | 137 | 138 | 140 | 141 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Erik Hellman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PoC Markdown rendered for Jetpack Compose 2 | 3 | This project is a small proof-of-concept implementation for rendering Markdown text using Jetpack Compose. 4 | 5 | It depends on the CommonMarks library (Java) and the Accompanist library by Chris Banes (for displaying images). -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 31 8 | 9 | defaultConfig { 10 | applicationId "se.hellsoft.markdowncomposer" 11 | minSdkVersion 23 12 | targetSdkVersion 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | useIR = true 32 | } 33 | buildFeatures { 34 | compose true 35 | } 36 | composeOptions { 37 | kotlinCompilerExtensionVersion compose_version 38 | kotlinCompilerVersion kotlin_version 39 | } 40 | } 41 | 42 | dependencies { 43 | 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 45 | implementation 'androidx.core:core-ktx:1.7.0' 46 | implementation 'androidx.appcompat:appcompat:1.4.0' 47 | implementation 'com.google.android.material:material:1.4.0' 48 | implementation "androidx.compose.ui:ui:$compose_version" 49 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 50 | implementation "androidx.compose.material:material:$compose_version" 51 | implementation "androidx.activity:activity-compose:1.4.0" 52 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0' 53 | implementation "com.atlassian.commonmark:commonmark:0.15.2" 54 | implementation 'io.coil-kt:coil:1.4.0' 55 | implementation "com.google.accompanist:accompanist-coil:0.15.0" 56 | testImplementation 'junit:junit:4.13' 57 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 58 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 59 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/se/hellsoft/markdowncomposer/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package se.hellsoft.markdowncomposer 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("se.hellsoft.markdowncomposer", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/assets/serios.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/assets/serios.jpg -------------------------------------------------------------------------------- /app/src/main/java/se/hellsoft/markdowncomposer/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package se.hellsoft.markdowncomposer 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.foundation.gestures.detectTapGestures 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.foundation.verticalScroll 12 | import androidx.compose.material.MaterialTheme 13 | import androidx.compose.material.Surface 14 | import androidx.compose.material.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.input.pointer.pointerInput 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.dp 24 | import org.commonmark.node.Document 25 | import org.commonmark.parser.Parser 26 | import se.hellsoft.markdowncomposer.ui.MarkdownComposerTheme 27 | 28 | class MainActivity : AppCompatActivity() { 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | val parser = Parser.builder().build() 32 | val root = parser.parse(MIXED_MD) as Document 33 | setContent { 34 | var render by remember { mutableStateOf(true) } 35 | MarkdownComposerTheme { 36 | Surface( 37 | color = MaterialTheme.colors.background, 38 | modifier = Modifier.pointerInput(Unit) { 39 | detectTapGestures( 40 | onDoubleTap = { 41 | render = !render 42 | } 43 | ) 44 | }) { 45 | Box(modifier = Modifier.padding(start = 16.dp, end = 16.dp)) { 46 | Column(modifier = Modifier.verticalScroll(rememberScrollState())) { 47 | if (render) { 48 | MDDocument(root) 49 | } else { 50 | Text(MIXED_MD) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | @Preview(showBackground = true) 61 | @Composable 62 | fun DefaultPreview() { 63 | val parser = Parser.builder().build() 64 | val root = parser.parse(MIXED_MD) as Document 65 | MarkdownComposerTheme { 66 | Surface( 67 | color = MaterialTheme.colors.background 68 | ) { 69 | Box { 70 | MDDocument(root) 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/se/hellsoft/markdowncomposer/MarkdownComposer.kt: -------------------------------------------------------------------------------- 1 | package se.hellsoft.markdowncomposer 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.gestures.detectTapGestures 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.text.InlineTextContent 10 | import androidx.compose.foundation.text.appendInlineContent 11 | import androidx.compose.material.Colors 12 | import androidx.compose.material.MaterialTheme 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.drawBehind 20 | import androidx.compose.ui.geometry.Offset 21 | import androidx.compose.ui.input.pointer.pointerInput 22 | import androidx.compose.ui.platform.LocalUriHandler 23 | import androidx.compose.ui.text.AnnotatedString 24 | import androidx.compose.ui.text.Placeholder 25 | import androidx.compose.ui.text.PlaceholderVerticalAlign 26 | import androidx.compose.ui.text.SpanStyle 27 | import androidx.compose.ui.text.TextLayoutResult 28 | import androidx.compose.ui.text.TextStyle 29 | import androidx.compose.ui.text.buildAnnotatedString 30 | import androidx.compose.ui.text.font.FontFamily 31 | import androidx.compose.ui.text.font.FontStyle 32 | import androidx.compose.ui.text.font.FontWeight 33 | import androidx.compose.ui.text.style.TextDecoration 34 | import androidx.compose.ui.unit.dp 35 | import coil.compose.rememberImagePainter 36 | import coil.size.OriginalSize 37 | import org.commonmark.node.BlockQuote 38 | import org.commonmark.node.BulletList 39 | import org.commonmark.node.Code 40 | import org.commonmark.node.Document 41 | import org.commonmark.node.Emphasis 42 | import org.commonmark.node.FencedCodeBlock 43 | import org.commonmark.node.HardLineBreak 44 | import org.commonmark.node.Heading 45 | import org.commonmark.node.Image 46 | import org.commonmark.node.IndentedCodeBlock 47 | import org.commonmark.node.Link 48 | import org.commonmark.node.ListBlock 49 | import org.commonmark.node.Node 50 | import org.commonmark.node.OrderedList 51 | import org.commonmark.node.Paragraph 52 | import org.commonmark.node.StrongEmphasis 53 | import org.commonmark.node.Text 54 | import org.commonmark.node.ThematicBreak 55 | 56 | /** 57 | * These functions will render a tree of Markdown nodes parsed with CommonMark. 58 | * Images will be rendered using Chris Banes Accompanist library (which uses Coil) 59 | * 60 | * To use this, you need the following two dependencies: 61 | * implementation "com.atlassian.commonmark:commonmark:0.15.2" 62 | * implementation "dev.chrisbanes.accompanist:accompanist-coil:0.2.0" 63 | * 64 | * The following is an example of how to use this: 65 | * ``` 66 | * val parser = Parser.builder().build() 67 | * val root = parser.parse(MIXED_MD) as Document 68 | * val markdownComposer = MarkdownComposer() 69 | * 70 | * MarkdownComposerTheme { 71 | * MDDocument(root) 72 | * } 73 | * ``` 74 | */ 75 | private const val TAG_URL = "url" 76 | private const val TAG_IMAGE_URL = "imageUrl" 77 | 78 | @Composable 79 | fun MDDocument(document: Document) { 80 | MDBlockChildren(document) 81 | } 82 | 83 | @Composable 84 | fun MDHeading(heading: Heading, modifier: Modifier = Modifier) { 85 | val style = when (heading.level) { 86 | 1 -> MaterialTheme.typography.h1 87 | 2 -> MaterialTheme.typography.h2 88 | 3 -> MaterialTheme.typography.h3 89 | 4 -> MaterialTheme.typography.h4 90 | 5 -> MaterialTheme.typography.h5 91 | 6 -> MaterialTheme.typography.h6 92 | else -> { 93 | // Invalid header... 94 | MDBlockChildren(heading) 95 | return 96 | } 97 | } 98 | 99 | val padding = if (heading.parent is Document) 8.dp else 0.dp 100 | Box(modifier = modifier.padding(bottom = padding)) { 101 | val text = buildAnnotatedString { 102 | appendMarkdownChildren(heading, MaterialTheme.colors) 103 | } 104 | MarkdownText(text, style) 105 | } 106 | } 107 | 108 | @Composable 109 | fun MDParagraph(paragraph: Paragraph, modifier: Modifier = Modifier) { 110 | if (paragraph.firstChild is Image && paragraph.firstChild == paragraph.lastChild) { 111 | // Paragraph with single image 112 | MDImage(paragraph.firstChild as Image, modifier) 113 | } else { 114 | val padding = if (paragraph.parent is Document) 8.dp else 0.dp 115 | Box(modifier = modifier.padding(bottom = padding)) { 116 | val styledText = buildAnnotatedString { 117 | pushStyle(MaterialTheme.typography.body1.toSpanStyle()) 118 | appendMarkdownChildren(paragraph, MaterialTheme.colors) 119 | pop() 120 | } 121 | MarkdownText(styledText, MaterialTheme.typography.body1) 122 | } 123 | } 124 | } 125 | 126 | @Composable 127 | fun MDImage(image: Image, modifier: Modifier = Modifier) { 128 | Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { 129 | Image( 130 | painter = rememberImagePainter( 131 | data = image.destination, 132 | builder = { 133 | size(OriginalSize) 134 | }, 135 | ), 136 | contentDescription = null, 137 | ) 138 | } 139 | } 140 | 141 | @Composable 142 | fun MDBulletList(bulletList: BulletList, modifier: Modifier = Modifier) { 143 | val marker = bulletList.bulletMarker 144 | MDListItems(bulletList, modifier = modifier) { 145 | val text = buildAnnotatedString { 146 | pushStyle(MaterialTheme.typography.body1.toSpanStyle()) 147 | append("$marker ") 148 | appendMarkdownChildren(it, MaterialTheme.colors) 149 | pop() 150 | } 151 | MarkdownText(text, MaterialTheme.typography.body1, modifier) 152 | } 153 | } 154 | 155 | @Composable 156 | fun MDOrderedList(orderedList: OrderedList, modifier: Modifier = Modifier) { 157 | var number = orderedList.startNumber 158 | val delimiter = orderedList.delimiter 159 | MDListItems(orderedList, modifier) { 160 | val text = buildAnnotatedString { 161 | pushStyle(MaterialTheme.typography.body1.toSpanStyle()) 162 | append("${number++}$delimiter ") 163 | appendMarkdownChildren(it, MaterialTheme.colors) 164 | pop() 165 | } 166 | MarkdownText(text, MaterialTheme.typography.body1, modifier) 167 | } 168 | } 169 | 170 | @Composable 171 | fun MDListItems( 172 | listBlock: ListBlock, 173 | modifier: Modifier = Modifier, 174 | item: @Composable (node: Node) -> Unit 175 | ) { 176 | val bottom = if (listBlock.parent is Document) 8.dp else 0.dp 177 | val start = if (listBlock.parent is Document) 0.dp else 8.dp 178 | Column(modifier = modifier.padding(start = start, bottom = bottom)) { 179 | var listItem = listBlock.firstChild 180 | while (listItem != null) { 181 | var child = listItem.firstChild 182 | while (child != null) { 183 | when (child) { 184 | is BulletList -> MDBulletList(child, modifier) 185 | is OrderedList -> MDOrderedList(child, modifier) 186 | else -> item(child) 187 | } 188 | child = child.next 189 | } 190 | listItem = listItem.next 191 | } 192 | } 193 | } 194 | 195 | @Composable 196 | fun MDBlockQuote(blockQuote: BlockQuote, modifier: Modifier = Modifier) { 197 | val color = MaterialTheme.colors.onBackground 198 | Box(modifier = modifier 199 | .drawBehind { 200 | drawLine( 201 | color = color, 202 | strokeWidth = 2f, 203 | start = Offset(12.dp.value, 0f), 204 | end = Offset(12.dp.value, size.height) 205 | ) 206 | } 207 | .padding(start = 16.dp, top = 4.dp, bottom = 4.dp)) { 208 | val text = buildAnnotatedString { 209 | pushStyle( 210 | MaterialTheme.typography.body1.toSpanStyle() 211 | .plus(SpanStyle(fontStyle = FontStyle.Italic)) 212 | ) 213 | appendMarkdownChildren(blockQuote, MaterialTheme.colors) 214 | pop() 215 | } 216 | Text(text, modifier) 217 | } 218 | } 219 | 220 | @Composable 221 | fun MDFencedCodeBlock(fencedCodeBlock: FencedCodeBlock, modifier: Modifier = Modifier) { 222 | val padding = if (fencedCodeBlock.parent is Document) 8.dp else 0.dp 223 | Box(modifier = modifier.padding(start = 8.dp, bottom = padding)) { 224 | Text( 225 | text = fencedCodeBlock.literal, 226 | style = TextStyle(fontFamily = FontFamily.Monospace), 227 | modifier = modifier 228 | ) 229 | } 230 | } 231 | 232 | @Composable 233 | fun MDIndentedCodeBlock(indentedCodeBlock: IndentedCodeBlock, modifier: Modifier = Modifier) { 234 | // Ignored 235 | } 236 | 237 | @Composable 238 | fun MDThematicBreak(thematicBreak: ThematicBreak, modifier: Modifier = Modifier) { 239 | //Ignored 240 | } 241 | 242 | @Composable 243 | fun MDBlockChildren(parent: Node) { 244 | var child = parent.firstChild 245 | while (child != null) { 246 | when (child) { 247 | is BlockQuote -> MDBlockQuote(child) 248 | is ThematicBreak -> MDThematicBreak(child) 249 | is Heading -> MDHeading(child) 250 | is Paragraph -> MDParagraph(child) 251 | is FencedCodeBlock -> MDFencedCodeBlock(child) 252 | is IndentedCodeBlock -> MDIndentedCodeBlock(child) 253 | is Image -> MDImage(child) 254 | is BulletList -> MDBulletList(child) 255 | is OrderedList -> MDOrderedList(child) 256 | } 257 | child = child.next 258 | } 259 | } 260 | 261 | fun AnnotatedString.Builder.appendMarkdownChildren( 262 | parent: Node, colors: Colors 263 | ) { 264 | var child = parent.firstChild 265 | while (child != null) { 266 | when (child) { 267 | is Paragraph -> appendMarkdownChildren(child, colors) 268 | is Text -> append(child.literal) 269 | is Image -> appendInlineContent(TAG_IMAGE_URL, child.destination) 270 | is Emphasis -> { 271 | pushStyle(SpanStyle(fontStyle = FontStyle.Italic)) 272 | appendMarkdownChildren(child, colors) 273 | pop() 274 | } 275 | is StrongEmphasis -> { 276 | pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) 277 | appendMarkdownChildren(child, colors) 278 | pop() 279 | } 280 | is Code -> { 281 | pushStyle(TextStyle(fontFamily = FontFamily.Monospace).toSpanStyle()) 282 | append(child.literal) 283 | pop() 284 | } 285 | is HardLineBreak -> { 286 | append("\n") 287 | } 288 | is Link -> { 289 | val underline = SpanStyle(colors.primary, textDecoration = TextDecoration.Underline) 290 | pushStyle(underline) 291 | pushStringAnnotation(TAG_URL, child.destination) 292 | appendMarkdownChildren(child, colors) 293 | pop() 294 | pop() 295 | } 296 | } 297 | child = child.next 298 | } 299 | } 300 | 301 | @Composable 302 | fun MarkdownText(text: AnnotatedString, style: TextStyle, modifier: Modifier = Modifier) { 303 | val uriHandler = LocalUriHandler.current 304 | val layoutResult = remember { mutableStateOf(null) } 305 | 306 | Text(text = text, 307 | modifier.pointerInput(Unit) { 308 | detectTapGestures { offset -> 309 | layoutResult.value?.let { layoutResult -> 310 | val position = layoutResult.getOffsetForPosition(offset) 311 | text.getStringAnnotations(position, position) 312 | .firstOrNull() 313 | ?.let { sa -> 314 | if (sa.tag == TAG_URL) { 315 | uriHandler.openUri(sa.item) 316 | } 317 | } 318 | } 319 | } 320 | }, 321 | style = style, 322 | inlineContent = mapOf( 323 | TAG_IMAGE_URL to InlineTextContent( 324 | Placeholder(style.fontSize, style.fontSize, PlaceholderVerticalAlign.Bottom) 325 | ) { 326 | Image( 327 | painter = rememberImagePainter( 328 | data = it, 329 | ), 330 | contentDescription = null, 331 | modifier = modifier, 332 | alignment = Alignment.Center 333 | ) 334 | 335 | } 336 | ), 337 | onTextLayout = { layoutResult.value = it } 338 | ) 339 | } -------------------------------------------------------------------------------- /app/src/main/java/se/hellsoft/markdowncomposer/Sample.kt: -------------------------------------------------------------------------------- 1 | package se.hellsoft.markdowncomposer 2 | 3 | import org.intellij.lang.annotations.Language 4 | 5 | @Language("Markdown") 6 | const val MIXED_MD = """ 7 | ### Markdown Header 8 | 9 | This is regular text without formatting in a single paragraph. 10 | 11 | ![Serious](file:///android_asset/serios.jpg) 12 | 13 | Images can also be inline: ![Serious](file:///android_asset/serios.jpg). [Links](http://hellsoft.se) and `inline code` also work. This *is* text __with__ inline styles for *__bold and italic__*. Those can be nested. 14 | 15 | Here is a code block: 16 | ```javascript 17 | function codeBlock() { 18 | return true; 19 | } 20 | ``` 21 | 22 | + Bullet 23 | + __Lists__ 24 | + Are 25 | + *Cool* 26 | 27 | 1. **First** 28 | 1. *Second* 29 | 1. Third 30 | 1. [Fourth is clickable](https://google.com) 31 | 1. And 32 | 1. Sublists 33 | 1. Mixed 34 | - With 35 | - Bullet 36 | - Lists 37 | 38 | 100) Lists 39 | 100) Can 40 | 100) Have 41 | 100) *Custom* 42 | 100) __Start__ 43 | 100) Numbers 44 | 45 | - List 46 | - Of 47 | - Items 48 | - With 49 | - Sublist 50 | 51 | > A blockquote is useful for quotes! 52 | 53 | """ 54 | -------------------------------------------------------------------------------- /app/src/main/java/se/hellsoft/markdowncomposer/ui/Color.kt: -------------------------------------------------------------------------------- 1 | package se.hellsoft.markdowncomposer.ui 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val purple200 = Color(0xFFBB86FC) 6 | val purple500 = Color(0xFF6200EE) 7 | val purple700 = Color(0xFF3700B3) 8 | val teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /app/src/main/java/se/hellsoft/markdowncomposer/ui/Shape.kt: -------------------------------------------------------------------------------- 1 | package se.hellsoft.markdowncomposer.ui 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/se/hellsoft/markdowncomposer/ui/Theme.kt: -------------------------------------------------------------------------------- 1 | package se.hellsoft.markdowncomposer.ui 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = purple200, 11 | primaryVariant = purple700, 12 | secondary = teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = purple500, 17 | primaryVariant = purple700, 18 | secondary = teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun MarkdownComposerTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { 32 | val colors = if (darkTheme) { 33 | DarkColorPalette 34 | } else { 35 | LightColorPalette 36 | } 37 | 38 | MaterialTheme( 39 | colors = colors, 40 | typography = typography, 41 | shapes = shapes, 42 | content = content 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/se/hellsoft/markdowncomposer/ui/Type.kt: -------------------------------------------------------------------------------- 1 | package se.hellsoft.markdowncomposer.ui 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikHellman/MarkdownComposer/2a74fc7897a8dd81518154e17ad59e2f4e48decf/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Markdown Composer 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |