}
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
)
}
}
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
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()
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(
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 }
}
}
}
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() }
}
}
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(),
|[/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:"
}
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 {
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 != "")
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)
}
.replace(">", ">")
.replace("&", "&")
- 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)
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()
}
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>) {
} 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")
#error-popup {
z-index: 998;
+
+ position: fixed;
+ width: 100vw;
+ height: 100vh;
+ left: 0;
+ top: 0;
}
#error-popup > .bg {