Gemtext: add a sweet streaming parser
This commit is contained in:
parent
3e42f2bace
commit
f2eae54234
64
app/src/main/java/dev/lowrespalmtree/comet/Gemtext.kt
Normal file
64
app/src/main/java/dev/lowrespalmtree/comet/Gemtext.kt
Normal file
|
@ -0,0 +1,64 @@
|
|||
package dev.lowrespalmtree.comet
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.CharBuffer
|
||||
import java.nio.charset.Charset
|
||||
|
||||
open class Line
|
||||
|
||||
class EmptyLine : Line()
|
||||
class ParagraphLine(val text: String) : Line()
|
||||
class TitleLine(val level: Int, val text: String) : Line()
|
||||
class LinkLine(val url: String, val label: String) : Line()
|
||||
class PreFenceLine(val caption: String) : Line()
|
||||
class PreTextLine(val text: String) : Line()
|
||||
class BlockquoteLine(val text: String) : Line()
|
||||
class ListItemLine(val text: String) : Line()
|
||||
|
||||
fun parseData(
|
||||
inChannel: Channel<ByteArray>,
|
||||
charset: Charset,
|
||||
scope: CoroutineScope
|
||||
): Channel<Line> {
|
||||
val channel = Channel<Line>()
|
||||
scope.launch {
|
||||
var isPref = false
|
||||
var buffer = ByteArray(0)
|
||||
for (data in inChannel) {
|
||||
buffer += data
|
||||
var nextLineFeed: Int = -1
|
||||
while (buffer.isNotEmpty() && buffer.indexOf(0x0A).also { nextLineFeed = it } > -1) {
|
||||
val lineData = buffer.sliceArray(0 until nextLineFeed)
|
||||
buffer = buffer.sliceArray(nextLineFeed + 1 until buffer.size)
|
||||
val lineString = charset.decode(ByteBuffer.wrap(lineData))
|
||||
val line = parseLine(lineString, isPref)
|
||||
when (line) {
|
||||
is PreFenceLine -> isPref = !isPref
|
||||
}
|
||||
channel.send(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
return channel
|
||||
}
|
||||
|
||||
private fun parseLine(line: CharBuffer, isPreformatted: Boolean): Line =
|
||||
when {
|
||||
line.isEmpty() -> EmptyLine()
|
||||
line.startsWith("###") -> TitleLine(3, getCharsFrom(line, 3))
|
||||
line.startsWith("##") -> TitleLine(2, getCharsFrom(line, 2))
|
||||
line.startsWith("#") -> TitleLine(1, getCharsFrom(line, 1))
|
||||
line.startsWith(">") -> BlockquoteLine(getCharsFrom(line, 1))
|
||||
line.startsWith("```") -> PreFenceLine(getCharsFrom(line, 3))
|
||||
line.startsWith("* ") -> ListItemLine(getCharsFrom(line, 2))
|
||||
line.startsWith("=>") -> getCharsFrom(line, 2).split(" ", limit = 2)
|
||||
.run { LinkLine(get(0), if (size == 2) get(1) else "") }
|
||||
else -> if (isPreformatted) PreTextLine(line.toString()) else ParagraphLine(line.toString())
|
||||
}
|
||||
|
||||
private fun getCharsFrom(line: CharBuffer, index: Int) = line.substring(index).removePrefix(" ")
|
||||
|
||||
private const val TAG = "Gemtext"
|
|
@ -89,9 +89,17 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private suspend fun handleRequestSuccess(response: Response) {
|
||||
val charset = Charset.defaultCharset()
|
||||
for (data in response.data) {
|
||||
val decoded = charset.decode(ByteBuffer.wrap(data)).toString()
|
||||
source += decoded
|
||||
for (line in parseData(response.data, charset, viewModelScope)) {
|
||||
when (line) {
|
||||
is EmptyLine -> { source += "\n" }
|
||||
is ParagraphLine -> { source += line.text + "\n" }
|
||||
is TitleLine -> { source += "TTL-${line.level} ${line.text}\n" }
|
||||
is LinkLine -> { source += "LNK ${line.url} + ${line.label}\n" }
|
||||
is PreFenceLine -> { source += "PRE ${line.caption}\n" }
|
||||
is PreTextLine -> { source += line.text + "\n" }
|
||||
is BlockquoteLine -> { source += "QUO ${line.text}\n" }
|
||||
is ListItemLine -> { source += "LST ${line.text}\n" }
|
||||
}
|
||||
sourceLiveData.postValue(source)
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue