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