Skip to content

Commit

Permalink
Adds FootnoteModifier and ClickableModifier
Browse files Browse the repository at this point in the history
  • Loading branch information
jisungbin committed Jul 1, 2024
1 parent 39e8d22 commit 3ae2367
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import org.jetbrains.annotations.TestOnly
public class MarkdownNode(
private val source: ((MarkdownOptions) -> String)? = null,
private val kind: MarkdownKind = MarkdownKind.TEXT,
private val contentKind: MarkdownKind? = null,
private val contentKind: MarkdownKind = MarkdownKind.ANY,
private val contentTag: ((index: Int, MarkdownOptions) -> String)? = null,
) {
internal val children = MutableVectorWithMutationTracking(MutableVector<MarkdownNode>(capacity = 20)) {
Expand All @@ -28,7 +28,7 @@ public class MarkdownNode(
@TestOnly internal constructor(
children: List<MarkdownNode>,
kind: MarkdownKind = MarkdownKind.GROUP,
contentKind: MarkdownKind,
contentKind: MarkdownKind = MarkdownKind.ANY,
contentTag: (index: Int, MarkdownOptions) -> String,
) : this(kind = kind, contentKind = contentKind, contentTag = contentTag) {
for (i in children.indices) {
Expand All @@ -47,15 +47,14 @@ public class MarkdownNode(
"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'."
}
if (group) require(contentKind != null) { "A node that is a group must have a 'contentKind'." }
require(contentTag != null) { "A node that is a group or a footnote must have a 'contentTag'." }
}
false -> {
require(source != null) {
"A MarkdownNode has been emitted as a MarkdownKind.TEXT, but the 'source' is null. " +
"Use a 'source' containing a markdown string instead of null."
}
require(contentKind == null) { "A node that is not a group or a footnote cannot have a 'contentKind'." }
require(contentKind == MarkdownKind.ANY) { "A node that is not a group or a footnote cannot have a 'contentKind'." }
require(contentTag == null) { "A node that is not a group or a footnote cannot have a 'contentTag'." }
}
}
Expand Down Expand Up @@ -97,7 +96,7 @@ public class MarkdownNode(
val source = child.draw(options).lineSequence().iterator()
while (source.hasNext()) {
val line = source.next()
val prefix = (contentKind!! + child.kind).prefix(tag = tag, forceConcat = !touched)
val prefix = (contentKind + child.kind).prefix(tag = tag, forceConcat = !touched)
append(prefix).append(line)
if (index != lastChildIndex || source.hasNext()) append(NEW_LINE)
if (!touched) touched = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ class MarkdownNodeTest {
)
}

@Test fun groupNodeShouldHaveContentKind() {
assertFailure { node = MarkdownNode(kind = MarkdownKind.GROUP, contentTag = { _, _ -> "" }) }
.hasMessage("A node that is a group must have a 'contentKind'.")
}

@Test fun layoutNodeShouldHaveContentTag() {
assertFailure { node = MarkdownNode(kind = MarkdownKind.GROUP, contentKind = MarkdownKind.TEXT) }
.hasMessage("A node that is a group or a footnote must have a 'contentTag'.")
Expand Down Expand Up @@ -88,7 +83,6 @@ class MarkdownNodeTest {
MarkdownNode(source = { worldSource2 }),
),
kind = MarkdownKind.FOOTNOTE,
contentKind = MarkdownKind.ANY,
contentTag = { _, _ -> "[^T]: " },
)

Expand Down Expand Up @@ -156,7 +150,6 @@ class MarkdownNodeTest {
.apply { index = actualIndex }
},
kind = MarkdownKind.GROUP + MarkdownKind.REPEATATION_PARENT_TAG,
contentKind = MarkdownKind.ANY,
contentTag = { index, _ -> "${index + 1}. " },
)
val childrenNodes = MarkdownNode(
Expand All @@ -175,7 +168,6 @@ class MarkdownNodeTest {
childrenNodes,
),
kind = MarkdownKind.GROUP,
contentKind = MarkdownKind.ANY,
contentTag = { _, _ -> "" },
)

Expand Down
3 changes: 3 additions & 0 deletions markdown-ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ plugins {

kotlin {
explicitApi()
sourceSets.all {
languageSettings.enableLanguageFeature("ExplicitBackingFields")
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ public inline fun List(
factory = {
MarkdownNode(
kind = MarkdownKind.GROUP,
contentKind = MarkdownKind.ANY,
contentTag = { index, _ -> if (ordered) "${index + 1}. " else "- " },
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,41 @@

package land.sungbin.markdown.ui.modifier

public fun Modifier.clickable(range: (String) -> IntRange, link: String): Modifier {
TODO()
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import land.sungbin.markdown.runtime.MarkdownOptions
import land.sungbin.markdown.ui.text.TextTransformer

private val DefaultClickableRange: (String) -> IntRange = { 0..it.length }

@Stable
public fun Modifier.clickable(link: String, range: (String) -> IntRange = DefaultClickableRange): Modifier =
this then ClickableTransformer(link, range)

@Immutable
public class ClickableTransformer(
private val link: String,
private val range: (String) -> IntRange,
) : TextTransformer {
override fun transform(options: MarkdownOptions, value: String): String {
val originalLength = value.length
val linkLength = link.length
val replacementRange = range(value)

return CharArray(originalLength + 4 + linkLength).also { new ->
if (replacementRange.first > 0) {
value.toCharArray(new, 0, 0, replacementRange.first)
}
new[replacementRange.first] = '['
value.toCharArray(new, replacementRange.first + 1, replacementRange.first, replacementRange.last + 1)
new[replacementRange.last + 2] = ']'
new[replacementRange.last + 3] = '('
link.toCharArray(new, replacementRange.last + 4, 0, linkLength)
new[replacementRange.last + 4 + linkLength] = ')'
if (replacementRange.last + 1 < originalLength) {
value.toCharArray(new, replacementRange.last + 4 + linkLength + 1, replacementRange.last + 1, originalLength)
}
}
.concatToString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,35 @@
package land.sungbin.markdown.ui.modifier

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import land.sungbin.markdown.runtime.MarkdownComposable
import land.sungbin.markdown.runtime.MarkdownOptions
import land.sungbin.markdown.ui.text.TextTransformer

private val DefaultFootnotePosition: (String) -> Int = { it.length }

@Stable
public fun Modifier.footnote(
tag: String,
range: (String) -> IntRange,
message: @Composable () -> Unit,
): Modifier {
TODO()
position: (String) -> Int = DefaultFootnotePosition,
message: @Composable @MarkdownComposable () -> Unit,
): Modifier =
this then FootnoteModifier(tag, position) with FootnoteGroup(tag, message)

// TODO make public
internal class FootnoteModifier(
internal val tag: String,
private val position: (String) -> Int = DefaultFootnotePosition,
) : TextTransformer {
override fun transform(options: MarkdownOptions, value: String): String =
when (val position = position(value)) {
0 -> "[^$tag]$value"
value.length - 1 -> "$value[^$tag]"
else -> value.substring(0, position + 1) + "[^$tag]" + value.substring(position + 1)
}
}

internal data class FootnoteGroup(
val tag: String,
val content: @Composable () -> Unit,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,53 @@
package land.sungbin.markdown.ui.modifier

import androidx.compose.runtime.Stable
import androidx.compose.ui.util.fastFold
import land.sungbin.markdown.runtime.MarkdownOptions
import land.sungbin.markdown.ui.text.TextTransformer

@Stable
public sealed interface Modifier : Collection<TextTransformer>, RandomAccess {
public operator fun get(index: Int): TextTransformer
public sealed interface Modifier {
public override fun hashCode(): Int
public override fun equals(other: Any?): Boolean

public companion object : Modifier, List<TextTransformer> by emptyList() {
public companion object : Modifier {
override fun hashCode(): Int = 0
override fun equals(other: Any?): Boolean = other === Modifier
}
}

private class MutableModifier : AbstractMutableCollection<TextTransformer>(), Modifier {
private val transformers = ArrayList<TextTransformer>()
internal class MutableModifier : Modifier {
val transformers: List<TextTransformer>
field = ArrayList()

override fun add(element: TextTransformer): Boolean = transformers.add(element)
override fun get(index: Int): TextTransformer = transformers[index]
val footnotes: List<FootnoteGroup>
field = ArrayList()

override val size: Int get() = transformers.size
override fun iterator(): MutableIterator<TextTransformer> = transformers.iterator()
fun add(transformer: TextTransformer) {
transformers.add(transformer)
}

fun add(footnote: FootnoteGroup) {
footnotes.add(footnote)
}

override fun hashCode(): Int {
var result = transformers.hashCode()
result = 31 * result + footnotes.hashCode()
return result
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

override fun hashCode(): Int = transformers.hashCode()
override fun equals(other: Any?): Boolean = transformers == other
other as MutableModifier

if (transformers != other.transformers) return false
if (footnotes != other.footnotes) return false

return true
}
}

@Stable
Expand All @@ -42,11 +63,12 @@ public infix fun Modifier.then(transformer: TextTransformer): Modifier {
return (this as MutableModifier).apply { add(transformer) }
}

// TODO not cloning the sink -> Documentation required
@PublishedApi
internal infix fun Modifier.with(footnote: FootnoteGroup): Modifier {
if (this === Modifier) return MutableModifier().apply { add(footnote) }
return (this as MutableModifier).apply { add(footnote) }
}

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
if (this !is MutableModifier) return value
return transformers.fastFold(value) { acc, transformer -> transformer.transform(options, acc) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import androidx.compose.ui.util.fastFold
import land.sungbin.markdown.runtime.MarkdownOptions

@Immutable
public class AnnotatedString(@Suppress("MemberVisibilityCanBePrivate") public val styledTexts: List<TextAndStyle>) : CharSequence {
public class AnnotatedString(
@Suppress("MemberVisibilityCanBePrivate") public val styledTexts: List<TextAndStyle>,
) : CharSequence {
private val backing = run {
styledTexts
.fastFold(StringBuilder()) { acc, (text, style) ->
Expand All @@ -37,7 +39,6 @@ public class AnnotatedString(@Suppress("MemberVisibilityCanBePrivate") public va
}

override fun hashCode(): Int = backing.hashCode()

override fun toString(): String = backing

@Immutable
Expand Down
17 changes: 15 additions & 2 deletions markdown-ui/src/main/kotlin/land/sungbin/markdown/ui/text/Text.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,31 @@ package land.sungbin.markdown.ui.text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposeNode
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.ui.util.fastForEach
import land.sungbin.markdown.runtime.MarkdownApplier
import land.sungbin.markdown.runtime.MarkdownComposable
import land.sungbin.markdown.runtime.MarkdownKind
import land.sungbin.markdown.runtime.MarkdownNode
import land.sungbin.markdown.ui.EmptyUpdater
import land.sungbin.markdown.ui.modifier.Modifier
import land.sungbin.markdown.ui.modifier.MutableModifier
import land.sungbin.markdown.ui.modifier.applyTo

@Suppress("NOTHING_TO_INLINE")
@[Composable NonRestartableComposable MarkdownComposable]
public inline fun Text(value: CharSequence, modifier: Modifier = Modifier) {
public fun Text(value: CharSequence, modifier: Modifier = Modifier) {
ComposeNode<MarkdownNode, MarkdownApplier>(
factory = { MarkdownNode(source = { options -> modifier.applyTo(options, value.toString()) }) },
update = EmptyUpdater,
content = {
if (modifier is MutableModifier) {
modifier.footnotes.fastForEach { (tag, content) ->
ComposeNode<MarkdownNode, MarkdownApplier>(
factory = { MarkdownNode(kind = MarkdownKind.FOOTNOTE, contentTag = { _, _ -> "[^$tag]: " }) },
update = EmptyUpdater,
content = content,
)
}
}
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.modifier

import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlin.test.Test
import land.sungbin.markdown.ui.transform

class ClickableTransformerTest {
@Test fun clickableRangeStartsWithZero() {
val result = ClickableTransformer(link = "bye", range = { 0..4 }).transform("hello, world!")
assertThat(result).isEqualTo("[hello](bye), world!")
}

@Test fun clickableRangeInMiddle() {
val result = ClickableTransformer(link = "bye", range = { 5..6 }).transform("hello, world!")
assertThat(result).isEqualTo("hello[, ](bye)world!")
}

@Test fun clickableRangeEndsWithLastIndex() {
val result = ClickableTransformer(link = "bye", range = { 7..12 }).transform("hello, world!")
assertThat(result).isEqualTo("hello, [world!](bye)")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.modifier

import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlin.test.Test
import land.sungbin.markdown.ui.transform

class FootnoteTransformerTest {
@Test fun footnotePositionStartsWithZero() {
val result = FootnoteModifier(tag = "bye", position = { 0 }).transform("hello, world!")
assertThat(result).isEqualTo("[^bye]hello, world!")
}

@Test fun footnotePositionInMiddle() {
val result = FootnoteModifier(tag = "bye", position = { 5 }).transform("hello, world!")
assertThat(result).isEqualTo("hello,[^bye] world!")
}

@Test fun footnotePositionEndsWithLastIndex() {
val result = FootnoteModifier(tag = "bye", position = { 12 }).transform("hello, world!")
assertThat(result).isEqualTo("hello, world![^bye]")
}
}
Loading

0 comments on commit 3ae2367

Please sign in to comment.