joinToString refactoring
authorLanius Trolling <lanius@laniustrolling.dev>
Sun, 6 Oct 2024 20:49:21 +0000 (16:49 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Sun, 6 Oct 2024 20:49:21 +0000 (16:49 -0400)
17 files changed:
src/jvmMain/kotlin/info/mechyrdia/Utils.kt [new file with mode: 0644]
src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt
src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt
src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt
src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserBuilder.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPlain.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserRobot.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt

diff --git a/src/jvmMain/kotlin/info/mechyrdia/Utils.kt b/src/jvmMain/kotlin/info/mechyrdia/Utils.kt
new file mode 100644 (file)
index 0000000..3bfd683
--- /dev/null
@@ -0,0 +1,5 @@
+package info.mechyrdia
+
+fun Iterable<String>.concat(delimiter: String = "", prefix: String = "", suffix: String = "") = joinToString(separator = delimiter, prefix = prefix, postfix = suffix)
+
+fun <T> Iterable<T>.concat(delimiter: String = "", prefix: String = "", suffix: String = "", converter: (T) -> String = Any?::toString) = joinToString(separator = delimiter, prefix = prefix, postfix = suffix, transform = converter)
index f16fa05ee67eca6d5b018c481a29e9c8298ddfe6..37262ab5ad092a4f1317eda14e0e1edc7bea83e6 100644 (file)
@@ -5,6 +5,7 @@ import com.mongodb.client.model.Sorts
 import com.mongodb.client.model.UpdateOneModel
 import com.mongodb.client.model.UpdateOptions
 import com.mongodb.client.model.Updates
+import info.mechyrdia.concat
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.toList
 import kotlinx.serialization.SerialName
@@ -34,7 +35,7 @@ data class Comment(
                }
                
                suspend fun getCommentsIn(page: List<String>): Flow<Comment> {
-                       return Table.select(Filters.eq(Comment::submittedIn.serialName, page.joinToString(separator = "/")), Sorts.descending(Comment::submittedAt.serialName))
+                       return Table.select(Filters.eq(Comment::submittedIn.serialName, page.concat("/")), Sorts.descending(Comment::submittedAt.serialName))
                }
                
                suspend fun getCommentsBy(user: Id<NationData>): Flow<Comment> {
index 8f2cf2255b280db9da38ae4d14028261d6c86879..7e081d7421ab8d87e37ec2a60b1e502baf148ff5 100644 (file)
@@ -5,6 +5,7 @@ import com.mongodb.client.model.Updates
 import com.mongodb.reactivestreams.client.gridfs.GridFSBucket
 import info.mechyrdia.Configuration
 import info.mechyrdia.FileStorageConfig
+import info.mechyrdia.concat
 import info.mechyrdia.lore.StoragePathAttributeKey
 import info.mechyrdia.lore.forEachSuspend
 import io.ktor.http.ContentType
@@ -64,7 +65,7 @@ value class StoragePath(val elements: List<String>) {
        init {
                for ((i, element) in elements.withIndex())
                        require(element.any { it != '.' }) {
-                               "Cannot have elements . or .. in path, got $element at index $i in path /${elements.joinToString(separator = "/")}"
+                               elements.concat("/", prefix = "Cannot have elements . or .. in path, got $element at index $i in path /")
                        }
        }
        
@@ -84,7 +85,7 @@ value class StoragePath(val elements: List<String>) {
        }.all { it }
        
        override fun toString(): String {
-               return elements.joinToString(separator = "/")
+               return elements.concat("/")
        }
        
        companion object {
@@ -293,8 +294,8 @@ private data class GridFsEntry(
 ) : DataDocument<GridFsEntry>
 
 private class GridFsStorage(val table: DocumentTable<GridFsEntry>, val bucket: GridFSBucket) : FileStorage {
-       private fun toExactPath(path: StoragePath) = path.elements.joinToString(separator = "") { "/$it" }
-       private fun toPrefixPath(path: StoragePath) = "${toExactPath(path)}/"
+       private fun toExactPath(path: StoragePath) = path.elements.concat("/", prefix = "/")
+       private fun toPrefixPath(path: StoragePath) = path.elements.concat("/", prefix = "/", suffix = "/")
        
        private suspend fun testExact(path: StoragePath) = table.number(Filters.eq(GridFsEntry::path.serialName, toExactPath(path))) > 0L
        private suspend fun getExact(path: StoragePath) = table.locate(Filters.eq(GridFsEntry::path.serialName, toExactPath(path)))
index 6a3d6503ba3c4809a20ea573baa55d0707d0aee7..a7040c1903367b5e3678e07e0620292ce4d159cf 100644 (file)
@@ -3,6 +3,7 @@ package info.mechyrdia.data
 import com.mongodb.client.model.Sorts
 import info.mechyrdia.OwnerNationId
 import info.mechyrdia.auth.ForbiddenException
+import info.mechyrdia.concat
 import info.mechyrdia.lore.ParserTree
 import info.mechyrdia.lore.PokhwalishAlphabetFont
 import info.mechyrdia.lore.TylanAlphabetFont
@@ -83,7 +84,7 @@ suspend fun ApplicationCall.newCommentRoute(pagePathParts: List<String>, content
        val now = Instant.now()
        val comment = Comment(
                submittedBy = loggedInAs.id,
-               submittedIn = pagePathParts.joinToString("/"),
+               submittedIn = pagePathParts.concat("/"),
                submittedAt = now,
                
                numEdits = 0,
index aa49aee26d7fd0827462485204d1261ac698d9d1..efa8271cae8faafd0eed18a36ea509e71076ecf4 100644 (file)
@@ -3,6 +3,7 @@ package info.mechyrdia.lore
 import info.mechyrdia.Configuration
 import info.mechyrdia.OwnerNationId
 import info.mechyrdia.auth.UserSession
+import info.mechyrdia.concat
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
 import info.mechyrdia.route.Root
@@ -67,5 +68,5 @@ suspend fun StoragePath.toFriendlyPathTitle(): String {
        
        return lorePath.indices.mapSuspend { index ->
                StoragePath(lorePath.take(index + 1)).toFriendlyPageTitle().title
-       }.joinToString(separator = " - ")
+       }.concat(" - ")
 }
index c0133a4ea2cd8b4e7ba55095587016a6d2b5b79c..63e3eef9c0c76bace60db5ec59681cc3d1599b0f 100644 (file)
@@ -1,5 +1,6 @@
 package info.mechyrdia.lore
 
+import info.mechyrdia.concat
 import info.mechyrdia.data.StoragePath
 
 data class ArticleTitle(val title: String, val css: String = "")
@@ -7,9 +8,9 @@ data class ArticleTitle(val title: String, val css: String = "")
 object ArticleTitleCache : FileDependentCache<ArticleTitle>() {
        private val StoragePath.defaultTitle: String
                get() = if (elements.size > 1)
-                       elements.lastOrNull()?.split('-')?.joinToString(separator = " ") { word ->
+                       elements.last().split('-').concat(" ") { word ->
                                word.lowercase().replaceFirstChar { it.titlecase() }
-                       }.orEmpty()
+                       }
                else TOC_TITLE
        
        private val StoragePath.defaultCssProps: Map<String, Any>
@@ -18,7 +19,7 @@ object ArticleTitleCache : FileDependentCache<ArticleTitle>() {
                        if (name.endsWith(".old")) "text-decoration" to "line-through" else null,
                )
        
-       private fun Map<String, Any>.toStyleString() = map { (k, v) -> "$k:$v" }.joinToString(separator = ";")
+       private fun Map<String, Any>.toStyleString() = asIterable().concat(";") { (k, v) -> "$k:$v" }
        
        override fun default(path: StoragePath): ArticleTitle {
                return ArticleTitle(path.defaultTitle, path.defaultCssProps.toStyleString())
index 3d0125fcdad7fd8d9814760c52356e9961188634..7d57a64ecdb7f69578adacc24a2f33b63e48c3d6 100644 (file)
@@ -3,6 +3,7 @@ package info.mechyrdia.lore
 import com.jaredrummler.fontreader.truetype.FontFileReader
 import com.jaredrummler.fontreader.truetype.TTFFile
 import com.jaredrummler.fontreader.util.GlyphSequence
+import info.mechyrdia.concat
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
 import info.mechyrdia.data.XmlTagConsumer
@@ -80,7 +81,7 @@ fun <T, C : XmlTagConsumer<T>> C.svg(svgDoc: SvgDoc) = declaration(standalone =
                                svgDoc.viewBoxY,
                                svgDoc.viewBoxW,
                                svgDoc.viewBoxH,
-                       ).joinToString(separator = " ") { it.xmlValue }
+                       ).concat(" ") { it.xmlValue }
                )
        ) { "path"(attributes = mapOf("d" to svgDoc.path.d, "fill-rule" to svgDoc.path.fillRule)) }
 
index 70d8be525408b9fdd6ca50b94e69eca318b21764..2e7204ab9f1e1626c999301119fdc480e8b8777e 100644 (file)
@@ -1,6 +1,7 @@
 package info.mechyrdia.lore
 
 import info.mechyrdia.MainDomainName
+import info.mechyrdia.concat
 import info.mechyrdia.data.Comment
 import info.mechyrdia.data.Id
 
@@ -49,8 +50,8 @@ class TableOfContentsBuilder {
                        levels.add(addedLevel)
                }
                
-               val number = levels.joinToString(separator = ".") { it.toString() }
-               links.add(NavLink("#$toAnchor", "$number. $text", aClasses = "left"))
+               val number = levels.concat(".", suffix = ". $text")
+               links.add(NavLink("#$toAnchor", number, aClasses = "left"))
        }
        
        private var description: String? = null
index 76ed1401580aee269f7dfdb9fb37b25dcf278a69..fea9d1edb5562249d957f851fe3848c0345738a6 100644 (file)
@@ -1,6 +1,7 @@
 package info.mechyrdia.lore
 
 import info.mechyrdia.JsonStorageCodec
+import info.mechyrdia.concat
 import kotlinx.html.*
 import kotlinx.html.org.w3c.dom.events.*
 import kotlinx.html.stream.*
@@ -203,7 +204,7 @@ class HtmlMetadataLexerTag(val absorb: Boolean) : HtmlLexerTag {
        }
 }
 
-fun ParserTree.treeToText(): String = joinToString(separator = "") {
+fun ParserTree.treeToText(): String = concat {
        when (it) {
                is ParserTreeNode.Text -> it.text
                ParserTreeNode.LineBreak -> " "
index 093a91d3dfa7797a855946690e39acb61fec54ca..b516e7cd91c00e9729ef3d077793a6994f0fa0f0 100644 (file)
@@ -1,5 +1,7 @@
 package info.mechyrdia.lore
 
+import info.mechyrdia.concat
+
 typealias PlainTextBuilderContext = Unit
 typealias PlainTextBuilderSubject = String
 
@@ -30,7 +32,7 @@ abstract class PlainTextFormattingProcessor : LexerTagFallback<PlainTextBuilderC
        }
        
        override fun combine(env: LexerTagEnvironment<PlainTextBuilderContext, PlainTextBuilderSubject>, subjects: List<PlainTextBuilderSubject>): PlainTextBuilderSubject {
-               return subjects.joinToString(separator = "")
+               return subjects.concat()
        }
 }
 
index 338a5df8dccf11d07691e37c7d2e8283367ff7b6..6f1e07d26c0d4c7503d2fa704ba913d1581d9934 100644 (file)
@@ -1,6 +1,7 @@
 package info.mechyrdia.lore
 
 import info.mechyrdia.JsonStorageCodec
+import info.mechyrdia.concat
 import info.mechyrdia.data.StoragePath
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
@@ -47,7 +48,7 @@ class PreProcessorContext private constructor(
                fun defaults(lorePath: StoragePath) = defaults(lorePath.elements.drop(1))
                
                fun defaults(lorePath: List<String>) = mapOf(
-                       PAGE_PATH_KEY to "/${lorePath.joinToString(separator = "/")}".textToTree(),
+                       PAGE_PATH_KEY to lorePath.concat("/", prefix = "/").textToTree(),
                        INSTANT_NOW_KEY to Instant.now().toEpochMilli().numberToTree(),
                )
        }
index a9e4d157675aea710c6b2aab0fabd27cb87ba975..f19ca507785d47beefc1a8a9ff40f6051c1922a1 100644 (file)
@@ -1,6 +1,7 @@
 package info.mechyrdia.lore
 
 import info.mechyrdia.JsonStorageCodec
+import info.mechyrdia.concat
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
 import kotlinx.serialization.json.JsonArray
@@ -45,7 +46,7 @@ fun ParserTreeNode.unparse(): String = when (this) {
        }
 }
 
-fun ParserTree.unparse() = joinToString(separator = "") { it.unparse() }
+fun ParserTree.unparse() = concat { it.unparse() }
 
 fun ParserTree.toPreProcessJson(): JsonElement {
        val noBlanks = filterNot { it.isWhitespace() }
index 74e21936ff2f6f1407e4d91efefab252d3e57ecc..889ffac4ada3e5cefdf82b7e09404a6a094b0fff 100644 (file)
@@ -1,5 +1,6 @@
 package info.mechyrdia.lore
 
+import info.mechyrdia.concat
 import info.mechyrdia.robot.toOpenAiName
 import java.time.Instant
 
@@ -7,7 +8,7 @@ fun String.toRobotUrl(context: RobotTextContext): String {
        val filePath = if (startsWith("/"))
                this.removePrefix("/")
        else
-               context.siblingFile(this).joinToString(separator = "/")
+               context.siblingFile(this).concat("/")
        
        return filePath.toOpenAiName()
 }
@@ -32,7 +33,7 @@ object RobotTextLexerProcessor : LexerTagFallback<RobotTextContext, RobotTextSub
        }
        
        override fun combine(env: LexerTagEnvironment<RobotTextContext, RobotTextSubject>, subjects: List<RobotTextSubject>): RobotTextSubject {
-               return subjects.joinToString(separator = "")
+               return subjects.concat()
        }
 }
 
@@ -90,20 +91,18 @@ enum class FactbookRobotFormattingTag(val type: RobotTextTag) {
        }),
        
        UL(RobotTextTag { env, _, subNodes ->
-               subNodes
-                       .mapNotNull { subNode ->
-                               if (subNode is ParserTreeNode.Tag && subNode isTag "li")
-                                       " * ${env.processTree(subNode.subNodes)}"
-                               else null
-                       }.joinToString(separator = "")
+               subNodes.concat { subNode ->
+                       if (subNode is ParserTreeNode.Tag && subNode isTag "li")
+                               " * ${env.processTree(subNode.subNodes)}"
+                       else ""
+               }
        }),
        OL(RobotTextTag { env, _, subNodes ->
-               subNodes
-                       .mapIndexedNotNull { i, subNode ->
-                               if (subNode is ParserTreeNode.Tag && subNode isTag "li")
-                                       " ${i + 1}. ${env.processTree(subNode.subNodes)}"
-                               else null
-                       }.joinToString(separator = "")
+               subNodes.withIndex().concat { (i, subNode) ->
+                       if (subNode is ParserTreeNode.Tag && subNode isTag "li")
+                               " ${i + 1}. ${env.processTree(subNode.subNodes)}"
+                       else ""
+               }
        }),
        
        TABLE(RobotTextTag { env, _, subNodes ->
@@ -200,7 +199,7 @@ object RobotFactbookLoader {
                return allPages(null).mapSuspend { pathStat ->
                        val lorePath = pathStat.path.elements.drop(1)
                        FactbookLoader.loadFactbook(lorePath)?.toFactbookRobotText(lorePath)?.let { robotText ->
-                               lorePath.joinToString(separator = "/") to robotText
+                               lorePath.concat("/") to robotText
                        }
                }.filterNotNull().toMap()
        }
@@ -210,7 +209,7 @@ object RobotFactbookLoader {
                        if (pathStat.stat.updated >= lastUpdated) {
                                val lorePath = pathStat.path.elements.drop(1)
                                FactbookLoader.loadFactbook(lorePath)?.toFactbookRobotText(lorePath)?.let { robotText ->
-                                       lorePath.joinToString(separator = "/") to robotText
+                                       lorePath.concat("/") to robotText
                                }
                        } else null
                }.filterNotNull().toMap()
index eef186c022f424aa3ce3549b34a0f1a7d1aec693..dad60f335e69a4f01b3cdc65b1d4552ecc64e64a 100644 (file)
@@ -3,6 +3,7 @@ package info.mechyrdia.lore
 import com.mongodb.client.model.Sorts
 import info.mechyrdia.MainDomainName
 import info.mechyrdia.OwnerNationId
+import info.mechyrdia.concat
 import info.mechyrdia.data.Comment
 import info.mechyrdia.data.CommentRenderData
 import info.mechyrdia.data.FileStorage
@@ -104,12 +105,13 @@ suspend fun generateRecentPageEdits(call: ApplicationCall): RssChannel {
                                )
                        }
                        
+                       val pageHref = pageLink.concat("/", prefix = "$MainDomainName/lore/")
                        RssItem(
                                title = pageToC.toPageTitle(),
                                description = pageOg?.description,
-                               link = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}",
+                               link = pageHref,
                                author = null,
-                               comments = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}#comments",
+                               comments = "$pageHref#comments",
                                enclosure = imageEnclosure,
                                pubDate = page.stat.updated,
                                categories = mechyrdiaCategories,
index 1c45ced336a68c0ef99b5bee3543516d914434ac..ee8882c1b60c7478fb9ca3184d2e2c48a0472434 100644 (file)
@@ -3,6 +3,7 @@ package info.mechyrdia.robot
 import info.mechyrdia.Configuration\r
 import info.mechyrdia.MainDomainName\r
 import info.mechyrdia.OpenAiConfig\r
+import info.mechyrdia.concat\r
 import info.mechyrdia.data.DataDocument\r
 import info.mechyrdia.data.DocumentTable\r
 import info.mechyrdia.data.Id\r
@@ -59,7 +60,6 @@ import kotlin.collections.flatMap
 import kotlin.collections.fold\r
 import kotlin.collections.forEach\r
 import kotlin.collections.iterator\r
-import kotlin.collections.joinToString\r
 import kotlin.collections.listOf\r
 import kotlin.collections.map\r
 import kotlin.collections.minus\r
@@ -381,7 +381,7 @@ class RobotService(
                                                        annotation.text to " [${annotationIndex + 1}]"\r
                                                }\r
                                                \r
-                                               val contents = eventData.delta.content.joinToString(separator = "") { textContent ->\r
+                                               val contents = eventData.delta.content.concat { textContent ->\r
                                                        textContent.text.value\r
                                                }\r
                                                \r
index 725401024482f33738bc7ff6a80f8931307a2357..e7831bd040e9fd92650d1e8354b727276d7efa6a 100644 (file)
@@ -5,6 +5,7 @@ import info.mechyrdia.auth.adminRequestWebDavToken
 import info.mechyrdia.auth.loginPage
 import info.mechyrdia.auth.loginRoute
 import info.mechyrdia.auth.logoutRoute
+import info.mechyrdia.concat
 import info.mechyrdia.data.Comment
 import info.mechyrdia.data.Id
 import info.mechyrdia.data.NationData
@@ -551,7 +552,7 @@ class Root : ResourceHandler, ResourceFilter {
                        override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall(payload: MechyrdiaSansPayload) {
                                with(utils) { call.filterCall() }
                                
-                               val svgDoc = MechyrdiaSansFont.renderTextToSvg(payload.lines.joinToString(separator = "\n") { it.trim() }, payload.bold, payload.italic, payload.align)
+                               val svgDoc = MechyrdiaSansFont.renderTextToSvg(payload.lines.concat("\n") { it.trim() }, payload.bold, payload.italic, payload.align)
                                call.respondXml(contentType = ContentType.Image.SVG) {
                                        svg(svgDoc)
                                }
@@ -563,7 +564,7 @@ class Root : ResourceHandler, ResourceFilter {
                        override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall(payload: TylanLanguagePayload) {
                                with(utils) { call.filterCall() }
                                
-                               call.respondText(TylanAlphabetFont.tylanToFontAlphabet(payload.lines.joinToString(separator = "\n")))
+                               call.respondText(TylanAlphabetFont.tylanToFontAlphabet(payload.lines.concat("\n")))
                        }
                }
                
@@ -572,7 +573,7 @@ class Root : ResourceHandler, ResourceFilter {
                        override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall(payload: PokhwalishLanguagePayload) {
                                with(utils) { call.filterCall() }
                                
-                               call.respondText(PokhwalishAlphabetFont.pokhwalToFontAlphabet(payload.lines.joinToString(separator = "\n")))
+                               call.respondText(PokhwalishAlphabetFont.pokhwalToFontAlphabet(payload.lines.concat("\n")))
                        }
                }
                
@@ -582,7 +583,7 @@ class Root : ResourceHandler, ResourceFilter {
                                with(utils) { call.filterCall() }
                                
                                call.respondText(
-                                       text = payload.lines.joinToString(separator = "\n").parseAs(ParserTree::toCommentHtml).toFragmentString(),
+                                       text = payload.lines.concat("\n").parseAs(ParserTree::toCommentHtml).toFragmentString(),
                                        contentType = ContentType.Text.Html
                                )
                        }
index f2a310d050318c2d7225ed7b242f2164e286d039..0b63763e60d0f18561cb3dae9be4c3249f141860 100644 (file)
@@ -2,6 +2,7 @@ package info.mechyrdia.route
 
 import info.mechyrdia.auth.WebDavToken
 import info.mechyrdia.auth.toNationId
+import info.mechyrdia.concat
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.Id
 import info.mechyrdia.data.StoragePath
@@ -133,7 +134,7 @@ private suspend fun getWebDavPropertiesWithIncludeTags(path: StoragePath, webRoo
                        .filterNotNull()
                        .flatten()
                
-               val pathWithSuffix = path.elements.joinToString(separator = "") { "$it/" }
+               val pathWithSuffix = path.elements.concat("/", suffix = "/")
                listOf(
                        WebDavProperties.Collection(
                                creationDate = subProps.mapNotNull { it.first.creationDate }.maxOrNull(),