From c91d1906e2153afc1127446defb4668796cbffef Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Thu, 7 Mar 2024 11:52:50 -0500 Subject: [PATCH] Add raw factbook rendering --- .../kotlin/info/mechyrdia/Factbooks.kt | 8 + .../info/mechyrdia/lore/article_listing.kt | 6 +- .../kotlin/info/mechyrdia/lore/parser_raw.kt | 188 ++++++++++++++++++ .../kotlin/info/mechyrdia/lore/parser_tags.kt | 4 +- .../kotlin/info/mechyrdia/lore/view_tpl.kt | 26 +++ .../kotlin/info/mechyrdia/lore/views_error.kt | 81 ++++---- .../kotlin/info/mechyrdia/lore/views_lore.kt | 76 +++++++ .../kotlin/info/mechyrdia/lore/views_quote.kt | 23 +++ 8 files changed, 368 insertions(+), 44 deletions(-) create mode 100644 src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt b/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt index 9a8d379..12738f5 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt @@ -148,6 +148,10 @@ fun Application.factbooks() { call.respondHtml(HttpStatusCode.OK, call.loreArticlePage()) } + get("/lore.raw") { + call.respondHtml(HttpStatusCode.OK, call.loreRawArticlePage("")) + } + staticFiles("/assets", File(Configuration.CurrentConfiguration.assetDir), index = null) { enableAutoHeadResponse() } @@ -162,6 +166,10 @@ fun Application.factbooks() { with(call) { respondHtml(HttpStatusCode.OK, randomQuote().toHtml("Random Quote")) } } + get("/quote.raw") { + with(call) { respondHtml(HttpStatusCode.OK, randomQuote().toRawHtml("Random Quote")) } + } + get("/quote.json") { call.respondText(randomQuote().toJson(), ContentType.Application.Json) } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt index 5bac26d..14643cc 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt @@ -31,15 +31,15 @@ val ArticleNode.isViewable: Boolean val File.isViewable: Boolean get() = name.isViewable -fun List.renderInto(list: UL, base: String? = null) { +fun List.renderInto(list: UL, base: String? = null, suffix: String = "") { val prefix by lazy(LazyThreadSafetyMode.NONE) { base?.let { "$it/" } ?: "" } for (node in this) { if (node.isViewable) list.li { - a(href = "/lore/$prefix${node.name}") { +node.name } + a(href = "/lore/$prefix${node.name}$suffix") { +node.name } if (node.subNodes.isNotEmpty()) ul { - node.subNodes.renderInto(this, "$prefix${node.name}") + node.subNodes.renderInto(this, "$prefix${node.name}", suffix) } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt new file mode 100644 index 0000000..1cc340b --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt @@ -0,0 +1,188 @@ +package info.mechyrdia.lore + +import info.mechyrdia.Configuration +import io.ktor.util.* +import java.io.File + +fun String.toRawLink() = substringBeforeLast('#') + ".raw" + +enum class TextParserRawPageTag(val type: TextParserTagType) { + IPA( + TextParserTagType.Direct( + false, + { _, _ -> "" }, + { _ -> "" }, + ) + ), + CODE( + TextParserTagType.Direct( + false, + { _, _ -> "" }, + { _ -> "" }, + ) + ), + CODE_BLOCK( + TextParserTagType.Direct( + false, + { _, _ -> "
" },
+			{ _ -> "
" }, + ) + ), + H1( + TextParserTagType.Direct( + true, + { _, _ -> "

" }, + { _ -> "

" } + ) + ), + H2( + TextParserTagType.Direct( + true, + { _, _ -> "

" }, + { _ -> "

" } + ) + ), + H3( + TextParserTagType.Direct( + true, + { _, _ -> "

" }, + { _ -> "

" } + ) + ), + H4( + TextParserTagType.Direct( + true, + { _, _ -> "

" }, + { _ -> "

" } + ) + ), + H5( + TextParserTagType.Direct( + true, + { _, _ -> "
" }, + { _ -> "
" } + ) + ), + H6( + TextParserTagType.Direct( + true, + { _, _ -> "
" }, + { _ -> "
" } + ) + ), + ASIDE( + TextParserTagType.Direct( + true, + { tagParam, _ -> + val floats = setOf("left", "right") + val float = tagParam?.takeIf { it in floats } ?: "right" + "
" + }, + { _ -> "
" }, + ) + ), + IMAGE( + TextParserTagType.Indirect(false) { tagParam, content, _ -> + val imageUrl = sanitizeLink(content) + + val (width, height) = getSizeParam(tagParam) + + if (imageUrl.endsWith(".svg")) + File(Configuration.CurrentConfiguration.assetDir, "images") + .combineSafe(imageUrl) + .readText() + .replace("" + } + ), + MODEL( + TextParserTagType.Indirect(false) { _, _, _ -> + "Unfortunately, raw view does not support interactive 3D model views" + } + ), + QUIZ( + TextParserTagType.Indirect(true) { _, _, _ -> + "

Unfortunately, raw view does not support interactive quizzes

" + } + ), + TABLE( + TextParserTagType.Direct( + true, + { _, _ -> "" }, + { _ -> "
" }, + ) + ), + TD( + TextParserTagType.Direct( + true, + { tagParam, _ -> + val (width, height) = getSizeParam(tagParam) + val sizeAttrs = getTableSizeAttributes(width, height) + + "" + }, + { _ -> "" }, + ) + ), + TH( + TextParserTagType.Direct( + true, + { tagParam, _ -> + val (width, height) = getSizeParam(tagParam) + val sizeAttrs = getTableSizeAttributes(width, height) + + "" + }, + { _ -> "" }, + ) + ), + LINK( + TextParserTagType.Direct( + false, + { tagParam, _ -> + val param = tagParam?.let { TextParserState.censorText(it) } + val url = param?.let { if (it.startsWith('/')) "/lore$it" else "./$it" } + val attr = url?.let { " href=\"${it.toRawLink()}\"" } ?: "" + + "" + }, + { _ -> "" }, + ) + ), + REDIRECT( + TextParserTagType.Indirect(false) { _, content, _ -> + val target = TextParserState.censorText(content) + val url = if (target.startsWith('/')) "/lore$target" else "./$target" + + "Click here for a manual redirect" + } + ), + LANG( + TextParserTagType.Direct( + false, + { _, _ -> "" }, + { _ -> "" } + ) + ), + ALPHABET( + TextParserTagType.Indirect(true) { _, _, _ -> + "

Unfortunately, raw view does not support interactive conscript previews

" + } + ), + VOCAB( + TextParserTagType.Indirect(true) { _, _, _ -> + "

Unfortunately, raw view does not support interactive conlang dictionaries

" + } + ), + ; + + companion object { + val asTags: TextParserTags by lazy { + TextParserFormattingTag.asTags + TextParserTags(entries.associate { it.name to it.type }) + } + } +} + +fun getRawImageSizeStyleValue(width: Int?, height: Int?) = (width?.let { "width: ${it * 0.25}px;" } ?: "") + (height?.let { "height: ${it * 0.25}px;" } ?: "") +fun getRawImageSizeAttributes(width: Int?, height: Int?) = " style=\"${getRawImageSizeStyleValue(width, height)}\"" diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt index bdc6af8..4f15987 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt @@ -165,9 +165,9 @@ enum class TextParserFormattingTag(val type: TextParserTagType) { val alignments = setOf("left", "center", "right", "justify") val alignment = tagParam?.takeIf { it in alignments } val styleAttr = alignment?.let { " style=\"text-align: $it\"" } ?: "" - "" + "" }, - { _ -> "" }, + { _ -> "

" }, ) ), ASIDE( diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt index e782e81..64bb487 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt @@ -169,3 +169,29 @@ fun ApplicationCall.page(pageTitle: String, navBar: List? = null, sideb } } } + +fun ApplicationCall.rawPage(pageTitle: String, ogData: OpenGraphData? = null, content: BODY.() -> Unit): HTML.() -> Unit { + return { + lang = "en" + + head { + meta(charset = "utf-8") + meta(name = "viewport", content = "width=device-width, initial-scale=1.0") + + meta(name = "theme-color", content = "#FFCC33") + + ogData?.let { data -> + renderOgData(pageTitle, data) + } + + link(rel = "icon", type = "image/svg+xml", href = "/static/images/icon.png") + + title { + +pageTitle + } + } + body { + content() + } + } +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_error.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/views_error.kt index 49cde50..4760549 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/views_error.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/views_error.kt @@ -5,55 +5,58 @@ import io.ktor.server.application.* import io.ktor.server.request.* import kotlinx.html.* -suspend fun ApplicationCall.error400(): HTML.() -> Unit = page("400 Bad Request", standardNavBar()) { - section { - h1 { +"400 Bad Request" } - p { +"The request your browser sent was improperly formatted." } - } +suspend fun ApplicationCall.errorPage(title: String, body: FlowContent.() -> Unit): HTML.() -> Unit { + return if (request.path().endsWith(".raw")) + rawPage(title) { + body() + } + else + page(title, standardNavBar()) { + section { + body() + } + } } -suspend fun ApplicationCall.error403(): HTML.() -> Unit = page("403 Forbidden", standardNavBar()) { - section { - h1 { +"403 Forbidden" } - p { +"You are not allowed to do that." } - } +suspend fun ApplicationCall.error400(): HTML.() -> Unit = errorPage("400 Bad Request") { + h1 { +"400 Bad Request" } + p { +"The request your browser sent was improperly formatted." } } -suspend fun ApplicationCall.error403PageExpired(formData: Parameters): HTML.() -> Unit = page("Page Expired", standardNavBar()) { - section { - h1 { +"Page Expired" } - formData["comment"]?.let { commentData -> - p { +"The comment you tried to submit had been preserved here:" } - textArea { - readonly = true - +commentData - } +suspend fun ApplicationCall.error403(): HTML.() -> Unit = errorPage("403 Forbidden") { + h1 { +"403 Forbidden" } + p { +"You are not allowed to do that." } +} + +suspend fun ApplicationCall.error403PageExpired(formData: Parameters): HTML.() -> Unit = errorPage("Page Expired") { + h1 { +"Page Expired" } + formData["comment"]?.let { commentData -> + p { +"The comment you tried to submit had been preserved here:" } + textArea { + readonly = true + +commentData } - p { - +"The page you were on has expired." - request.header(HttpHeaders.Referrer)?.let { referrer -> - +" You can " - a(href = referrer) { +"return to the previous page" } - +" and retry your action." - } + } + p { + +"The page you were on has expired." + request.header(HttpHeaders.Referrer)?.let { referrer -> + +" You can " + a(href = referrer) { +"return to the previous page" } + +" and retry your action." } } } -suspend fun ApplicationCall.error404(): HTML.() -> Unit = page("404 Not Found", standardNavBar()) { - section { - h1 { +"404 Not Found" } - p { - +"Unfortunately, we could not find what you were looking for. Would you like to " - a(href = "/") { +"return to the index page" } - +"?" - } +suspend fun ApplicationCall.error404(): HTML.() -> Unit = errorPage("404 Not Found") { + h1 { +"404 Not Found" } + p { + +"Unfortunately, we could not find what you were looking for. Would you like to " + a(href = "/") { +"return to the index page" } + +"?" } } -suspend fun ApplicationCall.error500(): HTML.() -> Unit = page("500 Internal Error", standardNavBar()) { - section { - h1 { +"500 Internal Error" } - p { +"The servers made a bit of a mistake. Please be patient while we clean up our mess." } - } +suspend fun ApplicationCall.error500(): HTML.() -> Unit = errorPage("500 Internal Error") { + h1 { +"500 Internal Error" } + p { +"The servers made a bit of a mistake. Please be patient while we clean up our mess." } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt index b6570fc..ea5fe36 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt @@ -39,11 +39,87 @@ suspend fun ApplicationCall.loreIntroPage(): HTML.() -> Unit { } } +private fun FlowContent.breadCrumbs(links: List>) = p { + var isNext = false + for ((url, text) in links) { + if (isNext) { + +Entities.nbsp + +Entities.gt + +Entities.nbsp + } else isNext = true + + a(href = url) { +text } + } +} + +fun ApplicationCall.loreRawArticlePage(rawPagePath: String): HTML.() -> Unit { + val articleDir = File(Configuration.CurrentConfiguration.articleDir) + + val pagePath = rawPagePath.removeSuffix(".raw") + val pageFile = if (pagePath.isEmpty()) articleDir else articleDir.combineSafe(pagePath) + val pageNode = pageFile.toArticleNode() + + val parentPaths = if (pagePath.isEmpty()) + emptyList() + else { + val pathParts = pagePath.split('/').dropLast(1) + listOf("/lore.raw" to "Table of Contents") + pathParts.mapIndexed { i, part -> + pathParts.take(i + 1).joinToString(separator = "/", prefix = "/lore/", postfix = ".raw") to part.toFriendlyPageTitle() + } + } + + val isValid = pageFile.exists() && pageFile.isViewable + + if (isValid) { + if (pageFile.isDirectory) { + val title = pagePath.takeIf { it.isNotEmpty() }?.toFriendlyIndexTitle() ?: "Table of Contents" + + return rawPage(title) { + breadCrumbs(parentPaths) + h1 { +title } + ul { + pageNode.subNodes.renderInto(this, pagePath.takeIf { it.isNotEmpty() }, ".raw") + } + } + } + + if (pageFile.isFile) { + val pageTemplate = pageFile.readText() + val pageMarkup = PreParser.preparse(pagePath, pageTemplate) + val pageHtml = TextParserState.parseText(pageMarkup, TextParserRawPageTag.asTags, Unit) + + val pageToC = TableOfContentsBuilder() + TextParserState.parseText(pageMarkup, TextParserToCBuilderTag.asTags, pageToC) + + return rawPage(pageToC.toPageTitle(), pageToC.toOpenGraph()) { + breadCrumbs(parentPaths) + unsafe { raw(pageHtml) } + } + } + } + + val title = pagePath.substringAfterLast('/').toFriendlyPageTitle() + + return rawPage(title) { + breadCrumbs(parentPaths) + h1 { +title } + p { + +"This factbook does not exist. Would you like to " + a(href = "/lore.raw") { +"return to the table of contents" } + +"?" + } + } +} + suspend fun ApplicationCall.loreArticlePage(): HTML.() -> Unit { val totalsData = processGuestbook() val pagePathParts = parameters.getAll("path")!! val pagePath = pagePathParts.joinToString("/") + + if (pagePath.endsWith(".raw")) + return loreRawArticlePage(pagePath) + val pageFile = File(Configuration.CurrentConfiguration.articleDir).combineSafe(pagePath) val pageNode = pageFile.toArticleNode() diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_quote.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/views_quote.kt index f7baa6c..1602f40 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/views_quote.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/views_quote.kt @@ -78,3 +78,26 @@ suspend fun Quote.toHtml(title: String): HTML.() -> Unit { } } } + +context(ApplicationCall) +fun Quote.toRawHtml(title: String): HTML.() -> Unit { + return rawPage(title) { + a { id = "page-top" } + h1 { +title } + blockQuote { + +quote + } + p { + style = "align:right" + unsafe { raw("―") } + +Entities.nbsp + a(href = fullLink) { +author } + } + p { + style = "align:center" + a(href = fullLink) { + img(src = fullPortrait, alt = "Image of $author") + } + } + } +} -- 2.25.1