Skip to content

Commit

Permalink
Runtime improvements and writes the test (#8)
Browse files Browse the repository at this point in the history
* Disable 'class-signature' ktlint rule

* Runtime improvements and runtime test writes

* Update UI codes
  • Loading branch information
jisungbin authored Jun 30, 2024
1 parent 560a448 commit 01f49fe
Show file tree
Hide file tree
Showing 12 changed files with 331 additions and 115 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ subprojects {
"ktlint_standard_function-naming" to "disabled",
"ktlint_standard_property-naming" to "disabled",
"ktlint_standard_backing-property-naming" to "disabled",
"ktlint_standard_class-signature" to "disabled",
"ktlint_standard_import-ordering" to "disabled",
"ktlint_standard_max-line-length" to "disabled",
"ktlint_standard_annotation" to "disabled",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public suspend fun markdown(
var composition: Composition? = null
try {
withRunningRecomposer { recomposer ->
composition = Composition(MarkdownApplier(options, root, footnotes), parent = recomposer).apply {
composition = Composition(MarkdownApplier(root, footnotes), parent = recomposer).apply {
setContent {
when (parentLocals) {
null -> content()
Expand All @@ -68,7 +68,10 @@ public suspend fun markdown(
job.cancelAndJoin()

return buildString {
appendLine(root.draw(parentTag = ""))
append(footnotes.draw(parentTag = ""))
appendLine(root.draw(options))
append(footnotes.draw(options))
}.also {
root.close()
footnotes.close()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import androidx.compose.runtime.collection.MutableVector

// AbstractApplier uses MutableList, but I want to use MutableVector.
public class MarkdownApplier internal constructor(
private val options: MarkdownOptions,
private val root: MarkdownNode = MarkdownNode(kind = MarkdownKind.GROUP),
private val footnotes: MarkdownNode = MarkdownNode(kind = MarkdownKind.GROUP),
) : Applier<MarkdownNode> {
Expand All @@ -32,7 +31,7 @@ public class MarkdownApplier internal constructor(
}

instance.index = index
current.children.add(instance.draw(options = options, parentTag = current.tag(options)))
current.children.add(instance)
}

override fun down(node: MarkdownNode) {
Expand All @@ -49,12 +48,10 @@ public class MarkdownApplier internal constructor(
current = stack.last()

if (tail.text) return // text can't be a group, so at this point it's already child of current.
val children = tail.draw(options = options, parentTag = if (current.footnote) "" else current.tag(options))

when {
current.text -> runtimeError { "Text nodes cannot have children." }
current.group -> current.children.add(children)
current.footnote -> footnotes.children.add(children)
current.group -> current.children.add(tail)
current.footnote -> footnotes.children.add(tail)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,15 @@ public value class MarkdownKind @PublishedApi internal constructor(@PublishedApi
public inline operator fun plus(other: MarkdownKind): MarkdownKind = MarkdownKind(mask or other.mask)
public inline operator fun contains(value: MarkdownKind): Boolean = (mask and value.mask) != 0

init {
if (contains(REPEATION_TAG) || contains(RESPECT_PARENT_TAG) || contains(CONSUMING_TAG)) {
require(contains(GROUP) || contains(FOOTNOTE)) {
"CONSUMING_TAG, REPEATION_TAG and RESPECT_PARENT_TAG can only be used with GROUP or FOOTNOTE"
}
}

require(!(contains(GROUP) && contains(FOOTNOTE))) {
"Cannot set GROUP and FOOTNOTE at the same time"
}

require(!(contains(CONSUMING_TAG) && contains(REPEATION_TAG))) {
"Cannot set CONSUMING_TAG and REPEATION_TAG at the same time"
}
}
public val layout: Boolean inline get() = GROUP in this || FOOTNOTE in this

public companion object {
public val TEXT: MarkdownKind inline get() = MarkdownKind(0b1 shl 0)
public val GROUP: MarkdownKind inline get() = MarkdownKind(0b1 shl 1)
public val FOOTNOTE: MarkdownKind inline get() = MarkdownKind(0b1 shl 2)
public val ANY: MarkdownKind inline get() = MarkdownKind(0b1 shl 0)
public val TEXT: MarkdownKind inline get() = MarkdownKind(0b1 shl 1)

public val GROUP: MarkdownKind inline get() = MarkdownKind(0b1 shl 5)
public val FOOTNOTE: MarkdownKind inline get() = MarkdownKind(0b1 shl 6)

public val CONSUMING_TAG: MarkdownKind inline get() = MarkdownKind(0b1 shl 10)
public val REPEATION_TAG: MarkdownKind inline get() = MarkdownKind(0b1 shl 11)
public val RESPECT_PARENT_TAG: MarkdownKind inline get() = MarkdownKind(0b1 shl 12)
public val REPEATATION_PARENT_TAG: MarkdownKind inline get() = MarkdownKind(0b1 shl 10)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,106 +11,126 @@ import androidx.compose.runtime.collection.MutableVector
import okio.Buffer
import okio.BufferedSource
import okio.Closeable
import okio.Source
import okio.Timeout
import okio.buffer
import org.jetbrains.annotations.TestOnly

// TODO documentation
public class MarkdownNode(
private val source: (MarkdownOptions) -> BufferedSource = EMPTY_SOURCE,
private val source: ((MarkdownOptions) -> BufferedSource)? = null,
private val kind: MarkdownKind = MarkdownKind.TEXT,
private val tag: (index: Int, MarkdownOptions) -> String = EMPTY_TAG,
) : Closeable,
Cloneable {
private var _source: BufferedSource? = null
internal val children = MutableVectorWithMutationTracking(MutableVector<BufferedSource>(capacity = 10)) {
check(group || footnote) { "Children can be added only to a group or footnote node." }
private val contentKind: MarkdownKind? = null,
private val contentTag: ((index: Int, MarkdownOptions) -> String)? = null,
) : Closeable, Cloneable {
internal val children = MutableVectorWithMutationTracking(MutableVector<MarkdownNode>(capacity = 20)) {
check(kind.layout) { "Children can be added only to a group or footnote node." }
}

internal val text get() = MarkdownKind.TEXT in kind
internal val group get() = MarkdownKind.GROUP in kind
internal val footnote get() = MarkdownKind.FOOTNOTE in kind

@TestOnly internal constructor(
children: List<MarkdownNode>,
kind: MarkdownKind = MarkdownKind.GROUP,
contentKind: MarkdownKind,
contentTag: (index: Int, MarkdownOptions) -> String,
) : this(kind = kind, contentKind = contentKind, contentTag = contentTag) {
for (i in children.indices) {
this.children.add(children[i])
}
}

init {
if (!group || !footnote) {
require(source !== EMPTY_SOURCE) {
"A MarkdownNode has been emitted as a text, but the source is empty. " +
"Use a source containing a markdown string instead of EMPTY_SOURCE."
require(text || group || footnote) {
"A MarkdownNode must have a kind of MarkdownKind.TEXT, MarkdownKind.GROUP, or MarkdownKind.FOOTNOTE."
}

when (kind.layout) {
true -> {
require(source == null) {
"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(contentTag == null) { "A node that is not a group or a footnote cannot have a 'contentTag'." }
}
}
}

internal var index: Int? = null
private var cachedTag: String? = null
internal var index = 0

internal fun tag(options: MarkdownOptions): String {
val cached = cachedTag
if (cached != null) return cached
val index = checkNotNull(index) { "The tag() was requested before current index was defined." }
return tag.invoke(index, options).also { cachedTag = it }
private fun tag(index: Int, options: MarkdownOptions): String {
runtimeCheck(kind.layout) { "A node that is not a group or a footnote cannot have a tag." }
return contentTag!!.invoke(index, options)
}

internal fun draw(options: MarkdownOptions? = null, parentTag: String): BufferedSource {
if (group || footnote) check(source === EMPTY_SOURCE) {
"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 (text) requireNotNull(options) { "Text nodes must have options to draw." }

val tag = tag(options ?: MarkdownOptions.Default)
val drawingSource = if (group || footnote) {
children.vector.fold(Buffer()) { acc, node -> acc.apply { writeAll(node) } }
} else source(options!!).also { _source = it }

return Buffer().apply {
while (!drawingSource.exhausted()) {
val line = (drawingSource.readUtf8Line() ?: break).let { line ->
prefix(
tag = if (!footnote || size == 0L) tag else if (footnote) " " else runtimeError { "Uncovered case: ${toString()}" },
concatTag = if (size == 0L || footnote) true else MarkdownKind.REPEATION_TAG in kind,
consumedTag = MarkdownKind.CONSUMING_TAG in kind && size != 0L,
parentTag = parentTag,
)
.plus(line)
}
writeUtf8(line + '\n')
}
internal fun draw(options: MarkdownOptions): BufferedSource = Buffer().apply {
when {
text -> writeStringMarkdown(options)
group -> writeGroupMarkdown(options)
footnote -> writeFootnoteMarkdown(options)
}
}

private fun prefix(tag: String, concatTag: Boolean, consumedTag: Boolean, parentTag: String): String {
var prefix = if (MarkdownKind.RESPECT_PARENT_TAG in kind) parentTag else ""
prefix += if (consumedTag) "" else if (concatTag) tag else " ".repeat(tag.length)
return prefix
private fun Buffer.writeStringMarkdown(options: MarkdownOptions) {
writeAll(source!!.invoke(options))
}

override fun toString(): String =
clone().apply { index = 0 }
.draw(options = MarkdownOptions.Default, parentTag = "")
.readUtf8()
private fun Buffer.writeFootnoteMarkdown(options: MarkdownOptions) {
var tag: String? = null

public override fun clone(): MarkdownNode = MarkdownNode(source, kind, tag).also { clone ->
children.forEach { child ->
clone.children.add(child.buffer.clone())
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)
if (tag != FOOTNOTE_INDENT) tag = FOOTNOTE_INDENT
}
}
}

private fun Buffer.writeGroupMarkdown(options: MarkdownOptions) {
children.forEach { child ->
val source = child.draw(options)
val tag = tag(child.index, options)
var touched = false
while (!source.exhausted()) {
val line = source.readUtf8Line() ?: break
val prefix = (contentKind!! + child.kind).prefix(tag = tag, forceConcat = !touched)
writeUtf8(prefix).writeUtf8(line).writeByte(NEW_LINE)
touched = true
}
}
}

private fun MarkdownKind.prefix(tag: String, forceConcat: Boolean): String = when (this) {
in MarkdownKind.TEXT,
in MarkdownKind.GROUP,
-> if (forceConcat || MarkdownKind.REPEATATION_PARENT_TAG in this) tag else " ".repeat(tag.length)
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() }
_source?.close()
_source = null
children.clear()
}

private companion object {
private val EMPTY_SOURCE: (MarkdownOptions) -> BufferedSource = {
object : Source {
override fun read(sink: Buffer, byteCount: Long): Long = -1
override fun timeout(): Timeout = Timeout.NONE
override fun close() {}
}
.buffer()
}

private val EMPTY_TAG: (index: Int, MarkdownOptions) -> String = { _, _ -> "" }
private const val NEW_LINE = '\n'.code
private const val FOOTNOTE_INDENT = " "
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ import androidx.compose.runtime.collection.MutableVector
* On mutation, the [onVectorMutated] lambda will be invoked.
*/
// Source: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MutableVectorWithMutationTracking.kt;l=25;drc=dcaa116fbfda77e64a319e1668056ce3b032469f
@Suppress("NOTHING_TO_INLINE")
internal class MutableVectorWithMutationTracking<T>(
internal val vector: MutableVector<T>,
private val onVectorMutated: () -> Unit,
) {
fun add(element: T) {
vector.add(element)
onVectorMutated()
}

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)
}
Loading

0 comments on commit 01f49fe

Please sign in to comment.