Fix HTML generation so it's a lot better now
authorLanius Trolling <lanius@laniustrolling.dev>
Fri, 23 Feb 2024 23:30:26 +0000 (18:30 -0500)
committerLanius Trolling <lanius@laniustrolling.dev>
Fri, 23 Feb 2024 23:30:26 +0000 (18:30 -0500)
src/jvmMain/kotlin/info/mechyrdia/lore/parser.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_plain.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_reply.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_toc.kt

index db20ec997170a22e297682ad3edaeda17b087818..17230a08a6e7b70f9d7e53122e683d312aa37275 100644 (file)
@@ -1,11 +1,15 @@
 package info.mechyrdia.lore
 
 data class TextParserScope<TContext>(
-       val write: Appendable,
+       val write: StringBuilder,
        val tags: TextParserTags<TContext>,
-       val ctx: TContext
+       val ctx: TContext,
 )
 
+class TextParserInternalState {
+       var suppressEndParagraph: Boolean = false
+}
+
 sealed class InsideTag {
        abstract val tag: String
        
@@ -15,12 +19,15 @@ sealed class InsideTag {
 
 sealed class TextParserState<TContext>(
        val scope: TextParserScope<TContext>,
-       val insideTags: List<InsideTag>
+       val insideTags: List<InsideTag>,
+       protected val internalState: TextParserInternalState,
 ) {
        abstract fun processCharacter(char: Char): TextParserState<TContext>
        abstract fun processEndOfText()
        
        protected fun appendText(text: String) {
+               if (text.isEmpty()) return
+               
                scope.write.append(
                        insideTags.foldRight(censorText(text)) { insideTag, t ->
                                if (insideTag is InsideTag.IndirectTag) {
@@ -31,18 +38,45 @@ sealed class TextParserState<TContext>(
                                } else t
                        }
                )
+               internalState.suppressEndParagraph = false
        }
        
        protected fun appendTextRaw(text: String) {
+               if (text.isEmpty()) return
+               
                scope.write.append(text)
+               internalState.suppressEndParagraph = false
        }
        
-       class Initial<TContext>(scope: TextParserScope<TContext>) : TextParserState<TContext>(scope, listOf()) {
+       protected fun nextParagraph() {
+               val newline = if (insideTags.none { it is InsideTag.DirectTag }) {
+                       if (internalState.suppressEndParagraph)
+                               "<p>"
+                       else
+                               "</p><p>"
+               } else
+                       "<br/>"
+               
+               appendTextRaw(newline)
+       }
+       
+       protected fun cancelEndParagraph() {
+               internalState.suppressEndParagraph = true
+       }
+       
+       protected fun cancelStartParagraph() {
+               if (scope.write.endsWith("<p>"))
+                       scope.write.deleteRange(scope.write.length - 3, scope.write.length)
+       }
+       
+       class Initial<TContext>(scope: TextParserScope<TContext>) : TextParserState<TContext>(scope, listOf(), TextParserInternalState()) {
                override fun processCharacter(char: Char): TextParserState<TContext> {
                        return if (char == '[')
-                               OpenTag(scope, "", insideTags)
-                       else
-                               PlainText(scope, "$char", insideTags)
+                               OpenTag(scope, "", insideTags, internalState)
+                       else {
+                               appendTextRaw("<p>")
+                               PlainText(scope, "$char", insideTags, internalState)
+                       }
                }
                
                override fun processEndOfText() {
@@ -50,118 +84,136 @@ sealed class TextParserState<TContext>(
                }
        }
        
-       class PlainText<TContext>(scope: TextParserScope<TContext>, private val text: String, insideTags: List<InsideTag>) : TextParserState<TContext>(scope, insideTags) {
+       class PlainText<TContext>(scope: TextParserScope<TContext>, private val text: String, insideTags: List<InsideTag>, internalState: TextParserInternalState) : TextParserState<TContext>(scope, insideTags, internalState) {
                override fun processCharacter(char: Char): TextParserState<TContext> {
                        return if (char == '[') {
                                appendText(text)
-                               OpenTag(scope, "", insideTags)
+                               OpenTag(scope, "", insideTags, internalState)
                        } else if (char == '\n' && text.endsWith('\n')) {
                                appendText(text.removeSuffix("\n"))
                                
-                               val newline = if (insideTags.none { it is InsideTag.DirectTag })
-                                       "</p><p>"
-                               else
-                                       "<br/>"
+                               nextParagraph()
                                
-                               appendTextRaw(newline)
-                               PlainText(scope, "", insideTags)
+                               PlainText(scope, "", insideTags, internalState)
                        } else
-                               PlainText(scope, text + char, insideTags)
+                               PlainText(scope, text + char, insideTags, internalState)
                }
                
                override fun processEndOfText() {
-                       appendText(text)
+                       appendText(text.removeSuffix("\n"))
+                       if (text.isNotBlank())
+                               appendTextRaw("</p>")
                }
        }
        
-       class NoFormatText<TContext>(scope: TextParserScope<TContext>, private val text: String, insideTags: List<InsideTag>) : TextParserState<TContext>(scope, insideTags) {
+       class NoFormatText<TContext>(scope: TextParserScope<TContext>, private val text: String, insideTags: List<InsideTag>, internalState: TextParserInternalState) : TextParserState<TContext>(scope, insideTags, internalState) {
                override fun processCharacter(char: Char): TextParserState<TContext> {
                        val newText = text + char
                        return if (newText.endsWith("[/$NO_FORMAT_TAG]")) {
                                appendText(newText.removeSuffix("[/$NO_FORMAT_TAG]"))
-                               PlainText(scope, "", insideTags)
+                               PlainText(scope, "", insideTags, internalState)
                        } else if (newText.endsWith('\n')) {
                                appendText(newText.removeSuffix("\n"))
                                appendTextRaw("<br/>")
-                               NoFormatText(scope, "", insideTags)
+                               NoFormatText(scope, "", insideTags, internalState)
                        } else
-                               NoFormatText(scope, newText, insideTags)
+                               NoFormatText(scope, newText, insideTags, internalState)
                }
                
                override fun processEndOfText() {
-                       appendText(text)
+                       appendText(text.removeSuffix("\n"))
+                       if (text.isNotBlank())
+                               appendTextRaw("</p>")
                }
        }
        
-       class OpenTag<TContext>(scope: TextParserScope<TContext>, private val tag: String, insideTags: List<InsideTag>) : TextParserState<TContext>(scope, insideTags) {
+       class OpenTag<TContext>(scope: TextParserScope<TContext>, private val tag: String, insideTags: List<InsideTag>, internalState: TextParserInternalState) : TextParserState<TContext>(scope, insideTags, internalState) {
                override fun processCharacter(char: Char): TextParserState<TContext> {
                        return if (char == ']') {
                                if (tag.equals(NO_FORMAT_TAG, ignoreCase = true))
-                                       NoFormatText(scope, "", insideTags)
-                               else when (val tagType = scope.tags[tag]) {
-                                       is TextParserTagType.Direct<TContext> -> {
-                                               appendTextRaw(tagType.begin(null, scope.ctx))
-                                               PlainText(scope, "", insideTags + InsideTag.DirectTag(tag))
-                                       }
+                                       NoFormatText(scope, "", insideTags, internalState)
+                               else {
+                                       val tagType = scope.tags[tag]
+                                       if (tagType?.isBlock == true)
+                                               cancelStartParagraph()
                                        
-                                       is TextParserTagType.Indirect<TContext> -> PlainText(scope, "", insideTags + InsideTag.IndirectTag(tag, null))
-                                       
-                                       else -> PlainText(scope, "[$tag]", insideTags)
+                                       when (tagType) {
+                                               is TextParserTagType.Direct<TContext> -> {
+                                                       appendTextRaw(tagType.begin(null, scope.ctx))
+                                                       PlainText(scope, "", insideTags + InsideTag.DirectTag(tag), internalState)
+                                               }
+                                               
+                                               is TextParserTagType.Indirect<TContext> -> PlainText(scope, "", insideTags + InsideTag.IndirectTag(tag, null), internalState)
+                                               
+                                               else -> PlainText(scope, "[$tag]", insideTags, internalState)
+                                       }
                                }
                        } else if (char == '/' && tag == "")
-                               CloseTag(scope, tag, insideTags)
+                               CloseTag(scope, tag, insideTags, internalState)
                        else if (char == '=' && tag != "")
-                               TagParam(scope, tag, "", insideTags)
+                               TagParam(scope, tag, "", insideTags, internalState)
                        else
-                               OpenTag(scope, tag + char, insideTags)
+                               OpenTag(scope, tag + char, insideTags, internalState)
                }
                
                override fun processEndOfText() {
                        appendText("[$tag")
+                       appendTextRaw("</p>")
                }
        }
        
-       class TagParam<TContext>(scope: TextParserScope<TContext>, private val tag: String, private val param: String, insideTags: List<InsideTag>) : TextParserState<TContext>(scope, insideTags) {
+       class TagParam<TContext>(scope: TextParserScope<TContext>, private val tag: String, private val param: String, insideTags: List<InsideTag>, internalState: TextParserInternalState) : TextParserState<TContext>(scope, insideTags, internalState) {
                override fun processCharacter(char: Char): TextParserState<TContext> {
-                       return if (char == ']')
-                               when (val tagType = scope.tags[tag]) {
+                       return if (char == ']') {
+                               val tagType = scope.tags[tag]
+                               if (tagType?.isBlock == true)
+                                       cancelStartParagraph()
+                               
+                               when (tagType) {
                                        is TextParserTagType.Direct<TContext> -> {
                                                appendTextRaw(tagType.begin(param, scope.ctx))
-                                               PlainText(scope, "", insideTags + InsideTag.DirectTag(tag))
+                                               PlainText(scope, "", insideTags + InsideTag.DirectTag(tag), internalState)
                                        }
                                        
-                                       is TextParserTagType.Indirect<TContext> -> PlainText(scope, "", insideTags + InsideTag.IndirectTag(tag, param))
+                                       is TextParserTagType.Indirect<TContext> -> PlainText(scope, "", insideTags + InsideTag.IndirectTag(tag, param), internalState)
                                        
-                                       else -> PlainText(scope, "[$tag=$param]", insideTags)
+                                       else -> PlainText(scope, "[$tag=$param]", insideTags, internalState)
                                }
-                       else
-                               TagParam(scope, tag, param + char, insideTags)
+                       else
+                               TagParam(scope, tag, param + char, insideTags, internalState)
                }
                
                override fun processEndOfText() {
                        appendText("[$tag=$param")
+                       appendTextRaw("</p>")
                }
        }
        
-       class CloseTag<TContext>(scope: TextParserScope<TContext>, private val tag: String, insideTags: List<InsideTag>) : TextParserState<TContext>(scope, insideTags) {
+       class CloseTag<TContext>(scope: TextParserScope<TContext>, private val tag: String, insideTags: List<InsideTag>, internalState: TextParserInternalState) : TextParserState<TContext>(scope, insideTags, internalState) {
                override fun processCharacter(char: Char): TextParserState<TContext> {
                        return if (char == ']') {
                                val tagType = scope.tags[tag]
-                               if (tagType is TextParserTagType.Direct<TContext> && insideTags.lastOrNull()?.tag == tag) {
+                               val nextState = if (tagType is TextParserTagType.Direct<TContext> && insideTags.lastOrNull()?.tag == tag) {
                                        appendTextRaw(tagType.end(scope.ctx))
                                        
-                                       PlainText(scope, "", insideTags.dropLast(1))
+                                       PlainText(scope, "", insideTags.dropLast(1), internalState)
                                } else if (insideTags.isNotEmpty() && (insideTags.last() as? InsideTag.IndirectTag)?.tag == tag) {
-                                       PlainText(scope, "", insideTags.dropLast(1))
+                                       PlainText(scope, "", insideTags.dropLast(1), internalState)
                                } else {
                                        appendText("[/$tag]")
-                                       PlainText(scope, "", insideTags)
+                                       PlainText(scope, "", insideTags, internalState)
                                }
-                       } else CloseTag(scope, tag + char, insideTags)
+                               
+                               if (tagType?.isBlock == true)
+                                       cancelEndParagraph()
+                               
+                               nextState
+                       } else CloseTag(scope, tag + char, insideTags, internalState)
                }
                
                override fun processEndOfText() {
                        appendText("[/$tag")
+                       appendTextRaw("</p>")
                }
        }
        
@@ -187,7 +239,7 @@ sealed class TextParserState<TContext>(
                                .fold<TextParserState<TContext>>(Initial(TextParserScope(builder, tags, context))) { state, char ->
                                        state.processCharacter(char)
                                }.processEndOfText()
-                       return "<p>$builder</p>"
+                       return "$builder"
                }
        }
 }
index b5a44594c26cd2056d8b59f562a5e085c1fcbcbb..42ff317b90f7c7bebdba35fc0cb39b292bf008d7 100644 (file)
@@ -1,16 +1,19 @@
 package info.mechyrdia.lore
 
 private val plainTextFormattingTag = TextParserTagType.Direct<Unit>(
+       false,
        { _, _ -> "" },
        { "" },
 )
 
 private val spacedFormattingTag = TextParserTagType.Direct<Unit>(
+       true,
        { _, _ -> " " },
        { " " },
 )
 
-private val embeddedFormattingTag = TextParserTagType.Indirect<Unit> { _, _, _ -> "" }
+private val embeddedFormattingTag = TextParserTagType.Indirect<Unit>(false) { _, _, _ -> "" }
+private val embeddedBlockFormattingTag = TextParserTagType.Indirect<Unit>(true) { _, _, _ -> "" }
 
 enum class TextParserFormattingTagPlainText(val type: TextParserTagType<Unit>) {
        // Basic formatting
@@ -18,8 +21,8 @@ enum class TextParserFormattingTagPlainText(val type: TextParserTagType<Unit>) {
        I(plainTextFormattingTag),
        U(plainTextFormattingTag),
        S(plainTextFormattingTag),
-       SUP(spacedFormattingTag),
-       SUB(spacedFormattingTag),
+       SUP(plainTextFormattingTag),
+       SUB(plainTextFormattingTag),
        COLOR(plainTextFormattingTag),
        IPA(plainTextFormattingTag),
        CODE(plainTextFormattingTag),
@@ -40,7 +43,7 @@ enum class TextParserFormattingTagPlainText(val type: TextParserTagType<Unit>) {
        IMAGE(embeddedFormattingTag),
        MODEL(embeddedFormattingTag),
        AUDIO(embeddedFormattingTag),
-       QUIZ(embeddedFormattingTag),
+       QUIZ(embeddedBlockFormattingTag),
        
        // Lists
        UL(spacedFormattingTag),
@@ -61,8 +64,8 @@ enum class TextParserFormattingTagPlainText(val type: TextParserTagType<Unit>) {
        
        // Conlangs
        LANG(plainTextFormattingTag),
-       ALPHABET(embeddedFormattingTag),
-       VOCAB(embeddedFormattingTag),
+       ALPHABET(embeddedBlockFormattingTag),
+       VOCAB(embeddedBlockFormattingTag),
        ;
        
        companion object {
@@ -103,8 +106,9 @@ enum class TextParserCommentTagsPlainText(val type: TextParserTagType<Unit>) {
        
        REPLY(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> ">>" },
-                       { "" },
+                       { _ -> "" },
                )
        ),
        
index 8e96602b81db3188562d4c3171a1a7f102b00b4c..dab6cdf5e08ba0c653bb844784b8c3584ba3d32a 100644 (file)
@@ -15,7 +15,7 @@ class CommentRepliesBuilder {
 
 enum class TextParserReplyCounterTag(val type: TextParserTagType<CommentRepliesBuilder>) {
        REPLY(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(false) { _, content, builder ->
                        sanitizeId(content)?.let { id ->
                                builder.addReplyTag(Id(id))
                        }
index f1dc72ce837aeec4576625dd12f18ce477d58cc4..b25e631f329e0d36ddb963f620ba31799dcd6eed 100644 (file)
@@ -8,12 +8,14 @@ import kotlinx.serialization.json.JsonPrimitive
 import java.io.File
 
 sealed class TextParserTagType<TContext> {
-       data class Direct<TContext>(val beginFunc: (String?, TContext) -> String, val endFunc: (TContext) -> String) : TextParserTagType<TContext>() {
+       abstract val isBlock: Boolean
+       
+       data class Direct<TContext>(override val isBlock: Boolean, val beginFunc: (String?, TContext) -> String, val endFunc: (TContext) -> String) : TextParserTagType<TContext>() {
                fun begin(param: String?, context: TContext) = beginFunc(param, context)
                fun end(context: TContext) = endFunc(context)
        }
        
-       data class Indirect<TContext>(val processFunc: (String?, String, TContext) -> String) : TextParserTagType<TContext>() {
+       data class Indirect<TContext>(override val isBlock: Boolean, val processFunc: (String?, String, TContext) -> String) : TextParserTagType<TContext>() {
                fun process(param: String?, content: String, context: TContext) = processFunc(param, content, context)
        }
 }
@@ -30,11 +32,12 @@ value class TextParserTags<TContext> private constructor(private val tags: Map<S
                fun <TContext> byIgnoringContext(tags: TextParserTags<Unit>) = TextParserTags<TContext>(tags.tags.mapValues { (_, tag) ->
                        when (tag) {
                                is TextParserTagType.Direct -> TextParserTagType.Direct(
+                                       tag.isBlock,
                                        { param, _ -> tag.begin(param, Unit) },
                                        { _ -> tag.end(Unit) }
                                )
                                
-                               is TextParserTagType.Indirect -> TextParserTagType.Indirect { param, content, _ ->
+                               is TextParserTagType.Indirect -> TextParserTagType.Indirect(tag.isBlock) { param, content, _ ->
                                        tag.process(param, content, Unit)
                                }
                        }
@@ -48,140 +51,152 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
        // Basic formatting
        B(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "<span style='font-weight: bold'>" },
-                       { "</span>" },
+                       { _ -> "</span>" },
                )
        ),
        I(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "<span style='font-style: italic'>" },
-                       { "</span>" },
+                       { _ -> "</span>" },
                )
        ),
        U(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "<span style='text-decoration: underline'>" },
-                       { "</span>" },
+                       { _ -> "</span>" },
                )
        ),
        S(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "<span style='text-decoration: line-through'>" },
-                       { "</span>" },
+                       { _ -> "</span>" },
                )
        ),
        SUP(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "<sup>" },
-                       { "</sup>" },
+                       { _ -> "</sup>" },
                )
        ),
        SUB(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "<sub>" },
-                       { "</sub>" },
+                       { _ -> "</sub>" },
                )
        ),
        COLOR(
                TextParserTagType.Direct(
+                       false,
                        { tagParam, _ ->
                                val color = tagParam?.toIntOrNull(16)?.toString(16)?.padStart(6, '0')
                                val style = color?.let { " style=\"color: #$it\"" } ?: ""
                                "<span$style>"
                        },
-                       { "</span>" },
+                       { _ -> "</span>" },
                )
        ),
        IPA(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "<span style='font-family: DejaVu Sans'>" },
-                       { "</span>" },
+                       { _ -> "</span>" },
                )
        ),
        CODE(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "<span style='font-family: JetBrains Mono'><pre>" },
-                       { "</pre></span>" },
+                       { _ -> "</pre></span>" },
                )
        ),
        H1(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        "<h1>$content</h1><script>window.checkRedirectTarget('');</script>"
                }
        ),
        H2(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        val anchor = headerContentToAnchor(content)
                        "</section><section><h2><a id='$anchor'></a>$content</h2><script>window.checkRedirectTarget('#$anchor');</script>"
                }
        ),
        H3(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        val anchor = headerContentToAnchor(content)
                        "<h3><a id='$anchor'></a>$content</h3><script>window.checkRedirectTarget('#$anchor');</script>"
                }
        ),
        H4(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        val anchor = headerContentToAnchor(content)
                        "<h4><a id='$anchor'></a>$content</h4><script>window.checkRedirectTarget('#$anchor');</script>"
                }
        ),
        H5(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        val anchor = headerContentToAnchor(content)
                        "<h5><a id='$anchor'></a>$content</h5><script>window.checkRedirectTarget('#$anchor');</script>"
                }
        ),
        H6(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        val anchor = headerContentToAnchor(content)
                        "<h6><a id='$anchor'></a>$content</h6><script>window.checkRedirectTarget('#$anchor');</script>"
                }
        ),
        ALIGN(
                TextParserTagType.Direct(
+                       true,
                        { tagParam, _ ->
                                val alignments = setOf("left", "center", "right", "justify")
                                val alignment = tagParam?.takeIf { it in alignments }
                                val styleAttr = alignment?.let { " style=\"text-align: $it\"" } ?: ""
                                "<div$styleAttr>"
                        },
-                       { "</div>" },
+                       { _ -> "</div>" },
                )
        ),
        ASIDE(
                TextParserTagType.Direct(
+                       true,
                        { tagParam, _ ->
                                val floats = setOf("left", "right")
                                val float = tagParam?.takeIf { it in floats } ?: "right"
                                "<div style='float: $float; max-width: var(--aside-width)'>"
                        },
-                       { "</div>" },
+                       { _ -> "</div>" },
                )
        ),
        BLOCKQUOTE(
-               TextParserTagType.Direct({ _, _ ->
-                       "<blockquote>"
-               }, { _ ->
-                       "</blockquote>"
-               })
+               TextParserTagType.Direct(
+                       true,
+                       { _, _ -> "<blockquote>" },
+                       { _ -> "</blockquote>" }
+               )
        ),
        
        // Metadata
        DESC(
                TextParserTagType.Direct(
+                       false,
                        { _, _ -> "" },
-                       { "" },
+                       { _ -> "" },
                )
        ),
        THUMB(
-               TextParserTagType.Indirect { _, _, _ -> "" }
+               TextParserTagType.Indirect(true) { _, _, _ -> "" }
        ),
        
        // Resource showing
        IMAGE(
-               TextParserTagType.Indirect { tagParam, content, _ ->
+               TextParserTagType.Indirect(false) { tagParam, content, _ ->
                        val imageUrl = sanitizeLink(content)
                        
                        val (width, height) = getSizeParam(tagParam)
@@ -196,7 +211,7 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
                }
        ),
        MODEL(
-               TextParserTagType.Indirect { tagParam, content, _ ->
+               TextParserTagType.Indirect(false) { tagParam, content, _ ->
                        val modelUrl = sanitizeLink(content)
                        
                        val (width, height) = getSizeParam(tagParam)
@@ -206,14 +221,14 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
                }
        ),
        AUDIO(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(false) { _, content, _ ->
                        val audioUrl = sanitizeLink(content)
                        
                        "<audio src=\"/assets/sounds/$audioUrl\" controls></audio>"
                }
        ),
        QUIZ(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        val quizText = File(Configuration.CurrentConfiguration.quizDir).combineSafe("$content.json").readText()
                        val quizJson = JsonStorageCodec.encodeToString(String.serializer(), quizText)
                        
@@ -224,62 +239,70 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
        // Lists
        UL(
                TextParserTagType.Direct(
+                       true,
                        { _, _ -> "<ul>" },
-                       { "</ul>" },
+                       { _ -> "</ul>" },
                )
        ),
        OL(
                TextParserTagType.Direct(
+                       true,
                        { _, _ -> "<ol>" },
-                       { "</ol>" },
+                       { _ -> "</ol>" },
                )
        ),
        LI(
                TextParserTagType.Direct(
+                       true,
                        { _, _ -> "<li>" },
-                       { "</li>" },
+                       { _ -> "</li>" },
                )
        ),
        
        // Tables
        TABLE(
                TextParserTagType.Direct(
+                       true,
                        { _, _ -> "<table>" },
-                       { "</table>" },
+                       { _ -> "</table>" },
                )
        ),
        TR(
                TextParserTagType.Direct(
+                       true,
                        { _, _ -> "<tr>" },
-                       { "</tr>" },
+                       { _ -> "</tr>" },
                )
        ),
        TD(
                TextParserTagType.Direct(
+                       true,
                        { tagParam, _ ->
                                val (width, height) = getSizeParam(tagParam)
                                val sizeAttrs = getTableSizeAttributes(width, height)
                                
                                "<td$sizeAttrs>"
                        },
-                       { "</td>" },
+                       { _ -> "</td>" },
                )
        ),
        TH(
                TextParserTagType.Direct(
+                       true,
                        { tagParam, _ ->
                                val (width, height) = getSizeParam(tagParam)
                                val sizeAttrs = getTableSizeAttributes(width, height)
                                
                                "<th$sizeAttrs>"
                        },
-                       { "</th>" },
+                       { _ -> "</th>" },
                )
        ),
        
        // Hyperformatting
        LINK(
                TextParserTagType.Direct(
+                       false,
                        { tagParam, _ ->
                                val param = tagParam?.let { TextParserState.censorText(it) }
                                val url = param?.let { if (it.startsWith('/')) "/lore$it" else "./$it" }
@@ -287,28 +310,29 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
                                
                                "<a$attr>"
                        },
-                       { "</a>" },
+                       { _ -> "</a>" },
                )
        ),
        EXTLINK(
                TextParserTagType.Direct(
+                       false,
                        { tagParam, _ ->
                                val url = tagParam?.let { TextParserState.censorText(it) }
                                val attr = url?.let { " href=\"$it\"" } ?: ""
                                
                                "<a$attr>"
                        },
-                       { "</a>" },
+                       { _ -> "</a>" },
                )
        ),
        ANCHOR(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(false) { _, content, _ ->
                        val anchor = sanitizeLink(content)
                        "<a id=\"$anchor\" name=\"$anchor\"></a>"
                }
        ),
        REDIRECT(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(false) { _, content, _ ->
                        val target = TextParserState.censorText(content)
                        val url = if (target.startsWith('/')) "/lore$target" else "./$target"
                        val string = JsonPrimitive(url).toString()
@@ -319,7 +343,7 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
        
        // Conlangs
        LANG(
-               TextParserTagType.Indirect { tagParam, content, _ ->
+               TextParserTagType.Indirect(false) { tagParam, content, _ ->
                        if (tagParam?.equals("tylan", ignoreCase = true) == true) {
                                val uncensored = TextParserState.uncensorText(content)
                                val tylan = TylanAlphabetFont.tylanToFontAlphabet(uncensored)
@@ -338,7 +362,7 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
                }
        ),
        ALPHABET(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        if (content.equals("mechyrdian", ignoreCase = true)) {
                                """
                                        |<div class="mechyrdia-sans-box">
@@ -394,7 +418,7 @@ enum class TextParserFormattingTag(val type: TextParserTagType<Unit>) {
                }
        ),
        VOCAB(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(true) { _, content, _ ->
                        if (content.isBlank())
                                ""
                        else {
@@ -436,6 +460,7 @@ enum class TextParserCommentTags(val type: TextParserTagType<Unit>) {
        TH(TextParserFormattingTag.TH.type),
        URL(
                TextParserTagType.Direct(
+                       false,
                        { tagParam, _ ->
                                val url = tagParam?.let { TextParserState.censorText(it) }
                                val attr = url?.let { " href=\"$it\" rel=\"ugc nofollow\"" } ?: ""
@@ -449,7 +474,7 @@ enum class TextParserCommentTags(val type: TextParserTagType<Unit>) {
        LANG(TextParserFormattingTag.LANG.type),
        
        IMGUR(
-               TextParserTagType.Indirect { tagParam, content, _ ->
+               TextParserTagType.Indirect(false) { tagParam, content, _ ->
                        val imageUrl = sanitizeExtLink(content)
                        val (width, height) = getSizeParam(tagParam)
                        
@@ -457,7 +482,7 @@ enum class TextParserCommentTags(val type: TextParserTagType<Unit>) {
                }
        ),
        IMGBB(
-               TextParserTagType.Indirect { tagParam, content, _ ->
+               TextParserTagType.Indirect(false) { tagParam, content, _ ->
                        val imageUrl = sanitizeExtLink(content)
                        val (width, height) = getSizeParam(tagParam)
                        
@@ -466,7 +491,7 @@ enum class TextParserCommentTags(val type: TextParserTagType<Unit>) {
        ),
        
        REPLY(
-               TextParserTagType.Indirect { _, content, _ ->
+               TextParserTagType.Indirect(false) { _, content, _ ->
                        sanitizeId(content)?.let { id ->
                                "<a href=\"/comment/view/$id\" rel=\"ugc\">&gt;&gt;$id</a>"
                        } ?: "[reply]$content[/reply]"
@@ -474,11 +499,11 @@ enum class TextParserCommentTags(val type: TextParserTagType<Unit>) {
        ),
        
        QUOTE(
-               TextParserTagType.Direct({ _, _ ->
-                       "<blockquote>"
-               }, { _ ->
-                       "</blockquote>"
-               })
+               TextParserTagType.Direct(
+                       true,
+                       { _, _ -> "<blockquote>" },
+                       { _ -> "</blockquote>" }
+               )
        )
        ;
        
index 4b82f0b66b3b6bdbd6a1e046264fffba383babf7..bc582916550f08792c57b07c753c13559ecb8dc2 100644 (file)
@@ -54,55 +54,55 @@ class TableOfContentsBuilder {
 
 enum class TextParserToCBuilderTag(val type: TextParserTagType<TableOfContentsBuilder>) {
        H1(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(true) { _, content, builder ->
                        builder.addHeader(headerContentToLabel(content), 0, headerContentToAnchor(content))
-                       "[h1]$content[/h1]"
+                       content
                }
        ),
        H2(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(true) { _, content, builder ->
                        builder.addHeader(headerContentToLabel(content), 1, headerContentToAnchor(content))
-                       "[h2]$content[/h2]"
+                       content
                }
        ),
        H3(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(true) { _, content, builder ->
                        builder.addHeader(headerContentToLabel(content), 2, headerContentToAnchor(content))
-                       "[h3]$content[/h3]"
+                       content
                }
        ),
        H4(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(true) { _, content, builder ->
                        builder.addHeader(headerContentToLabel(content), 3, headerContentToAnchor(content))
-                       "[h4]$content[/h4]"
+                       content
                }
        ),
        H5(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(true) { _, content, builder ->
                        builder.addHeader(headerContentToLabel(content), 4, headerContentToAnchor(content))
-                       "[h5]$content[/h5]"
+                       content
                }
        ),
        H6(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(true) { _, content, builder ->
                        builder.addHeader(headerContentToLabel(content), 5, headerContentToAnchor(content))
-                       "[h6]$content[/h6]"
+                       content
                }
        ),
        DESC(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(false) { _, content, builder ->
                        builder.addDescription(descriptionContentToPlainText(content))
                        content
                }
        ),
        IMAGE(
-               TextParserTagType.Indirect { param, content, builder ->
+               TextParserTagType.Indirect(false) { param, content, builder ->
                        builder.addImage(imagePathToOpenGraphValue(content))
-                       "[image=$param]$content[/image]"
+                       ""
                }
        ),
        THUMB(
-               TextParserTagType.Indirect { _, content, builder ->
+               TextParserTagType.Indirect(false) { _, content, builder ->
                        builder.addImage(imagePathToOpenGraphValue(content), true)
                        ""
                }