From f2eae54234f211ab0cee31ef17474f891013d3e2 Mon Sep 17 00:00:00 2001 From: dece Date: Tue, 7 Dec 2021 23:02:34 +0100 Subject: [PATCH] Gemtext: add a sweet streaming parser --- .../java/dev/lowrespalmtree/comet/Gemtext.kt | 64 +++++++++++++++++++ .../dev/lowrespalmtree/comet/MainActivity.kt | 14 +++- 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/dev/lowrespalmtree/comet/Gemtext.kt diff --git a/app/src/main/java/dev/lowrespalmtree/comet/Gemtext.kt b/app/src/main/java/dev/lowrespalmtree/comet/Gemtext.kt new file mode 100644 index 0000000..5e8a7cb --- /dev/null +++ b/app/src/main/java/dev/lowrespalmtree/comet/Gemtext.kt @@ -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, + charset: Charset, + scope: CoroutineScope +): Channel { + val channel = Channel() + 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" \ No newline at end of file diff --git a/app/src/main/java/dev/lowrespalmtree/comet/MainActivity.kt b/app/src/main/java/dev/lowrespalmtree/comet/MainActivity.kt index 4a1ff3c..950dee2 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/MainActivity.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/MainActivity.kt @@ -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) } }