diff --git a/README.md b/README.md index 82779fe..8880a5c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ ## Compose Markdown ```kotlin -Buffer().markdown { +markdown { H1("Hello, Compose-Markdown!") Quote( - modifier = Modifier.clickable(link = "https://github.com/jisungbin/compose-markdown"), + modifier = Modifier.clickable("https://github.com/jisungbin/compose-markdown"), text = buildAnnotatedString { append("Build ") withStyle(TextStyle(italic = true)) { append("Markdown") } @@ -17,7 +17,3 @@ Buffer().markdown { ``` WIP - -### Limitations - -- Text larger than 8 KB might not output properly. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8f53ea..ffe64c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,6 @@ kotlin-coroutines = "1.9.0-RC" # K2 compose-runtime = "1.6.8" androidx-annotation = "1.8.0" -okio = "3.9.0" test-assertk = "0.28.1" @@ -30,7 +29,6 @@ compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = " compose-uiutil = { module = "androidx.compose.ui:ui-util", version.ref = "compose-runtime" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } -okio = { module = "com.squareup.okio:okio", version.ref = "okio" } test-assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "test-assertk" } test-kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlin-coroutines" } diff --git a/markdown-runtime/build.gradle.kts b/markdown-runtime/build.gradle.kts index 57e87c9..1da6704 100644 --- a/markdown-runtime/build.gradle.kts +++ b/markdown-runtime/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { implementation(libs.androidx.annotation) implementation(libs.compose.runtime) - api(libs.okio) api(libs.kotlin.coroutines) testImplementation(kotlin("test-junit5")) diff --git a/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/Markdown.kt b/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/Markdown.kt index 53dc965..82f5774 100644 --- a/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/Markdown.kt +++ b/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/Markdown.kt @@ -70,8 +70,5 @@ public suspend fun markdown( return buildString { appendLine(root.draw(options)) append(footnotes.draw(options)) - }.also { - root.close() - footnotes.close() } } diff --git a/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/MarkdownNode.kt b/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/MarkdownNode.kt index 9f72fa5..e6a6493 100644 --- a/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/MarkdownNode.kt +++ b/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/MarkdownNode.kt @@ -8,18 +8,15 @@ package land.sungbin.markdown.runtime import androidx.compose.runtime.collection.MutableVector -import okio.Buffer -import okio.BufferedSource -import okio.Closeable import org.jetbrains.annotations.TestOnly // TODO documentation public class MarkdownNode( - private val source: ((MarkdownOptions) -> BufferedSource)? = null, + private val source: ((MarkdownOptions) -> String)? = null, private val kind: MarkdownKind = MarkdownKind.TEXT, private val contentKind: MarkdownKind? = null, private val contentTag: ((index: Int, MarkdownOptions) -> String)? = null, -) : Closeable, Cloneable { +) { internal val children = MutableVectorWithMutationTracking(MutableVector(capacity = 20)) { check(kind.layout) { "Children can be added only to a group or footnote node." } } @@ -71,42 +68,39 @@ public class MarkdownNode( return contentTag!!.invoke(index, options) } - internal fun draw(options: MarkdownOptions): BufferedSource = Buffer().apply { - when { - text -> writeStringMarkdown(options) - group -> writeGroupMarkdown(options) - footnote -> writeFootnoteMarkdown(options) - } - } - - private fun Buffer.writeStringMarkdown(options: MarkdownOptions) { - writeAll(source!!.invoke(options)) + internal fun draw(options: MarkdownOptions): String = when { + text -> drawStringMarkdown(options) + group -> drawGroupMarkdown(options) + footnote -> drawFootnoteMarkdown(options) + else -> runtimeError { "[draw] unreachable code: $kind" } } - private fun Buffer.writeFootnoteMarkdown(options: MarkdownOptions) { - var tag: String? = null + private fun drawStringMarkdown(options: MarkdownOptions): String = + source!!.invoke(options) - children.forEach { child -> - if (tag == null) tag = tag(child.index, options) - val source = child.draw(options) - while (!source.exhausted()) { - val line = source.readUtf8Line() ?: break - writeUtf8(tag!!).writeUtf8(line).writeByte(NEW_LINE) + private fun drawFootnoteMarkdown(options: MarkdownOptions) = buildString { + var tag = tag(children.vector.firstOrNull()?.index ?: return@buildString, options) + children.vector.forEach { child -> + val source = child.draw(options).lineSequence() + for (line in source) { + append(tag).append(line).append(NEW_LINE) if (tag != FOOTNOTE_INDENT) tag = FOOTNOTE_INDENT } } } - private fun Buffer.writeGroupMarkdown(options: MarkdownOptions) { - children.forEach { child -> - val source = child.draw(options) + private fun drawGroupMarkdown(options: MarkdownOptions) = buildString { + val lastChildIndex = children.vector.lastIndex + children.vector.forEachIndexed { index, child -> val tag = tag(child.index, options) var touched = false - while (!source.exhausted()) { - val line = source.readUtf8Line() ?: break + val source = child.draw(options).lineSequence().iterator() + while (source.hasNext()) { + val line = source.next() val prefix = (contentKind!! + child.kind).prefix(tag = tag, forceConcat = !touched) - writeUtf8(prefix).writeUtf8(line).writeByte(NEW_LINE) - touched = true + append(prefix).append(line) + if (index != lastChildIndex || source.hasNext()) append(NEW_LINE) + if (!touched) touched = true } } } @@ -118,19 +112,10 @@ public class MarkdownNode( else -> runtimeError { "[prefix] unreachable code: $this" } } - override fun toString(): String = clone().draw(MarkdownOptions.Default).readUtf8() - - public override fun clone(): MarkdownNode = MarkdownNode(source, kind, contentKind, contentTag).also { clone -> - children.forEach { child -> clone.children.add(child) } - } - - override fun close() { - children.forEach { it.close() } - children.clear() - } + override fun toString(): String = draw(MarkdownOptions.Default) private companion object { - private const val NEW_LINE = '\n'.code + private const val NEW_LINE = '\n' private const val FOOTNOTE_INDENT = " " } } diff --git a/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/MutableVectorWithMutationTracking.kt b/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/MutableVectorWithMutationTracking.kt index 40d069e..ccff981 100644 --- a/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/MutableVectorWithMutationTracking.kt +++ b/markdown-runtime/src/main/kotlin/land/sungbin/markdown/runtime/MutableVectorWithMutationTracking.kt @@ -20,6 +20,4 @@ internal class MutableVectorWithMutationTracking( private val onVectorMutated: () -> Unit, ) { inline fun add(element: T) = vector.add(element).also { onVectorMutated() } - inline fun clear() = if (vector.isNotEmpty()) vector.clear().also { onVectorMutated() } else Unit - inline fun forEach(block: (T) -> Unit) = vector.forEach(block) } diff --git a/markdown-runtime/src/test/kotlin/land/sungbin/markdown/MarkdownNodeTest.kt b/markdown-runtime/src/test/kotlin/land/sungbin/markdown/MarkdownNodeTest.kt index 47dae8e..796f36a 100644 --- a/markdown-runtime/src/test/kotlin/land/sungbin/markdown/MarkdownNodeTest.kt +++ b/markdown-runtime/src/test/kotlin/land/sungbin/markdown/MarkdownNodeTest.kt @@ -11,26 +11,17 @@ import assertk.assertFailure import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo -import kotlin.test.AfterTest import kotlin.test.Test import land.sungbin.markdown.runtime.MarkdownKind import land.sungbin.markdown.runtime.MarkdownNode import land.sungbin.markdown.runtime.MarkdownOptions -import okio.Buffer class MarkdownNodeTest { - private val worldSource = Buffer().writeUtf8("Hello, World!\nMorning, World!\nBye, World!") - private val worldSource2 = Buffer().writeUtf8("Hello, World2!\nMorning, World2!\nBye, World2!") + private val worldSource = "Hello, World!\nMorning, World!\nBye, World!" + private val worldSource2 = "Hello, World2!\nMorning, World2!\nBye, World2!" private lateinit var node: MarkdownNode - @AfterTest fun cleanup() { - if (::node.isInitialized) try { - node.close() - } catch (_: Exception) { - } - } - @Test fun markdownNodeShouldHaveTextOrLayoutKind() { assertFailure { node = MarkdownNode(kind = MarkdownKind.REPEATATION_PARENT_TAG) } .hasMessage( @@ -48,17 +39,17 @@ class MarkdownNodeTest { } @Test fun noneLayoutNodeShouldntHaveContentKind() { - assertFailure { node = MarkdownNode(source = { Buffer() }, contentKind = MarkdownKind.TEXT) } + assertFailure { node = MarkdownNode(source = { "" }, contentKind = MarkdownKind.TEXT) } .hasMessage("A node that is not a group or a footnote cannot have a 'contentKind'.") } @Test fun noneLayoutNodeShouldntHaveContentTag() { - assertFailure { node = MarkdownNode(source = { Buffer() }, contentTag = { _, _ -> "" }) } + assertFailure { node = MarkdownNode(source = { "" }, contentTag = { _, _ -> "" }) } .hasMessage("A node that is not a group or a footnote cannot have a 'contentTag'.") } @Test fun layoutNodeShouldntHaveSource() { - assertFailure { node = MarkdownNode(source = { Buffer() }, kind = MarkdownKind.GROUP) } + assertFailure { node = MarkdownNode(source = { "" }, kind = MarkdownKind.GROUP) } .hasMessage( "A node that is a group or a footnote cannot have its own 'source'. " + "Only children are allowed to be the source of a 'source'.", @@ -77,8 +68,8 @@ class MarkdownNodeTest { @Test fun childrenCanBeAddedOnlyToLayoutNode() { assertFailure { - node = MarkdownNode(source = { Buffer() }) - node.children.add(MarkdownNode(source = { Buffer() })) + node = MarkdownNode(source = { "" }) + node.children.add(MarkdownNode(source = { "" })) } .hasMessage("Children can be added only to a group or footnote node.") } @@ -86,7 +77,7 @@ class MarkdownNodeTest { @Test fun drawingText() { node = MarkdownNode(source = { worldSource }) - assertThat(node.draw(MarkdownOptions()).readUtf8()) + assertThat(node.draw(MarkdownOptions())) .isEqualTo("Hello, World!\nMorning, World!\nBye, World!") } @@ -101,7 +92,7 @@ class MarkdownNodeTest { contentTag = { _, _ -> "[^T]: " }, ) - assertThat(node.draw(MarkdownOptions()).readUtf8()) + assertThat(node.draw(MarkdownOptions())) .isEqualTo( "[^T]: Hello, World!\n Morning, World!\n Bye, World!\n" + " Hello, World2!\n Morning, World2!\n Bye, World2!\n", @@ -119,10 +110,10 @@ class MarkdownNodeTest { contentTag = { _, _ -> "P. " }, ) - assertThat(node.draw(MarkdownOptions()).readUtf8()) + assertThat(node.draw(MarkdownOptions())) .isEqualTo( "P. Hello, World!\nP. Morning, World!\nP. Bye, World!\n" + - "P. Hello, World2!\nP. Morning, World2!\nP. Bye, World2!\n", + "P. Hello, World2!\nP. Morning, World2!\nP. Bye, World2!", ) } @@ -145,7 +136,7 @@ class MarkdownNodeTest { contentTag = { _, _ -> "> " }, ) - assertThat(node.draw(MarkdownOptions()).readUtf8()) + assertThat(node.draw(MarkdownOptions())) .isEqualTo( """ > Hello, World! @@ -154,7 +145,6 @@ class MarkdownNodeTest { > - Hello, World2! > - Morning, World2! > - Bye, World2! - """.trimIndent(), ) } @@ -162,7 +152,7 @@ class MarkdownNodeTest { @Test fun drawingGroupInNestedGroup() { val nestedChildrenNodes = MarkdownNode( children = List(3) { actualIndex -> - MarkdownNode(source = { Buffer().writeUtf8("My ordered list!\nMy ordered list's new line!") }) + MarkdownNode(source = { "My ordered list!\nMy ordered list's new line!" }) .apply { index = actualIndex } }, kind = MarkdownKind.GROUP + MarkdownKind.REPEATATION_PARENT_TAG, @@ -181,7 +171,7 @@ class MarkdownNodeTest { ) node = MarkdownNode( children = listOf( - MarkdownNode(source = { Buffer().writeUtf8("Hello!") }), + MarkdownNode(source = { "Hello!" }), childrenNodes, ), kind = MarkdownKind.GROUP, @@ -189,7 +179,7 @@ class MarkdownNodeTest { contentTag = { _, _ -> "" }, ) - assertThat(node.draw(MarkdownOptions()).readUtf8()) + assertThat(node.draw(MarkdownOptions())) .isEqualTo( """ Hello! @@ -205,7 +195,6 @@ class MarkdownNodeTest { > Hello, World2! > Morning, World2! > Bye, World2! - """.trimIndent(), ) } diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/singletons.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/EmptyUpdater.kt similarity index 84% rename from markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/singletons.kt rename to markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/EmptyUpdater.kt index e923b80..453fa8d 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/singletons.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/EmptyUpdater.kt @@ -8,9 +8,6 @@ package land.sungbin.markdown.ui import androidx.compose.runtime.Updater -import okio.Buffer - -internal val bufferCursor = Buffer.UnsafeCursor() @PublishedApi internal data object EmptyUpdater : (Updater) -> Unit { diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/image/Image.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/image/Image.kt index dc2d09d..02e11f3 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/image/Image.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/image/Image.kt @@ -7,41 +7,36 @@ package land.sungbin.markdown.ui.image +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.NonRestartableComposable import land.sungbin.markdown.runtime.MarkdownComposable import land.sungbin.markdown.ui.modifier.Modifier -import land.sungbin.markdown.ui.text.AbstractText import land.sungbin.markdown.ui.text.Text import land.sungbin.markdown.ui.unit.Size -@PublishedApi -internal class ImageText( +@VisibleForTesting +internal fun buildImageText( url: String, size: Size? = null, alt: String? = null, -) : AbstractText() { - init { - sink { - writeUtf8("") - } +): String = buildString { + append("") } -@Suppress("NOTHING_TO_INLINE") @[Composable NonRestartableComposable MarkdownComposable] -public inline fun Image( +public fun Image( url: String, size: Size? = null, alt: String? = null, modifier: Modifier = Modifier, ) { - Text(modifier = modifier, value = ImageText(url, size, alt)) + Text(modifier = modifier, value = buildImageText(url, size, alt)) } diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/list/List.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/list/List.kt index 07f6a54..7a3702c 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/list/List.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/list/List.kt @@ -26,7 +26,7 @@ public inline fun List( MarkdownNode( kind = MarkdownKind.GROUP, contentKind = MarkdownKind.ANY, - contentTag = { index, _ -> if (ordered) "${index + 1}. " else "* " }, + contentTag = { index, _ -> if (ordered) "${index + 1}. " else "- " }, ) }, update = EmptyUpdater, diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/modifier/Modifier.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/modifier/Modifier.kt index f00a62a..4133c16 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/modifier/Modifier.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/modifier/Modifier.kt @@ -10,7 +10,6 @@ package land.sungbin.markdown.ui.modifier import androidx.compose.runtime.Stable import land.sungbin.markdown.runtime.MarkdownOptions import land.sungbin.markdown.ui.text.TextTransformer -import okio.BufferedSink @Stable public sealed interface Modifier : Collection, RandomAccess { @@ -45,9 +44,9 @@ public infix fun Modifier.then(transformer: TextTransformer): Modifier { // TODO not cloning the sink -> Documentation required @PublishedApi -internal fun Modifier.applyTo(options: MarkdownOptions, sink: BufferedSink): BufferedSink { - if (isEmpty()) return sink - var acc = sink +internal fun Modifier.applyTo(options: MarkdownOptions, value: String): String { + if (isEmpty()) return value + var acc = value repeat(size) { index -> acc = get(index).transform(options, acc) } return acc } diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/AbstractText.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/AbstractText.kt deleted file mode 100644 index 42abe47..0000000 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/AbstractText.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Developed by Ji Sungbin 2024. - * - * Licensed under the MIT. - * Please see full license: https://github.com/jisungbin/compose-markdown/blob/main/LICENSE - */ - -package land.sungbin.markdown.ui.text - -import androidx.compose.runtime.Stable -import okio.Buffer -import okio.BufferedSink -import okio.BufferedSource -import okio.ByteString - -@Stable -public abstract class AbstractText : CharSequence { - @PublishedApi - internal val buffer: Buffer = Buffer() - - final override val length: Int get() = buffer.size.toInt() - - final override fun get(index: Int): Char = buffer[index.toLong()].toInt().toChar() - - public inline fun source(action: BufferedSource.() -> T): T = action(buffer) - public inline fun sink(action: BufferedSink.() -> T): T = action(buffer) - - final override fun subSequence(startIndex: Int, endIndex: Int): CharSequence = - buffer.snapshot().substring(startIndex, endIndex).asCharSequence() - - override fun toString(): String = buffer.snapshot().utf8() - - override fun hashCode(): Int = buffer.hashCode() - override fun equals(other: Any?): Boolean = buffer == other - - protected fun ByteString.asCharSequence(): CharSequence = object : CharSequence { - override val length: Int get() = this@asCharSequence.size - - override fun get(index: Int): Char = this@asCharSequence[index].toInt().toChar() - - override fun subSequence(startIndex: Int, endIndex: Int): CharSequence = - this@asCharSequence.substring(startIndex, endIndex).asCharSequence() - - override fun toString(): String = this@asCharSequence.utf8() - } -} diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/AnnotatedString.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/AnnotatedString.kt index b0ce126..0312333 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/AnnotatedString.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/AnnotatedString.kt @@ -11,18 +11,35 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.util.fastFold import land.sungbin.markdown.runtime.MarkdownOptions -import okio.Buffer @Immutable -public data class AnnotatedString(public val styledTexts: List) : AbstractText() { - init { - val styled = styledTexts.fastFold(Buffer()) { acc, (text, style) -> - val styled = style.transform(MarkdownOptions.Default, bufferOf(text)) - acc.apply { writeAll(styled.buffer) } - } - sink { writeAll(styled) } +public class AnnotatedString(@Suppress("MemberVisibilityCanBePrivate") public val styledTexts: List) : CharSequence { + private val backing = run { + styledTexts + .fastFold(StringBuilder()) { acc, (text, style) -> + val styled = style.transform(MarkdownOptions.Default, text.toString()) + acc.append(styled) + } + .toString() + } + + override val length: Int = backing.length + + override fun get(index: Int): Char = backing[index] + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence = + backing.subSequence(startIndex, endIndex) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CharSequence) return false + return backing == other.toString() } + override fun hashCode(): Int = backing.hashCode() + + override fun toString(): String = backing + @Immutable public data class TextAndStyle( public val text: CharSequence, @@ -44,11 +61,6 @@ public data class AnnotatedString(public val styledTexts: List) : @PublishedApi internal fun build(): AnnotatedString = AnnotatedString(texts) } - - private fun bufferOf(value: CharSequence): Buffer { - if (value is AbstractText) return value.buffer.clone() - return Buffer().writeUtf8(value.toString()) - } } @Stable diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/HeaderText.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/HeaderText.kt index 958ff8b..b3e29da 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/HeaderText.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/HeaderText.kt @@ -12,9 +12,8 @@ import androidx.compose.runtime.NonRestartableComposable import land.sungbin.markdown.runtime.MarkdownComposable import land.sungbin.markdown.ui.modifier.Modifier -@Suppress("NOTHING_TO_INLINE") @[Composable NonRestartableComposable MarkdownComposable] -public inline fun Header( +public fun Header( level: Int, text: CharSequence, modifier: Modifier = Modifier, diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/Text.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/Text.kt index ca4354f..b63dd85 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/Text.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/Text.kt @@ -13,26 +13,15 @@ import androidx.compose.runtime.NonRestartableComposable import land.sungbin.markdown.runtime.MarkdownApplier import land.sungbin.markdown.runtime.MarkdownComposable import land.sungbin.markdown.runtime.MarkdownNode -import land.sungbin.markdown.runtime.MarkdownOptions import land.sungbin.markdown.ui.EmptyUpdater import land.sungbin.markdown.ui.modifier.Modifier import land.sungbin.markdown.ui.modifier.applyTo -import okio.Buffer -import okio.BufferedSource - -@Suppress("NOTHING_TO_INLINE") -@PublishedApi -internal inline fun sourceOf(modifier: Modifier, value: String): (MarkdownOptions) -> BufferedSource = - { options -> - val source = modifier.applyTo(options, Buffer().writeUtf8(value)) - source.buffer - } @Suppress("NOTHING_TO_INLINE") @[Composable NonRestartableComposable MarkdownComposable] public inline fun Text(value: CharSequence, modifier: Modifier = Modifier) { ComposeNode( - factory = { MarkdownNode(source = sourceOf(modifier, value.toString())) }, + factory = { MarkdownNode(source = { options -> modifier.applyTo(options, value.toString()) }) }, update = EmptyUpdater, ) } diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextStyle.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextStyle.kt index ca9c867..bfb2b10 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextStyle.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextStyle.kt @@ -10,8 +10,6 @@ package land.sungbin.markdown.ui.text import androidx.compose.runtime.Immutable import androidx.compose.ui.util.fastFold import land.sungbin.markdown.runtime.MarkdownOptions -import land.sungbin.markdown.ui.bufferCursor -import okio.BufferedSink @Immutable public data class TextStyle( @@ -37,8 +35,8 @@ public data class TextStyle( if (monospace) add(TextStyleDefinition.Monospace) } - override fun transform(options: MarkdownOptions, sink: BufferedSink): BufferedSink = - transformers.fastFold(sink) { acc, transformer -> transformer.transform(options, acc) } + override fun transform(options: MarkdownOptions, value: String): String = + transformers.fastFold(value) { acc, transformer -> transformer.transform(options, acc) } public companion object { public val Default: TextStyle = TextStyle() @@ -46,37 +44,9 @@ public data class TextStyle( } private object UppercaseTransformer : TextTransformer { - private const val UPPER_CASE_OFFSET = ('A'.code - 'a'.code).toByte() - - override fun transform(options: MarkdownOptions, sink: BufferedSink): BufferedSink = sink.apply { - buffer.readAndWriteUnsafe(bufferCursor).use { cursor -> - cursor.seek(0) - repeat(cursor.end) { offset -> - cursor.data!![offset] = cursor.data!![offset].uppercase() - } - } - } - - private fun Byte.uppercase(): Byte { - if (this !in 'a'.code.toByte()..'z'.code.toByte()) return this - return (this + UPPER_CASE_OFFSET).toByte() - } + override fun transform(options: MarkdownOptions, value: String): String = value.uppercase() } private object LowercaseTransformer : TextTransformer { - private const val LOWER_CASE_OFFSET = ('a'.code - 'A'.code).toByte() - - override fun transform(options: MarkdownOptions, sink: BufferedSink): BufferedSink = sink.apply { - buffer.readAndWriteUnsafe(bufferCursor).use { cursor -> - cursor.seek(0) - repeat(cursor.end) { offset -> - cursor.data!![offset] = cursor.data!![offset].lowercase() - } - } - } - - private fun Byte.lowercase(): Byte { - if (this !in 'A'.code.toByte()..'Z'.code.toByte()) return this - return (this + LOWER_CASE_OFFSET).toByte() - } + override fun transform(options: MarkdownOptions, value: String): String = value.lowercase() } diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextStyleDefinition.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextStyleDefinition.kt index 5dc385b..0b1b68f 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextStyleDefinition.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextStyleDefinition.kt @@ -9,30 +9,19 @@ package land.sungbin.markdown.ui.text import androidx.compose.runtime.Immutable import land.sungbin.markdown.runtime.MarkdownOptions -import land.sungbin.markdown.ui.bufferCursor -import okio.BufferedSink @Immutable public data class TextStyleDefinition(public val open: String, public val end: String = open) : TextTransformer { - override fun transform(options: MarkdownOptions, sink: BufferedSink): BufferedSink = sink.apply { - buffer.readAndWriteUnsafe(bufferCursor).use { cursor -> - val previousSize = cursor.resizeBuffer(buffer.size + open.length + end.length) - cursor.seek(0) - cursor.data!!.copyInto( - destination = cursor.data!!, - destinationOffset = open.length, - startIndex = cursor.start, - endIndex = previousSize.toInt(), - ) - repeat(open.length) { offset -> - cursor.data!![cursor.start + offset] = open[offset].code.toByte() + override fun transform(options: MarkdownOptions, value: String): String = + value.toCharArray( + destination = CharArray(open.length + value.length + end.length), + destinationOffset = open.length, + ) + .also { new -> + repeat(open.length) { new[it] = open[it] } + (open.length + value.length).let { pos -> repeat(end.length) { new[pos + it] = end[it] } } } - cursor.seek(cursor.buffer!!.size - end.length) - repeat(end.length) { offset -> - cursor.data!![cursor.start + offset] = end[offset].code.toByte() - } - } - } + .concatToString() public companion object { public val Bold: TextStyleDefinition = TextStyleDefinition("**") diff --git a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextTransformer.kt b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextTransformer.kt index a474628..ab3a8b3 100644 --- a/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextTransformer.kt +++ b/markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/TextTransformer.kt @@ -9,9 +9,8 @@ package land.sungbin.markdown.ui.text import androidx.compose.runtime.Immutable import land.sungbin.markdown.runtime.MarkdownOptions -import okio.BufferedSink @Immutable public fun interface TextTransformer { - public fun transform(options: MarkdownOptions, sink: BufferedSink): BufferedSink + public fun transform(options: MarkdownOptions, value: String): String } diff --git a/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/extensions.kt b/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/extensions.kt index 6f48959..424d93a 100644 --- a/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/extensions.kt +++ b/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/extensions.kt @@ -8,16 +8,6 @@ package land.sungbin.markdown.ui import land.sungbin.markdown.runtime.MarkdownOptions -import land.sungbin.markdown.ui.text.AbstractText import land.sungbin.markdown.ui.text.TextTransformer -import okio.Buffer -class TextForTest(value: String? = null) : AbstractText() { - init { - if (value != null) { - sink { writeUtf8(value) } - } - } -} - -fun TextTransformer.transform(value: String) = transform(MarkdownOptions.Default, Buffer().apply { writeUtf8(value) }) +fun TextTransformer.transform(value: String) = transform(MarkdownOptions.Default, value) diff --git a/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/image/ImageTest.kt b/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/image/ImageTest.kt index 99ba6e1..444d770 100644 --- a/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/image/ImageTest.kt +++ b/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/image/ImageTest.kt @@ -14,14 +14,13 @@ import land.sungbin.markdown.ui.unit.FixedSize class ImageTest { @Test fun urlOnly() { - val image = ImageText("https://example.com/image.jpg") - assertThat(image.source { readUtf8() }) - .isEqualTo("") + val image = buildImageText("https://example.com/image.jpg") + assertThat(image).isEqualTo("") } @Test fun urlAndSize() { - val image = ImageText("https://example.com/image.jpg", size = FixedSize(100, 200)) - assertThat(image.source { readUtf8() }).isEqualTo( + val image = buildImageText("https://example.com/image.jpg", size = FixedSize(100, 200)) + assertThat(image).isEqualTo( "An image안녕하세요 ~~**잘가세요**~~") + assertThat(annotated).isEqualTo("Hello ~~**Bye**~~") } @Test fun annotatedBuilder() { @@ -56,7 +55,7 @@ class AnnotatedStringTest { "multipleUppercaseStyled" } } - assertThat(annotated.source { readUtf8() }).isEqualTo( + assertThat(annotated).isEqualTo( "" + "normal " + "**bold** " + diff --git a/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/text/TextStyleDefinitionTest.kt b/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/text/TextStyleDefinitionTest.kt index 33594d2..25d6870 100644 --- a/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/text/TextStyleDefinitionTest.kt +++ b/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/text/TextStyleDefinitionTest.kt @@ -14,27 +14,27 @@ import land.sungbin.markdown.ui.transform class TextStyleDefinitionTest { @Test fun bold() { - val styled = TextStyleDefinition.Bold.transform("안녕하세요 hello~") - assertThat(styled.buffer.readUtf8()).isEqualTo("**안녕하세요 hello~**") + val styled = TextStyleDefinition.Bold.transform("HELLO hello~") + assertThat(styled).isEqualTo("**HELLO hello~**") } @Test fun italic() { - val styled = TextStyleDefinition.Italic.transform("안녕하세요 hello~") - assertThat(styled.buffer.readUtf8()).isEqualTo("_안녕하세요 hello~_") + val styled = TextStyleDefinition.Italic.transform("HELLO hello~") + assertThat(styled).isEqualTo("_HELLO hello~_") } @Test fun strikethrough() { - val styled = TextStyleDefinition.Strikethrough.transform("안녕하세요 hello~") - assertThat(styled.buffer.readUtf8()).isEqualTo("~~안녕하세요 hello~~~") + val styled = TextStyleDefinition.Strikethrough.transform("HELLO hello~") + assertThat(styled).isEqualTo("~~HELLO hello~~~") } @Test fun underline() { - val styled = TextStyleDefinition.Unerline.transform("안녕하세요 hello~") - assertThat(styled.buffer.readUtf8()).isEqualTo("안녕하세요 hello~") + val styled = TextStyleDefinition.Unerline.transform("HELLO hello~") + assertThat(styled).isEqualTo("HELLO hello~") } @Test fun monospace() { - val styled = TextStyleDefinition.Monospace.transform("안녕하세요 hello~") - assertThat(styled.buffer.readUtf8()).isEqualTo("`안녕하세요 hello~`") + val styled = TextStyleDefinition.Monospace.transform("HELLO hello~") + assertThat(styled).isEqualTo("`HELLO hello~`") } } diff --git a/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/text/TextStyleTest.kt b/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/text/TextStyleTest.kt index 113705e..cab4202 100644 --- a/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/text/TextStyleTest.kt +++ b/markdown-ui/src/test/kotlin/land/sungbin/markdown/ui/text/TextStyleTest.kt @@ -23,9 +23,9 @@ class TextStyleTest { underline = true, monospace = true, uppercase = true, - ).transform("안녕하세요 hello~") + ).transform("hello~") - assertThat(styled.buffer.readUtf8()).isEqualTo("`~~_**안녕하세요 HELLO~**_~~`") + assertThat(styled).isEqualTo("`~~_**HELLO~**_~~`") } @Test fun cannotUppercaseAndLowercaseAtTheSameTime() { @@ -35,6 +35,6 @@ class TextStyleTest { @Test fun lowercaseStyled() { val styled = TextStyle(lowercase = true).transform("HELLO") - assertThat(styled.buffer.readUtf8()).isEqualTo("hello") + assertThat(styled).isEqualTo("hello") } }