--- /dev/null
+package info.mechyrdia.data
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.response.*
+import kotlinx.html.Tag
+import kotlinx.html.TagConsumer
+import kotlinx.html.consumers.DelayedConsumer
+import kotlinx.html.consumers.FinalizeConsumer
+import kotlinx.html.consumers.TraceConsumer
+import kotlinx.html.dom.HTMLDOMBuilder
+import kotlinx.html.impl.DelegatingMap
+import kotlinx.html.org.w3c.dom.events.Event
+import kotlinx.html.stream.HTMLStreamBuilder
+import kotlinx.html.stream.appendHTML
+import kotlinx.html.stream.createHTML
+import kotlinx.html.visit
+import kotlinx.html.visitAndFinalize
+import org.w3c.dom.Document
+import kotlin.collections.component1
+import kotlin.collections.component2
+import kotlin.collections.contains
+import kotlin.collections.set
+
+@DslMarker
+annotation class XmlTagMarker
+
+fun createXml(prettyPrint: Boolean = true): XmlTagConsumer<String> =
+ createHTML(prettyPrint, xhtmlCompatible = true).xml()
+
+fun <O : Appendable> O.appendXml(prettyPrint: Boolean = true): XmlTagConsumer<O> =
+ appendHTML(prettyPrint, xhtmlCompatible = true).xml()
+
+@Suppress("UNCHECKED_CAST")
+fun <T, C : TagConsumer<T>> C.xml(): XmlTagConsumer<T> = if (this is XmlTagConsumer<*>)
+ this as XmlTagConsumer<T>
+else
+ XmlTagConsumerImpl(this)
+
+interface XmlTagConsumer<out R> : TagConsumer<R> {
+ fun onTagDeclaration(version: String, standalone: Boolean?)
+ fun onTagDeclaration(standalone: Boolean?) = onTagDeclaration("1.0", standalone)
+
+ fun onTagNamespace(prefix: String?, namespace: String)
+
+ override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
+ tagEventsNotSupported()
+ }
+
+ fun tagEventsNotSupported(): Nothing {
+ throw UnsupportedOperationException("Events are not supported in XML")
+ }
+}
+
+private fun interface XmlDeclarationConsumer {
+ fun consumeDeclaration(version: String, standalone: Boolean?)
+}
+
+private val Appendable.declarationConsumer: XmlDeclarationConsumer
+ get() = XmlDeclarationConsumer { version, standalone ->
+ append("<?xml version=\"")
+ append(version)
+ append("\" encoding=\"UTF-8\"")
+ if (standalone != null) {
+ append(" standalone=\"")
+ if (standalone)
+ append("yes")
+ else
+ append("no")
+ append("\"")
+ }
+ append("?>")
+ appendLine()
+ }
+
+private val Document.declarationConsumer: XmlDeclarationConsumer
+ get() = XmlDeclarationConsumer { version, standalone ->
+ xmlVersion = version
+ xmlStandalone = standalone == true
+ }
+
+private fun TagConsumer<*>.getDeclarationConsumer(): XmlDeclarationConsumer =
+ when (this) {
+ is HTMLStreamBuilder<*> -> out.declarationConsumer
+ is HTMLDOMBuilder -> document.declarationConsumer
+ is DelayedConsumer<*> -> downstream.getDeclarationConsumer()
+ is FinalizeConsumer<*, *> -> downstream.getDeclarationConsumer()
+ is TraceConsumer<*> -> downstream.getDeclarationConsumer()
+ is XmlTagConsumerImpl<*> -> downstream.getDeclarationConsumer()
+ else -> throw IllegalArgumentException("Unsupported TagConsumer subtype ${this::class.qualifiedName}")
+ }
+
+private data class XmlTagImpl(val tag: Tag, val prefix: String?) : Tag by tag {
+ override val namespace: String?
+ get() = null
+
+ override val tagName: String
+ get() = prefix?.let { "$it:" }.orEmpty() + tag.tagName
+}
+
+private class XmlTagConsumerImpl<out R>(val downstream: TagConsumer<R>) : XmlTagConsumer<R>, TagConsumer<R> by downstream {
+ private var isDeclared = false
+
+ override fun onTagDeclaration(version: String, standalone: Boolean?) {
+ if (isDeclared)
+ error("Cannot write XML declaration twice")
+
+ downstream.getDeclarationConsumer().consumeDeclaration(version, standalone)
+ isDeclared = true
+ }
+
+ private var defaultNamespace: String? = null
+ private var namespaces: MutableMap<String, String>? = mutableMapOf()
+ private val namespaceLookup = mutableMapOf<String, String>()
+
+ override fun onTagNamespace(prefix: String?, namespace: String) {
+ if (prefix == null) {
+ if (namespaces == null)
+ error("Unable to add xmlns attribute after document has already started")
+ defaultNamespace = namespace
+ } else
+ (namespaces
+ ?: error("Unable to add xmlns attribute after document has already started"))[prefix] = namespace
+ }
+
+ override fun onTagStart(tag: Tag) {
+ namespaces?.let { namespaceList ->
+ defaultNamespace?.let { namespaceDefault ->
+ tag.attributes["xmlns"] = namespaceDefault
+ }
+
+ for ((prefix, namespace) in namespaceList) {
+ tag.attributes["xmlns:$prefix"] = namespace
+ namespaceLookup[namespace] = prefix
+ }
+
+ namespaces = null
+ }
+
+ val prefix = when (tag.namespace) {
+ null, defaultNamespace -> null
+ in namespaceLookup -> namespaceLookup[tag.namespace]
+ else -> throw IllegalArgumentException("Unrecognized namespace ${tag.namespace}")
+ }
+
+ downstream.onTagStart(XmlTagImpl(tag, prefix))
+ }
+
+ override fun onTagEnd(tag: Tag) {
+ val prefix = when (tag.namespace) {
+ null, defaultNamespace -> null
+ in namespaceLookup -> namespaceLookup[tag.namespace]
+ else -> throw IllegalArgumentException("Unrecognized namespace ${tag.namespace}")
+ }
+
+ downstream.onTagEnd(XmlTagImpl(tag, prefix))
+ }
+
+ override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
+ tagEventsNotSupported()
+ }
+}
+
+@XmlTagMarker
+fun <T, C : XmlTagConsumer<T>> C.declaration(version: String = "1.0", standalone: Boolean? = null) = apply {
+ onTagDeclaration(version, standalone)
+}
+
+@XmlTagMarker
+fun <T, C : XmlTagConsumer<T>> C.defaultXmlns(namespace: String) = apply {
+ onTagNamespace(null, namespace)
+}
+
+@XmlTagMarker
+fun <T, C : XmlTagConsumer<T>> C.prefixedXmlns(prefix: String, namespace: String) = apply {
+ onTagNamespace(prefix, namespace)
+}
+
+@XmlTagMarker
+fun <T, C : XmlTagConsumer<T>> C.prefixedXmlns(map: Map<String, String>) = apply {
+ for ((prefix, namespace) in map)
+ onTagNamespace(prefix, namespace)
+}
+
+@XmlTagMarker
+fun <T, C : XmlTagConsumer<T>> C.prefixedXmlns(map: Iterable<Pair<String, String>>) = apply {
+ for ((prefix, namespace) in map)
+ onTagNamespace(prefix, namespace)
+}
+
+@XmlTagMarker
+class XmlTag(
+ override val tagName: String,
+ override val consumer: XmlTagConsumer<*>,
+ initialAttributes: Map<String, String>,
+ override val namespace: String? = null,
+ override val inlineTag: Boolean,
+ override val emptyTag: Boolean
+) : Tag {
+ override val attributes: DelegatingMap = DelegatingMap(initialAttributes, this) { consumer }
+
+ override val attributesEntries: Collection<Map.Entry<String, String>>
+ get() = attributes.immutableEntries
+
+ operator fun String.invoke(attributes: Map<String, String> = emptyMap(), namespace: String? = null, isInline: Boolean = false, block: (XmlTag.() -> Unit)? = null) = XmlTag(this, consumer, attributes, namespace, isInline, block == null).visit(block ?: emptyBlock)
+}
+
+private val emptyBlock: XmlTag.() -> Unit = {}
+
+@XmlTagMarker
+fun <T, C : XmlTagConsumer<T>> C.root(name: String, attributes: Map<String, String> = emptyMap(), namespace: String? = null, block: (XmlTag.() -> Unit)? = null) = XmlTag(name, this, attributes, namespace, false, block == null).visitAndFinalize(this, block ?: emptyBlock)
+
+suspend fun ApplicationCall.respondXml(status: HttpStatusCode? = null, contentType: ContentType = ContentType.Text.Xml, prettyPrint: Boolean = true, block: XmlTagConsumer<String>.() -> String) {
+ respondText(createXml(prettyPrint).block(), contentType.withCharsetIfNeeded(Charsets.UTF_8), status)
+}
package info.mechyrdia.lore
import info.mechyrdia.JsonFileCodec
-import info.mechyrdia.data.FileStorage
-import info.mechyrdia.data.StoragePath
+import info.mechyrdia.data.*
import info.mechyrdia.route.KeyedEnumSerializer
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.response.*
-import io.ktor.util.*
import kotlinx.html.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
},
XML("xml") {
override suspend fun ApplicationCall.respondQuote(quote: Quote) {
- respondText(quote.toXml(), contentType = ContentType.Application.Xml)
+ respondXml { quote(quote) }
}
},
;
const val RANDOM_QUOTE_HTML_TITLE = "Random Quote"
-fun Quote.toXml(standalone: Boolean = true): String {
- return buildString {
- if (standalone)
- appendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
- appendLine("<quote>")
- append("<text>").append(quote.escapeHTML()).appendLine("</text>")
- append("<author>").append(author.escapeHTML()).appendLine("</author>")
- append("<portrait href=\"").append(fullPortrait.escapeHTML()).appendLine("\" />")
- append("<link href=\"").append(fullLink.escapeHTML()).appendLine("\" />")
- appendLine("</quote>")
+fun <T, C : XmlTagConsumer<T>> C.quote(quote: Quote) = declaration(standalone = true)
+ .root("quote") {
+ "text" { +quote.quote }
+ "author" { +quote.author }
+ "portrait"(attributes = mapOf("href" to quote.fullPortrait))
+ "link"(attributes = mapOf("href" to quote.fullLink))
}
-}
fun Quote.toJson(): String {
return buildJsonObject {
package info.mechyrdia.lore
-import info.mechyrdia.data.FileStorage
-import info.mechyrdia.data.StoragePath
-
-private const val AVERAGE_FACTBOOK_INTRO_CHANGEFREQ = "daily"
-private const val AVERAGE_FACTBOOK_PAGE_CHANGEFREQ = "hourly"
-private const val FACTBOOK_INTRO_PRIORITY = "0.7"
-private const val FACTBOOK_PAGE_PRIORITY = "0.8"
-
-private suspend fun Appendable.renderLoreSitemap() {
- for (page in allPages()) {
- if (page.path.isViewable) {
- val lastModified = page.stat.updated
-
- appendLine("\t<url>")
- appendLine("\t\t<loc>https://mechyrdia.info/lore/${page.path}</loc>")
- appendLine("\t\t<lastmod>$lastModified</lastmod>")
- appendLine("\t\t<changefreq>$AVERAGE_FACTBOOK_PAGE_CHANGEFREQ</changefreq>")
- appendLine("\t\t<priority>$FACTBOOK_PAGE_PRIORITY</priority>")
- appendLine("\t</url>")
- }
- }
+import info.mechyrdia.data.*
+import java.time.Instant
+import java.time.format.DateTimeFormatter
+
+private val AVERAGE_FACTBOOK_INTRO_CHANGEFREQ = SitemapChangeFrequency.DAILY
+private val AVERAGE_FACTBOOK_PAGE_CHANGEFREQ = SitemapChangeFrequency.HOURLY
+private const val FACTBOOK_INTRO_PRIORITY = 0.5
+private const val FACTBOOK_PAGE_PRIORITY = 0.75
+
+enum class SitemapChangeFrequency {
+ NEVER,
+ YEARLY,
+ MONTHLY,
+ WEEKLY,
+ DAILY,
+ HOURLY,
+ ALWAYS;
}
-private suspend fun Appendable.renderIntroSitemap() {
- val introFile = FileStorage.instance.statFile(StoragePath.Root / "intro.html") ?: return
- val introMetaFile = FileStorage.instance.statFile(StoragePath.Root / "introMeta.json") ?: return
+val SitemapChangeFrequency.xmlValue: String
+ get() = name.lowercase()
+
+val Instant.xmlValue: String
+ get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(this)
+
+val Double.xmlValue: String
+ get() = "%f".format(this)
+
+data class SitemapEntry(
+ val loc: String,
+ val lastModified: Instant,
+ val changeFrequency: SitemapChangeFrequency,
+ val priority: Double,
+)
+
+context(XmlTag)
+operator fun SitemapEntry.unaryPlus() = "url" {
+ "loc" { +loc }
+ "lastmod" { +lastModified.xmlValue }
+ "changefreq" { +changeFrequency.xmlValue }
+ "priority" { +priority.xmlValue }
+}
+
+private suspend fun buildIntroSitemap(): SitemapEntry? {
+ val introFile = FileStorage.instance.statFile(StoragePath.Root / "intro.html") ?: return null
+ val introMetaFile = FileStorage.instance.statFile(StoragePath.Root / "introMeta.json") ?: return null
val introLastModified = maxOf(introFile.updated, introMetaFile.updated)
- appendLine("\t<url>")
- appendLine("\t\t<loc>https://mechyrdia.info/</loc>")
- appendLine("\t\t<lastmod>$introLastModified</lastmod>")
- appendLine("\t\t<changefreq>$AVERAGE_FACTBOOK_INTRO_CHANGEFREQ</changefreq>")
- appendLine("\t\t<priority>$FACTBOOK_INTRO_PRIORITY</priority>")
- appendLine("\t</url>")
+ return SitemapEntry(
+ loc = "https://mechyrdia.info/",
+ lastModified = introLastModified,
+ changeFrequency = AVERAGE_FACTBOOK_INTRO_CHANGEFREQ,
+ priority = FACTBOOK_INTRO_PRIORITY
+ )
}
-suspend fun Appendable.generateSitemap() {
- appendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
- appendLine("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">")
- renderIntroSitemap()
- renderLoreSitemap()
- appendLine("</urlset>")
+private suspend fun buildLoreSitemap(): List<SitemapEntry> {
+ return allPages().mapNotNull { page ->
+ if (!page.path.isViewable) null
+ else SitemapEntry(
+ loc = "https://mechyrdia.info/lore/${page.path}",
+ lastModified = page.stat.updated,
+ changeFrequency = AVERAGE_FACTBOOK_PAGE_CHANGEFREQ,
+ priority = FACTBOOK_PAGE_PRIORITY
+ )
+ }
}
+
+suspend fun buildSitemap() = listOfNotNull(buildIntroSitemap()) + buildLoreSitemap()
+
+fun <T, C : XmlTagConsumer<T>> C.sitemap(entries: List<SitemapEntry>) = declaration()
+ .defaultXmlns("http://www.sitemaps.org/schemas/sitemap/0.9")
+ .root("urlset") {
+ for (entry in entries)
+ +entry
+ }
import info.mechyrdia.data.*
import io.ktor.http.*
import io.ktor.server.application.*
-import io.ktor.util.*
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
+suspend fun ApplicationCall.respondRss(rss: RssChannel) {
+ respondXml(contentType = ContentType.Application.Rss) { rss(rss) }
+}
+
data class StoragePathWithStat(val path: StoragePath, val stat: StoredFileStats)
private suspend fun ArticleNode.getPages(base: StoragePath): List<StoragePathWithStat> {
}
}
-suspend fun Appendable.generateRecentPageEdits() {
+suspend fun generateRecentPageEdits(): RssChannel {
val pages = allPages().sortedByDescending { it.stat.updated }
val mostRecentChange = pages.firstOrNull()?.stat?.updated
- RssChannel(
+ return RssChannel(
title = "Recently Edited Factbooks | The Hour of Decision",
link = "https://mechyrdia.info",
description = "An RSS feed containing all factbooks in The Hour of Decision, in order of most recently edited.",
}
}.awaitAll().filterNotNull()
}
- ).toXml(this)
+ )
}
-suspend fun ApplicationCall.recentCommentsRssFeedGenerator(limit: Int): Appendable.() -> Unit {
+suspend fun ApplicationCall.recentCommentsRssFeedGenerator(limit: Int): RssChannel {
val currNation = currentNation()
val validLimits = 1..100
- if (limit !in validLimits)
+ if (limit !in validLimits) {
+ response.status(HttpStatusCode.BadRequest)
+
return RssChannel(
title = "Recent Comments - Error | The Hour of Decision",
link = "https://mechyrdia.info/comment/recent",
pubDate = null,
lastBuildDate = Instant.now(),
ttl = 120,
- )::toXml
+ )
+ }
val comments = CommentRenderData(
Comment.Table
)
)
}
- )::toXml
+ )
}
data class RssCategory(
val category: String,
val domain: String? = null
-) {
- fun toXml(appendable: Appendable) {
- appendable.append("<category")
- if (domain == null)
- appendable.append(">")
- else
- appendable.append(" domain=\"").append(domain.escapeHTML()).append("\">")
- appendable.append(category.escapeHTML()).appendLine("</category>")
- }
+)
+
+context(XmlTag)
+operator fun RssCategory.unaryPlus() = "category" {
+ if (domain != null) attributes["domain"] = domain
+ +category
}
data class RssChannelImage(
val url: String,
val title: String,
val link: String,
-) {
- fun toXml(appendable: Appendable) {
- appendable.appendLine("<image>")
- appendable.append("<url>").append(url.escapeHTML()).appendLine("</url>")
- appendable.append("<title>").append(title.escapeHTML()).appendLine("</title>")
- appendable.append("<link>").append(link.escapeHTML()).appendLine("</link>")
- appendable.appendLine("</image>")
- }
+)
+
+context(XmlTag)
+operator fun RssChannelImage.unaryPlus() = "image" {
+ "url" { +url }
+ "title" { +title }
+ "link" { +link }
}
const val DEFAULT_RSS_COPYRIGHT = "Copyright 2022 Lanius Trolling"
const val DEFAULT_RSS_EMAIL = "lanius@laniustrolling.dev (Lanius Trolling)"
val RssDateFormat: DateTimeFormatter = DateTimeFormatter.RFC_1123_DATE_TIME
-fun Instant.toXml() = RssDateFormat.format(atOffset(ZoneOffset.UTC)).escapeHTML()
+val Instant.rssValue: String
+ get() = RssDateFormat.format(atOffset(ZoneOffset.UTC))
data class RssChannel(
val title: String,
val image: RssChannelImage? = null,
val categories: List<RssCategory> = emptyList(),
val items: List<RssItem> = emptyList(),
-) {
- fun toXml(appendable: Appendable) {
- appendable.appendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
- appendable.appendLine("<rss version=\"2.0\">")
- appendable.appendLine("<channel>")
-
- appendable.append("<title>").append(title.escapeHTML()).appendLine("</title>")
- appendable.append("<link>").append(link.escapeHTML()).appendLine("</link>")
- appendable.append("<description>").append(description.escapeHTML()).appendLine("</description>")
-
- if (language != null)
- appendable.append("<language>").append(language.escapeHTML()).appendLine("</language>")
- if (copyright != null)
- appendable.append("<copyright>").append(copyright.escapeHTML()).appendLine("</copyright>")
- if (managingEditor != null)
- appendable.append("<managingEditor>").append(managingEditor.escapeHTML()).appendLine("</managingEditor>")
- if (webMaster != null)
- appendable.append("<webMaster>").append(webMaster.escapeHTML()).appendLine("</webMaster>")
- if (pubDate != null)
- appendable.append("<pubDate>").append(pubDate.toXml()).appendLine("</pubDate>")
- if (lastBuildDate != null)
- appendable.append("<lastBuildDate>").append(lastBuildDate.toXml()).appendLine("</lastBuildDate>")
- if (ttl != null)
- appendable.append("<ttl>").append(ttl.toString()).appendLine("</ttl>")
-
- image?.toXml(appendable)
-
- for (category in categories)
- category.toXml(appendable)
- for (item in items)
- item.toXml(appendable)
-
- appendable.appendLine("</channel>")
- appendable.appendLine("</rss>")
+)
+
+fun <T, C : XmlTagConsumer<T>> C.rss(rssChannel: RssChannel) = declaration()
+ .root("rss") {
+ attributes["version"] = "2.0"
+ "channel" {
+ "title" { +rssChannel.title }
+ "link" { +rssChannel.link }
+ "description" { +rssChannel.description }
+
+ if (rssChannel.language != null) "language" { +rssChannel.language }
+ if (rssChannel.copyright != null) "copyright" { +rssChannel.copyright }
+ if (rssChannel.managingEditor != null) "managingEditor" { +rssChannel.managingEditor }
+ if (rssChannel.webMaster != null) "webMaster" { +rssChannel.webMaster }
+ if (rssChannel.pubDate != null) "pubDate" { +rssChannel.pubDate.rssValue }
+ if (rssChannel.lastBuildDate != null) "lastBuildDate" { +rssChannel.lastBuildDate.rssValue }
+ if (rssChannel.ttl != null) "ttl" { +rssChannel.ttl.toString() }
+
+ if (rssChannel.image != null) +rssChannel.image
+
+ for (category in rssChannel.categories)
+ +category
+ for (item in rssChannel.items)
+ +item
+ }
}
-}
data class RssItemEnclosure(
val url: String,
val length: Long,
val type: String,
-) {
- fun toXml(appendable: Appendable) {
- appendable.append("<enclosure ")
- .append("url=\"").append(url.escapeHTML()).append("\" ")
- .append("length=\"").append(length.toString()).append("\" ")
- .append("type=\"").append(type.escapeHTML()).append("\" ")
- .appendLine("/>")
- }
-}
+)
+
+context(XmlTag)
+operator fun RssItemEnclosure.unaryPlus() = "enclosure"(
+ attributes = mapOf(
+ "url" to url,
+ "length" to length.toString(),
+ "type" to type,
+ )
+)
data class RssItem(
val title: String? = null,
init {
require(title != null || description != null) { "Either title or description must be provided, got null for both" }
}
+}
+
+context(XmlTag)
+operator fun RssItem.unaryPlus() = "item" {
+ if (title != null) "title" { +title }
+ if (description != null) "description" { +description }
+ if (link != null) "link" { +link }
+ if (author != null) "author" { +author }
+ if (comments != null) "comments" { +comments }
+ if (enclosure != null) +enclosure
+ if (pubDate != null) "pubDate" { +pubDate.rssValue }
- fun toXml(appendable: Appendable) {
- appendable.appendLine("<item>")
-
- if (title != null)
- appendable.append("<title>").append(title.escapeHTML()).appendLine("</title>")
- if (description != null)
- appendable.append("<description>").append(description.escapeHTML()).appendLine("</description>")
- if (link != null)
- appendable.append("<link>").append(link.escapeHTML()).appendLine("</link>")
- if (author != null)
- appendable.append("<author>").append(author.escapeHTML()).appendLine("</author>")
- if (comments != null)
- appendable.append("<comments>").append(comments.escapeHTML()).appendLine("</comments>")
- enclosure?.toXml(appendable)
-
- if (pubDate != null)
- appendable.append("<pubDate>").append(pubDate.toXml()).appendLine("</pubDate>")
-
- for (category in categories)
- category.toXml(appendable)
-
- appendable.appendLine("</item>")
- }
+ for (category in categories)
+ +category
}
import io.ktor.server.response.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
-import kotlinx.coroutines.withContext
val ErrorMessageAttributeKey = AttributeKey<String>("ErrorMessage")
override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall() {
with(root) { filterCall() }
- call.respondText(buildString { generateSitemap() }, ContentType.Application.Xml)
+ val sitemap = buildSitemap()
+ call.respondXml(contentType = ContentType.Application.Xml) {
+ sitemap(sitemap)
+ }
}
}
override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall() {
with(root) { filterCall() }
- call.respondText(buildString { generateRecentPageEdits() }, ContentType.Application.Rss)
+ call.respondRss(generateRecentPageEdits())
}
}
override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall() {
with(root) { filterCall() }
- call.respondText(buildString(call.recentCommentsRssFeedGenerator(limit ?: 10)), ContentType.Application.Rss)
+ call.respondRss(call.recentCommentsRssFeedGenerator(limit ?: 10))
}
}
override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall(payload: MechyrdiaSansPayload) {
with(utils) { filterCall() }
- call.respondText(withContext(Dispatchers.Default) {
- MechyrdiaSansFont.renderTextToSvg(payload.lines.joinToString(separator = "\n") { it.trim() }, payload.bold, payload.italic, payload.align)
- }, ContentType.Image.SVG)
+ val svgDoc = MechyrdiaSansFont.renderTextToSvg(payload.lines.joinToString(separator = "\n") { it.trim() }, payload.bold, payload.italic, payload.align)
+ call.respondXml(contentType = ContentType.Image.SVG) {
+ svg(svgDoc)
+ }
}
}