Fix various anomalous behaviors
authorLanius Trolling <lanius@laniustrolling.dev>
Thu, 16 Feb 2023 19:21:30 +0000 (14:21 -0500)
committerLanius Trolling <lanius@laniustrolling.dev>
Thu, 16 Feb 2023 19:21:30 +0000 (14:21 -0500)
src/main/kotlin/info/mechyrdia/Factbooks.kt
src/main/kotlin/info/mechyrdia/data/comments.kt
src/main/kotlin/info/mechyrdia/data/view_comments.kt
src/main/kotlin/info/mechyrdia/data/views_comment.kt
src/main/kotlin/info/mechyrdia/data/views_user.kt
src/main/kotlin/info/mechyrdia/lore/parser.kt
src/main/kotlin/info/mechyrdia/lore/parser_reply.kt
src/main/kotlin/info/mechyrdia/lore/parser_toc.kt
src/main/kotlin/info/mechyrdia/lore/views_lore.kt
src/main/resources/static/style.css

index de4ff2ee5d6ef0d5f108b50bd6a15e7b9f563bb9..9bcdde7f23f2fa1879ab245c48027f30526f7511 100644 (file)
@@ -203,13 +203,9 @@ fun Application.factbooks() {
                }
                
                post("/preview-comment") {
-                       val result = TextParserState.parseText(call.receiveText(), TextParserCommentTags.asTags, Unit)
                        call.respondText(
-                               text = result.html,
-                               contentType = ContentType.Text.Html,
-                               status = if (result.succeeded)
-                                       HttpStatusCode.OK
-                               else HttpStatusCode.BadRequest
+                               text = TextParserState.parseText(call.receiveText(), TextParserCommentTags.asTags, Unit),
+                               contentType = ContentType.Text.Html
                        )
                }
        }
index 38397c9681b513398830ab0a752a29ee171018df..2fa4feba7e36dd9658acc77fcdc78cb91c7573f7 100644 (file)
@@ -1,13 +1,14 @@
 package info.mechyrdia.data
 
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.toSet
+import kotlinx.coroutines.flow.toList
 import kotlinx.serialization.Contextual
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
+import org.litote.kmongo.and
 import org.litote.kmongo.descending
 import org.litote.kmongo.eq
+import org.litote.kmongo.nin
 import java.time.Instant
 
 @Serializable
@@ -48,7 +49,9 @@ data class CommentReplyLink(
        override val id: Id<CommentReplyLink> = Id(),
        
        val originalPost: Id<Comment>,
-       val replyingPost: Id<Comment>
+       val replyingPost: Id<Comment>,
+       
+       val repliedAt: @Contextual Instant = Instant.now(),
 ) : DataDocument<CommentReplyLink> {
        companion object : TableHolder<CommentReplyLink> {
                override val Table: DocumentTable<CommentReplyLink> = DocumentTable()
@@ -56,11 +59,16 @@ data class CommentReplyLink(
                override suspend fun initialize() {
                        Table.index(CommentReplyLink::originalPost)
                        Table.index(CommentReplyLink::replyingPost)
-                       Table.unique(CommentReplyLink::originalPost, CommentReplyLink::replyingPost)
+                       Table.unique(CommentReplyLink::replyingPost, CommentReplyLink::originalPost)
                }
                
                suspend fun updateComment(updatedReply: Id<Comment>, repliesTo: Set<Id<Comment>>) {
-                       Table.remove(CommentReplyLink::replyingPost eq updatedReply)
+                       Table.remove(
+                               and(
+                                       CommentReplyLink::originalPost nin repliesTo,
+                                       CommentReplyLink::replyingPost eq updatedReply
+                               )
+                       )
                        Table.put(
                                repliesTo.map { original ->
                                        CommentReplyLink(
@@ -73,8 +81,11 @@ data class CommentReplyLink(
                
                suspend fun deleteComment(deletedReply: Id<Comment>) = updateComment(deletedReply, emptySet())
                
-               suspend fun getReplies(original: Id<Comment>): Set<Id<Comment>> {
-                       return Table.filter(CommentReplyLink::originalPost eq original).map { it.replyingPost }.toSet()
+               suspend fun getReplies(original: Id<Comment>): List<Id<Comment>> {
+                       return Table.filter(CommentReplyLink::originalPost eq original)
+                               .toList()
+                               .sortedBy { it.repliedAt }
+                               .map { it.replyingPost }
                }
        }
 }
index 17402c12a27dd3acb02aba3b7c0ecb174dcc6162..427835fba5ec242ee7185ace19248caf4b7212fd 100644 (file)
@@ -25,30 +25,28 @@ data class CommentRenderData(
        val contentsRaw: String,
        val contentsHtml: String,
        
-       val replyLinks: Set<Id<Comment>>,
+       val replyLinks: List<Id<Comment>>,
 ) {
        companion object {
                suspend operator fun invoke(comments: List<Comment>, nations: MutableMap<Id<NationData>, NationData> = mutableMapOf()): List<CommentRenderData> {
                        return coroutineScope {
-                               comments.mapNotNull { comment ->
+                               comments.map { comment ->
                                        val nationData = nations.getNation(comment.submittedBy)
                                        val htmlResult = TextParserState.parseText(comment.contents, TextParserCommentTags.asTags, Unit)
                                        
-                                       if (htmlResult.succeeded)
-                                               async {
-                                                       CommentRenderData(
-                                                               id = comment.id,
-                                                               submittedBy = nationData,
-                                                               submittedIn = comment.submittedIn,
-                                                               submittedAt = comment.submittedAt,
-                                                               numEdits = comment.numEdits,
-                                                               lastEdit = comment.lastEdit,
-                                                               contentsRaw = comment.contents,
-                                                               contentsHtml = htmlResult.html,
-                                                               replyLinks = CommentReplyLink.getReplies(comment.id),
-                                                       )
-                                               }
-                                       else null
+                                       async {
+                                               CommentRenderData(
+                                                       id = comment.id,
+                                                       submittedBy = nationData,
+                                                       submittedIn = comment.submittedIn,
+                                                       submittedAt = comment.submittedAt,
+                                                       numEdits = comment.numEdits,
+                                                       lastEdit = comment.lastEdit,
+                                                       contentsRaw = comment.contents,
+                                                       contentsHtml = htmlResult,
+                                                       replyLinks = CommentReplyLink.getReplies(comment.id),
+                                               )
+                                       }
                                }.map { it.await() }
                        }
                }
index 75edefff434ab45f3906910433090aa918c3134d..28c590d5ac2364ab5d9134279917ff66f0c842e1 100644 (file)
@@ -105,6 +105,10 @@ suspend fun ApplicationCall.editCommentRoute(): Nothing {
        
        val newContents = formParams.getOrFail("comment")
        
+       // Check for null edits, i.e. edits that don't change anything
+       if (newContents == oldComment.contents)
+               redirect("/comment/view/$commentId")
+       
        val newComment = oldComment.copy(
                numEdits = oldComment.numEdits + 1,
                lastEdit = Instant.now(),
@@ -405,7 +409,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin
                                |[/tr]
                                |[/table]
                        """.trimMargin()
-               val tableDemoHtml = TextParserState.parseText(tableDemoMarkup, TextParserCommentTags.asTags, Unit).html
+               val tableDemoHtml = TextParserState.parseText(tableDemoMarkup, TextParserCommentTags.asTags, Unit)
                p {
                        +"Table cells in this custom BBCode markup also support row-spans and column-spans, even at the same time:"
                }
index 6bdf218f62d42c61089349d57b8fe5e6e7dce656..d5838b1618faa1280d49e024012baff5ea87eb13 100644 (file)
@@ -15,7 +15,10 @@ suspend fun ApplicationCall.userPage(): HTML.() -> Unit {
        val currNation = currentNation()
        val viewingNation = NationData.get(Id(parameters["id"]!!))
        
-       val comments = CommentRenderData(Comment.getCommentsBy(viewingNation.id).toList())
+       val comments = CommentRenderData(
+               Comment.getCommentsBy(viewingNation.id).toList(),
+               mutableMapOf(viewingNation.id to viewingNation)
+       )
        
        return page(viewingNation.name, standardNavBar()) {
                section {
index 51f416e57e3eb72d3525e2f263f852f22abd2d50..531513fa06e5ee5a5b4034c9daa43005786261c3 100644 (file)
@@ -89,14 +89,16 @@ sealed class TextParserState<TContext>(
                        return if (char == ']') {
                                if (tag.equals(NO_FORMAT_TAG, ignoreCase = true))
                                        NoFormatText(scope, "", insideTags, insideDirectTags)
-                               else if (scope.tags[tag] is TextParserTagType.Direct<TContext>) {
-                                       (scope.tags[tag] as? TextParserTagType.Direct<TContext>)?.begin(null, scope.ctx)?.let {
-                                               appendTextRaw(it)
+                               else when (val tagType = scope.tags[tag]) {
+                                       is TextParserTagType.Direct<TContext> -> {
+                                               appendTextRaw(tagType.begin(null, scope.ctx))
+                                               PlainText(scope, "", insideTags, insideDirectTags + tag)
                                        }
                                        
-                                       PlainText(scope, "", insideTags, insideDirectTags + tag)
-                               } else
-                                       PlainText(scope, "", insideTags + (tag to null), insideDirectTags)
+                                       is TextParserTagType.Indirect<TContext> -> PlainText(scope, "", insideTags + (tag to null), insideDirectTags)
+                                       
+                                       else -> PlainText(scope, "[$tag]", insideTags, insideDirectTags)
+                               }
                        } else if (char == '/' && tag == "")
                                CloseTag(scope, tag, insideTags, insideDirectTags)
                        else if (char == '=' && tag != "")
@@ -112,15 +114,18 @@ sealed class TextParserState<TContext>(
        
        class TagParam<TContext>(scope: TextParserScope<TContext>, val tag: String, val param: String, insideTags: List<Pair<String, String?>>, insideDirectTags: List<String>) : TextParserState<TContext>(scope, insideTags, insideDirectTags) {
                override fun processCharacter(char: Char): TextParserState<TContext> {
-                       return if (char == ']') {
-                               val tagType = scope.tags[tag]
-                               if (tagType is TextParserTagType.Direct<TContext>) {
-                                       appendTextRaw(tagType.begin(param, scope.ctx))
+                       return if (char == ']')
+                               when (val tagType = scope.tags[tag]) {
+                                       is TextParserTagType.Direct<TContext> -> {
+                                               appendTextRaw(tagType.begin(param, scope.ctx))
+                                               PlainText(scope, "", insideTags, insideDirectTags + tag)
+                                       }
                                        
-                                       PlainText(scope, "", insideTags, insideDirectTags + tag)
-                               } else
-                                       PlainText(scope, "", insideTags + (tag to param), insideDirectTags)
-                       } else
+                                       is TextParserTagType.Indirect<TContext> -> PlainText(scope, "", insideTags + (tag to param), insideDirectTags)
+                                       
+                                       else -> PlainText(scope, "[$tag=$param]", insideTags, insideDirectTags)
+                               }
+                       else
                                TagParam(scope, tag, param + char, insideTags, insideDirectTags)
                }
                
@@ -166,17 +171,14 @@ sealed class TextParserState<TContext>(
                        .replace("&gt;", ">")
                        .replace("&amp;", "&")
                
-               fun <TContext> parseText(text: String, tags: TextParserTags<TContext>, context: TContext): ParseOutcome {
+               fun <TContext> parseText(text: String, tags: TextParserTags<TContext>, context: TContext): String {
                        val builder = StringBuilder()
-                       try {
-                               val fixedText = text.replace("\r\n", "\n").replace('\r', '\n')
-                               fixedText.fold<TextParserState<TContext>>(Initial(TextParserScope(builder, tags, context))) { state, char -> state.processCharacter(char) }.processEndOfText()
-                       } catch (ex: Exception) {
-                               return ParseOutcome("<p>$builder</p><h1>Internal Error!</h1><pre>${ex.stackTraceToString()}</pre>", false)
-                       }
-                       return ParseOutcome("<p>$builder</p>", true)
+                       val fixedText = text.replace("\r\n", "\n").replace('\r', '\n')
+                       fixedText
+                               .fold<TextParserState<TContext>>(Initial(TextParserScope(builder, tags, context))) { state, char ->
+                                       state.processCharacter(char)
+                               }.processEndOfText()
+                       return "<p>$builder</p>"
                }
        }
 }
-
-data class ParseOutcome(val html: String, val succeeded: Boolean)
index 1e59569484bd772ebcb29830c7fe0ab80b38de2a..16f26205c0a59eccbc6ee97c503fd4ec9f209de7 100644 (file)
@@ -30,8 +30,7 @@ enum class TextParserReplyCounterTag(val type: TextParserTagType<CommentRepliesB
 fun getReplies(commentContents: String): Set<Id<Comment>> {
        val builder = CommentRepliesBuilder()
        
-       if (!TextParserState.parseText(commentContents, TextParserReplyCounterTag.asTags, builder).succeeded)
-               return emptySet()
+       TextParserState.parseText(commentContents, TextParserReplyCounterTag.asTags, builder)
        
        return builder.toReplySet()
 }
index b14ba199530158ce469a47d0e4ad03ec0ae706fe..e36d3e6a11da96f3b8175892541655b93249cef5 100644 (file)
@@ -7,33 +7,31 @@ class TableOfContentsBuilder {
        
        fun add(text: String, level: Int, toAnchor: String) {
                if (level == 0) {
-                       if (title != null)
-                               throw IllegalArgumentException("[h1] cannot appear multiple times in an article!")
+                       if (title == null)
+                               title = text
                        
-                       title = text
                        return
                }
                
-               val number = if (level > levels.size) {
-                       if (level == levels.size + 1) {
-                               levels.add(1)
-                               levels.joinToString(separator = ".") { it.toString() }
-                       } else
-                               throw IllegalArgumentException("[h${level + 1}] cannot appear after [h${levels.size + 1}]!")
-               } else {
+               if (level > levels.size)
+                       levels.add(1)
+               else {
                        val newLevels = levels.take(level).mapIndexed { i, n -> if (i == level - 1) n + 1 else n }
                        levels.clear()
                        levels.addAll(newLevels)
-                       
-                       levels.joinToString(separator = ".") { it.toString() }
                }
                
+               val number = levels.joinToString(separator = ".") { it.toString() }
                links.plusAssign(NavLink("#$toAnchor", "$number. $text", aClasses = "left"))
        }
        
-       fun toPageTitle() = title!!
+       fun toPageTitle() = title ?: MISSING_TITLE
+       
+       fun toNavBar(): List<NavItem> = listOf(NavLink("#page-top", title ?: MISSING_TITLE, aClasses = "left")) + links.toList()
        
-       fun toNavBar(): List<NavItem> = listOf(NavLink("#page-top", title!!, aClasses = "left")) + links.toList()
+       companion object {
+               const val MISSING_TITLE = "Untitled"
+       }
 }
 
 enum class TextParserToCBuilderTag(val type: TextParserTagType<TableOfContentsBuilder>) {
index d4743c23f2d0066bda9291315741bc1e436b1777..9b633cfec00ff7ff3bb5b6b70d0f21827e4cae15 100644 (file)
@@ -32,27 +32,10 @@ suspend fun ApplicationCall.loreArticlePage(): HTML.() -> Unit {
        } else {
                val pageTemplate = pageFile.readText()
                val pageMarkup = PreParser.preparse(pagePath, pageTemplate)
-               val pageResult = TextParserState.parseText(pageMarkup, TextParserFormattingTag.asTags, Unit)
-               val pageHtml = pageResult.html
-               if (!pageResult.succeeded) {
-                       return page("Error rendering page", standardNavBar(pagePathParts), null) {
-                               section {
-                                       a { id = "page-top" }
-                                       unsafe { raw(pageHtml) }
-                               }
-                       }
-               }
+               val pageHtml = TextParserState.parseText(pageMarkup, TextParserFormattingTag.asTags, Unit)
                
                val pageToC = TableOfContentsBuilder()
-               val pageToCResult = TextParserState.parseText(pageMarkup, TextParserToCBuilderTag.asTags, pageToC)
-               if (!pageToCResult.succeeded) {
-                       return page("Error generating table of contents", standardNavBar(pagePathParts), null) {
-                               section {
-                                       a { id = "page-top" }
-                                       unsafe { raw(pageToCResult.html) }
-                               }
-                       }
-               }
+               TextParserState.parseText(pageMarkup, TextParserToCBuilderTag.asTags, pageToC)
                
                val pageNav = pageToC.toNavBar() + NavLink("#comments", "Comments", aClasses = "left")
                
index 0a7a83cb541090697f43ce7f48f7f00192757324..002e495f8cec5ded33dfdef0d93fed5f9d0883c3 100644 (file)
@@ -817,6 +817,12 @@ iframe {
 
 #error-popup {
        z-index: 998;
+
+       position: fixed;
+       width: 100vw;
+       height: 100vh;
+       left: 0;
+       top: 0;
 }
 
 #error-popup > .bg {