From: Lanius Trolling
Date: Wed, 20 Dec 2023 16:37:02 +0000 (-0500)
Subject: Add interactive map viewer to Git repository
X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=f3d0de6049f02b4c2da6a52ac324a55db7004bd2;p=factbooks
Add interactive map viewer to Git repository
---
diff --git a/.idea/artifacts/factbooks_jvm.xml b/.idea/artifacts/factbooks_jvm.xml
new file mode 100644
index 0000000..5b34d56
--- /dev/null
+++ b/.idea/artifacts/factbooks_jvm.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/factbooks_map.xml b/.idea/artifacts/factbooks_map.xml
new file mode 100644
index 0000000..6856480
--- /dev/null
+++ b/.idea/artifacts/factbooks_map.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/map_viewer_js.xml b/.idea/artifacts/map_viewer_js.xml
new file mode 100644
index 0000000..9769f80
--- /dev/null
+++ b/.idea/artifacts/map_viewer_js.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/map-viewer/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 1b3f3e9..5404067 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,8 +4,6 @@
"
+ else
+ "
"
+
+ appendTextRaw(newline)
+ PlainText(scope, "", insideTags)
+ } else
+ PlainText(scope, text + char, insideTags)
+ }
+
+ override fun processEndOfText() {
+ appendText(text)
+ }
+ }
+
+ class NoFormatText(scope: TextParserScope, private val text: String, insideTags: List) : TextParserState(scope, insideTags) {
+ override fun processCharacter(char: Char): TextParserState {
+ val newText = text + char
+ return if (newText.endsWith("[/$NO_FORMAT_TAG]")) {
+ appendText(newText.removeSuffix("[/$NO_FORMAT_TAG]"))
+ PlainText(scope, "", insideTags)
+ } else if (newText.endsWith('\n')) {
+ appendText(newText.removeSuffix("\n"))
+ appendTextRaw("
")
+ NoFormatText(scope, "", insideTags)
+ } else
+ NoFormatText(scope, newText, insideTags)
+ }
+
+ override fun processEndOfText() {
+ appendText(text)
+ }
+ }
+
+ class OpenTag(scope: TextParserScope, private val tag: String, insideTags: List) : TextParserState(scope, insideTags) {
+ override fun processCharacter(char: Char): TextParserState {
+ return if (char == ']') {
+ if (tag.equals(NO_FORMAT_TAG, ignoreCase = true))
+ NoFormatText(scope, "", insideTags)
+ else when (val tagType = scope.tags[tag]) {
+ is TextParserTagType.Direct -> {
+ appendTextRaw(tagType.begin(null, scope.ctx))
+ PlainText(scope, "", insideTags + InsideTag.DirectTag(tag))
+ }
+
+ is TextParserTagType.Indirect -> PlainText(scope, "", insideTags + InsideTag.IndirectTag(tag, null))
+
+ else -> PlainText(scope, "[$tag]", insideTags)
+ }
+ } else if (char == '/' && tag == "")
+ CloseTag(scope, tag, insideTags)
+ else if (char == '=' && tag != "")
+ TagParam(scope, tag, "", insideTags)
+ else
+ OpenTag(scope, tag + char, insideTags)
+ }
+
+ override fun processEndOfText() {
+ appendText("[$tag")
+ }
+ }
+
+ class TagParam(scope: TextParserScope, private val tag: String, private val param: String, insideTags: List) : TextParserState(scope, insideTags) {
+ override fun processCharacter(char: Char): TextParserState {
+ return if (char == ']')
+ when (val tagType = scope.tags[tag]) {
+ is TextParserTagType.Direct -> {
+ appendTextRaw(tagType.begin(param, scope.ctx))
+ PlainText(scope, "", insideTags + InsideTag.DirectTag(tag))
+ }
+
+ is TextParserTagType.Indirect -> PlainText(scope, "", insideTags + InsideTag.IndirectTag(tag, param))
+
+ else -> PlainText(scope, "[$tag=$param]", insideTags)
+ }
+ else
+ TagParam(scope, tag, param + char, insideTags)
+ }
+
+ override fun processEndOfText() {
+ appendText("[$tag=$param")
+ }
+ }
+
+ class CloseTag(scope: TextParserScope, private val tag: String, insideTags: List) : TextParserState(scope, insideTags) {
+ override fun processCharacter(char: Char): TextParserState {
+ return if (char == ']') {
+ val tagType = scope.tags[tag]
+ if (tagType is TextParserTagType.Direct && insideTags.lastOrNull() == InsideTag.DirectTag(tag)) {
+ appendTextRaw(tagType.end(scope.ctx))
+
+ PlainText(scope, "", insideTags.dropLast(1))
+ } else if (insideTags.isNotEmpty() && (insideTags.last() as? InsideTag.IndirectTag)?.tag == tag) {
+ PlainText(scope, "", insideTags.dropLast(1))
+ } else {
+ appendText("[/$tag]")
+ PlainText(scope, "", insideTags)
+ }
+ } else CloseTag(scope, tag + char, insideTags)
+ }
+
+ override fun processEndOfText() {
+ appendText("[/$tag")
+ }
+ }
+
+ companion object {
+ const val NO_FORMAT_TAG = "noformat"
+
+ fun censorText(uncensored: String) = uncensored
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("\"", """)
+
+ fun uncensorText(censored: String) = censored
+ .replace(""", "\"")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("&", "&")
+
+ fun parseText(text: String, tags: TextParserTags, context: TContext): String {
+ val builder = StringBuilder()
+ val fixedText = text.replace("\r\n", "\n").replace('\r', '\n')
+ fixedText
+ .fold>(Initial(TextParserScope(builder, tags, context))) { state, char ->
+ state.processCharacter(char)
+ }.processEndOfText()
+ return "$builder
"
+ }
+ }
+}
diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_reply.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_reply.kt
new file mode 100644
index 0000000..8299c7a
--- /dev/null
+++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_reply.kt
@@ -0,0 +1,38 @@
+package info.mechyrdia.lore
+
+import info.mechyrdia.data.Comment
+import info.mechyrdia.data.Id
+
+class CommentRepliesBuilder {
+ private val repliesTo = mutableSetOf>()
+
+ fun addReplyTag(reply: Id) {
+ repliesTo += reply
+ }
+
+ fun toReplySet() = repliesTo.toSet()
+}
+
+enum class TextParserReplyCounterTag(val type: TextParserTagType) {
+ REPLY(
+ TextParserTagType.Indirect { _, content, builder ->
+ sanitizeId(content)?.let { id ->
+ builder.addReplyTag(Id(id))
+ }
+ "[reply]$content[/reply]"
+ }
+ );
+
+ companion object {
+ val asTags: TextParserTags
+ get() = TextParserTags(entries.associate { it.name to it.type })
+ }
+}
+
+fun getReplies(commentContents: String): Set> {
+ val builder = CommentRepliesBuilder()
+
+ TextParserState.parseText(commentContents, TextParserReplyCounterTag.asTags, builder)
+
+ return builder.toReplySet()
+}
diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt
new file mode 100644
index 0000000..60479cd
--- /dev/null
+++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt
@@ -0,0 +1,466 @@
+package info.mechyrdia.lore
+
+import info.mechyrdia.Configuration
+import info.mechyrdia.JsonStorageCodec
+import io.ktor.util.*
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.json.JsonPrimitive
+import java.io.File
+
+sealed class TextParserTagType {
+ data class Direct(val beginFunc: (String?, TContext) -> String, val endFunc: (TContext) -> String) : TextParserTagType() {
+ fun begin(param: String?, context: TContext) = beginFunc(param, context)
+ fun end(context: TContext) = endFunc(context)
+ }
+
+ data class Indirect(val processFunc: (String?, String, TContext) -> String) : TextParserTagType() {
+ fun process(param: String?, content: String, context: TContext) = processFunc(param, content, context)
+ }
+}
+
+@JvmInline
+value class TextParserTags private constructor(private val tags: Map>) {
+ operator fun get(name: String) = tags[name.lowercase()]
+
+ companion object {
+ operator fun invoke(tags: Map>) = TextParserTags(tags.mapKeys { (name, _) -> name.lowercase() })
+ }
+}
+
+enum class TextParserFormattingTag(val type: TextParserTagType) {
+ // Basic formatting
+ B(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ I(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ U(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ S(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ SUP(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ SUB(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ COLOR(
+ TextParserTagType.Direct(
+ { tagParam, _ ->
+ val color = tagParam?.toIntOrNull(16)?.toString(16)?.padStart(6, '0')
+ val style = color?.let { " style=\"color: #$it\"" } ?: ""
+ ""
+ },
+ { "" },
+ )
+ ),
+ IPA(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ CODE(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ H1(
+ TextParserTagType.Indirect { _, content, _ ->
+ "$content
"
+ }
+ ),
+ H2(
+ TextParserTagType.Indirect { _, content, _ ->
+ val anchor = headerContentToAnchor(content)
+ "$content
"
+ }
+ ),
+ H3(
+ TextParserTagType.Indirect { _, content, _ ->
+ val anchor = headerContentToAnchor(content)
+ "$content
"
+ }
+ ),
+ H4(
+ TextParserTagType.Indirect { _, content, _ ->
+ val anchor = headerContentToAnchor(content)
+ "$content
"
+ }
+ ),
+ H5(
+ TextParserTagType.Indirect { _, content, _ ->
+ val anchor = headerContentToAnchor(content)
+ "$content
"
+ }
+ ),
+ H6(
+ TextParserTagType.Indirect { _, content, _ ->
+ val anchor = headerContentToAnchor(content)
+ "$content
"
+ }
+ ),
+ ALIGN(
+ TextParserTagType.Direct(
+ { tagParam, _ ->
+ val alignments = setOf("left", "center", "right", "justify")
+ val alignment = tagParam?.takeIf { it in alignments }
+ val styleAttr = alignment?.let { " style=\"text-align: $it\"" } ?: ""
+ ""
+ },
+ { "
" },
+ )
+ ),
+ ASIDE(
+ TextParserTagType.Direct(
+ { tagParam, _ ->
+ val floats = setOf("left", "right")
+ val float = tagParam?.takeIf { it in floats } ?: "right"
+ ""
+ },
+ { "
" },
+ )
+ ),
+ BLOCKQUOTE(
+ TextParserTagType.Direct({ _, _ ->
+ ""
+ }, { _ ->
+ "
"
+ })
+ ),
+
+ // Metadata
+ DESC(
+ TextParserTagType.Direct(
+ { _, _ -> "" },
+ { "" },
+ )
+ ),
+ THUMB(
+ TextParserTagType.Indirect { _, _, _ -> "" }
+ ),
+
+ // Resource showing
+ IMAGE(
+ TextParserTagType.Indirect { tagParam, content, _ ->
+ val imageUrl = sanitizeLink(content)
+
+ val (width, height) = getSizeParam(tagParam)
+
+ if (imageUrl.endsWith(".svg"))
+ File(Configuration.CurrentConfiguration.assetDir)
+ .combineSafe("images/$imageUrl")
+ .readText()
+ .replace("