Skip to content

Commit

Permalink
KTOR-7584 HTMX extension for Ktor (#4553)
Browse files Browse the repository at this point in the history
  • Loading branch information
bjhham authored Feb 13, 2025
1 parent 091b1eb commit 985f31c
Show file tree
Hide file tree
Showing 26 changed files with 1,778 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
public final class io/ktor/server/htmx/HXRequestHeaders {
public static final synthetic fun box-impl (Lio/ktor/http/Headers;)Lio/ktor/server/htmx/HXRequestHeaders;
public static fun constructor-impl (Lio/ktor/http/Headers;)Lio/ktor/http/Headers;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lio/ktor/http/Headers;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lio/ktor/http/Headers;Lio/ktor/http/Headers;)Z
public static final fun getCurrentUrl-impl (Lio/ktor/http/Headers;)Lio/ktor/http/Url;
public static final fun getPrompt-impl (Lio/ktor/http/Headers;)Ljava/lang/String;
public static final fun getTargetId-impl (Lio/ktor/http/Headers;)Ljava/lang/String;
public static final fun getTriggerId-impl (Lio/ktor/http/Headers;)Ljava/lang/String;
public static final fun getTriggerName-impl (Lio/ktor/http/Headers;)Ljava/lang/String;
public fun hashCode ()I
public static fun hashCode-impl (Lio/ktor/http/Headers;)I
public static final fun isBoosted-impl (Lio/ktor/http/Headers;)Z
public static final fun isHistoryRestore-impl (Lio/ktor/http/Headers;)Z
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lio/ktor/http/Headers;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lio/ktor/http/Headers;
}

public final class io/ktor/server/htmx/HXResponseHeaders : io/ktor/util/collections/StringMap {
public fun <init> (Lio/ktor/server/response/ResponseHeaders;)V
public fun get (Ljava/lang/String;)Ljava/lang/String;
public final fun getLocation ()Ljava/lang/String;
public final fun getPushUrl ()Ljava/lang/String;
public final fun getRedirect ()Ljava/lang/String;
public final fun getRefresh ()Ljava/lang/Boolean;
public final fun getReplaceUrl ()Ljava/lang/String;
public fun remove (Ljava/lang/String;)Ljava/lang/String;
public fun set (Ljava/lang/String;Ljava/lang/String;)V
public final fun setLocation (Ljava/lang/String;)V
public final fun setPushUrl (Ljava/lang/String;)V
public final fun setRedirect (Ljava/lang/String;)V
public final fun setRefresh (Ljava/lang/Boolean;)V
}

public final class io/ktor/server/htmx/HxHeadersKt {
public static final fun getHx (Lio/ktor/server/routing/RoutingRequest;)Lio/ktor/http/Headers;
public static final fun getHx (Lio/ktor/server/routing/RoutingResponse;)Lio/ktor/server/htmx/HXResponseHeaders;
public static final fun isHtmx (Lio/ktor/server/routing/RoutingRequest;)Z
}

public final class io/ktor/server/htmx/HxRoute : io/ktor/server/routing/Route {
public static final synthetic fun box-impl (Lio/ktor/server/routing/Route;)Lio/ktor/server/htmx/HxRoute;
public fun createChild (Lio/ktor/server/routing/RouteSelector;)Lio/ktor/server/routing/Route;
public static fun createChild-impl (Lio/ktor/server/routing/Route;Lio/ktor/server/routing/RouteSelector;)Lio/ktor/server/routing/Route;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lio/ktor/server/routing/Route;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lio/ktor/server/routing/Route;Lio/ktor/server/routing/Route;)Z
public fun getAttributes ()Lio/ktor/util/Attributes;
public static fun getAttributes-impl (Lio/ktor/server/routing/Route;)Lio/ktor/util/Attributes;
public fun getEnvironment ()Lio/ktor/server/application/ApplicationEnvironment;
public static fun getEnvironment-impl (Lio/ktor/server/routing/Route;)Lio/ktor/server/application/ApplicationEnvironment;
public fun getParent ()Lio/ktor/server/routing/Route;
public static fun getParent-impl (Lio/ktor/server/routing/Route;)Lio/ktor/server/routing/Route;
public fun handle (Lkotlin/jvm/functions/Function2;)V
public static fun handle-impl (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;)V
public fun hashCode ()I
public static fun hashCode-impl (Lio/ktor/server/routing/Route;)I
public fun install (Lio/ktor/server/application/Plugin;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static fun install-impl (Lio/ktor/server/routing/Route;Lio/ktor/server/application/Plugin;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public fun plugin (Lio/ktor/server/application/Plugin;)Ljava/lang/Object;
public static fun plugin-impl (Lio/ktor/server/routing/Route;Lio/ktor/server/application/Plugin;)Ljava/lang/Object;
public static final fun target-impl (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/ktor/server/routing/Route;
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lio/ktor/server/routing/Route;)Ljava/lang/String;
public static final fun trigger-impl (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/ktor/server/routing/Route;
public final synthetic fun unbox-impl ()Lio/ktor/server/routing/Route;
}

public final class io/ktor/server/htmx/HxRoutingKt {
public static final fun getHx (Lio/ktor/server/routing/Route;)Lio/ktor/server/routing/Route;
public static final fun hx (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function1;)Lio/ktor/server/routing/Route;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Klib ABI Dump
// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true

// Library unique name: <io.ktor:ktor-server-htmx>
final class io.ktor.server.htmx/HXResponseHeaders : io.ktor.util.collections/StringMap { // io.ktor.server.htmx/HXResponseHeaders|null[0]
constructor <init>(io.ktor.server.response/ResponseHeaders) // io.ktor.server.htmx/HXResponseHeaders.<init>|<init>(io.ktor.server.response.ResponseHeaders){}[0]

final val replaceUrl // io.ktor.server.htmx/HXResponseHeaders.replaceUrl|{}replaceUrl[0]
final fun <get-replaceUrl>(): kotlin/String? // io.ktor.server.htmx/HXResponseHeaders.replaceUrl.<get-replaceUrl>|<get-replaceUrl>(){}[0]

final var location // io.ktor.server.htmx/HXResponseHeaders.location|{}location[0]
final fun <get-location>(): kotlin/String? // io.ktor.server.htmx/HXResponseHeaders.location.<get-location>|<get-location>(){}[0]
final fun <set-location>(kotlin/String?) // io.ktor.server.htmx/HXResponseHeaders.location.<set-location>|<set-location>(kotlin.String?){}[0]
final var pushUrl // io.ktor.server.htmx/HXResponseHeaders.pushUrl|{}pushUrl[0]
final fun <get-pushUrl>(): kotlin/String? // io.ktor.server.htmx/HXResponseHeaders.pushUrl.<get-pushUrl>|<get-pushUrl>(){}[0]
final fun <set-pushUrl>(kotlin/String?) // io.ktor.server.htmx/HXResponseHeaders.pushUrl.<set-pushUrl>|<set-pushUrl>(kotlin.String?){}[0]
final var redirect // io.ktor.server.htmx/HXResponseHeaders.redirect|{}redirect[0]
final fun <get-redirect>(): kotlin/String? // io.ktor.server.htmx/HXResponseHeaders.redirect.<get-redirect>|<get-redirect>(){}[0]
final fun <set-redirect>(kotlin/String?) // io.ktor.server.htmx/HXResponseHeaders.redirect.<set-redirect>|<set-redirect>(kotlin.String?){}[0]
final var refresh // io.ktor.server.htmx/HXResponseHeaders.refresh|{}refresh[0]
final fun <get-refresh>(): kotlin/Boolean? // io.ktor.server.htmx/HXResponseHeaders.refresh.<get-refresh>|<get-refresh>(){}[0]
final fun <set-refresh>(kotlin/Boolean?) // io.ktor.server.htmx/HXResponseHeaders.refresh.<set-refresh>|<set-refresh>(kotlin.Boolean?){}[0]

final fun get(kotlin/String): kotlin/String? // io.ktor.server.htmx/HXResponseHeaders.get|get(kotlin.String){}[0]
final fun remove(kotlin/String): kotlin/String? // io.ktor.server.htmx/HXResponseHeaders.remove|remove(kotlin.String){}[0]
final fun set(kotlin/String, kotlin/String) // io.ktor.server.htmx/HXResponseHeaders.set|set(kotlin.String;kotlin.String){}[0]
}

final value class io.ktor.server.htmx/HXRequestHeaders { // io.ktor.server.htmx/HXRequestHeaders|null[0]
constructor <init>(io.ktor.http/Headers) // io.ktor.server.htmx/HXRequestHeaders.<init>|<init>(io.ktor.http.Headers){}[0]

final val currentUrl // io.ktor.server.htmx/HXRequestHeaders.currentUrl|{}currentUrl[0]
final fun <get-currentUrl>(): io.ktor.http/Url? // io.ktor.server.htmx/HXRequestHeaders.currentUrl.<get-currentUrl>|<get-currentUrl>(){}[0]
final val isBoosted // io.ktor.server.htmx/HXRequestHeaders.isBoosted|{}isBoosted[0]
final fun <get-isBoosted>(): kotlin/Boolean // io.ktor.server.htmx/HXRequestHeaders.isBoosted.<get-isBoosted>|<get-isBoosted>(){}[0]
final val isHistoryRestore // io.ktor.server.htmx/HXRequestHeaders.isHistoryRestore|{}isHistoryRestore[0]
final fun <get-isHistoryRestore>(): kotlin/Boolean // io.ktor.server.htmx/HXRequestHeaders.isHistoryRestore.<get-isHistoryRestore>|<get-isHistoryRestore>(){}[0]
final val prompt // io.ktor.server.htmx/HXRequestHeaders.prompt|{}prompt[0]
final fun <get-prompt>(): kotlin/String? // io.ktor.server.htmx/HXRequestHeaders.prompt.<get-prompt>|<get-prompt>(){}[0]
final val targetId // io.ktor.server.htmx/HXRequestHeaders.targetId|{}targetId[0]
final fun <get-targetId>(): kotlin/String? // io.ktor.server.htmx/HXRequestHeaders.targetId.<get-targetId>|<get-targetId>(){}[0]
final val triggerId // io.ktor.server.htmx/HXRequestHeaders.triggerId|{}triggerId[0]
final fun <get-triggerId>(): kotlin/String? // io.ktor.server.htmx/HXRequestHeaders.triggerId.<get-triggerId>|<get-triggerId>(){}[0]
final val triggerName // io.ktor.server.htmx/HXRequestHeaders.triggerName|{}triggerName[0]
final fun <get-triggerName>(): kotlin/String? // io.ktor.server.htmx/HXRequestHeaders.triggerName.<get-triggerName>|<get-triggerName>(){}[0]

final fun equals(kotlin/Any?): kotlin/Boolean // io.ktor.server.htmx/HXRequestHeaders.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // io.ktor.server.htmx/HXRequestHeaders.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // io.ktor.server.htmx/HXRequestHeaders.toString|toString(){}[0]
}

final value class io.ktor.server.htmx/HxRoute : io.ktor.server.routing/Route { // io.ktor.server.htmx/HxRoute|null[0]
final val attributes // io.ktor.server.htmx/HxRoute.attributes|{}attributes[0]
final fun <get-attributes>(): io.ktor.util/Attributes // io.ktor.server.htmx/HxRoute.attributes.<get-attributes>|<get-attributes>(){}[0]
final val environment // io.ktor.server.htmx/HxRoute.environment|{}environment[0]
final fun <get-environment>(): io.ktor.server.application/ApplicationEnvironment // io.ktor.server.htmx/HxRoute.environment.<get-environment>|<get-environment>(){}[0]
final val parent // io.ktor.server.htmx/HxRoute.parent|{}parent[0]
final fun <get-parent>(): io.ktor.server.routing/Route? // io.ktor.server.htmx/HxRoute.parent.<get-parent>|<get-parent>(){}[0]

final fun <#A1: kotlin/Any, #B1: kotlin/Any> install(io.ktor.server.application/Plugin<io.ktor.server.application/ApplicationCallPipeline, #A1, #B1>, kotlin/Function1<#A1, kotlin/Unit>): #B1 // io.ktor.server.htmx/HxRoute.install|install(io.ktor.server.application.Plugin<io.ktor.server.application.ApplicationCallPipeline,0:0,0:1>;kotlin.Function1<0:0,kotlin.Unit>){0§<kotlin.Any>;1§<kotlin.Any>}[0]
final fun <#A1: kotlin/Any> plugin(io.ktor.server.application/Plugin<*, *, #A1>): #A1 // io.ktor.server.htmx/HxRoute.plugin|plugin(io.ktor.server.application.Plugin<*,*,0:0>){0§<kotlin.Any>}[0]
final fun createChild(io.ktor.server.routing/RouteSelector): io.ktor.server.routing/Route // io.ktor.server.htmx/HxRoute.createChild|createChild(io.ktor.server.routing.RouteSelector){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // io.ktor.server.htmx/HxRoute.equals|equals(kotlin.Any?){}[0]
final fun handle(kotlin.coroutines/SuspendFunction1<io.ktor.server.routing/RoutingContext, kotlin/Unit>) // io.ktor.server.htmx/HxRoute.handle|handle(kotlin.coroutines.SuspendFunction1<io.ktor.server.routing.RoutingContext,kotlin.Unit>){}[0]
final fun hashCode(): kotlin/Int // io.ktor.server.htmx/HxRoute.hashCode|hashCode(){}[0]
final fun target(kotlin/String, kotlin/Function1<io.ktor.server.routing/Route, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.htmx/HxRoute.target|target(kotlin.String;kotlin.Function1<io.ktor.server.routing.Route,kotlin.Unit>){}[0]
final fun toString(): kotlin/String // io.ktor.server.htmx/HxRoute.toString|toString(){}[0]
final fun trigger(kotlin/String, kotlin/Function1<io.ktor.server.routing/Route, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.htmx/HxRoute.trigger|trigger(kotlin.String;kotlin.Function1<io.ktor.server.routing.Route,kotlin.Unit>){}[0]
}

final val io.ktor.server.htmx/hx // io.ktor.server.htmx/hx|@io.ktor.server.routing.Route{}hx[0]
final fun (io.ktor.server.routing/Route).<get-hx>(): io.ktor.server.htmx/HxRoute // io.ktor.server.htmx/hx.<get-hx>|<get-hx>@io.ktor.server.routing.Route(){}[0]
final val io.ktor.server.htmx/hx // io.ktor.server.htmx/hx|@io.ktor.server.routing.RoutingRequest{}hx[0]
final fun (io.ktor.server.routing/RoutingRequest).<get-hx>(): io.ktor.server.htmx/HXRequestHeaders // io.ktor.server.htmx/hx.<get-hx>|<get-hx>@io.ktor.server.routing.RoutingRequest(){}[0]
final val io.ktor.server.htmx/hx // io.ktor.server.htmx/hx|@io.ktor.server.routing.RoutingResponse{}hx[0]
final fun (io.ktor.server.routing/RoutingResponse).<get-hx>(): io.ktor.server.htmx/HXResponseHeaders // io.ktor.server.htmx/hx.<get-hx>|<get-hx>@io.ktor.server.routing.RoutingResponse(){}[0]
final val io.ktor.server.htmx/isHtmx // io.ktor.server.htmx/isHtmx|@io.ktor.server.routing.RoutingRequest{}isHtmx[0]
final fun (io.ktor.server.routing/RoutingRequest).<get-isHtmx>(): kotlin/Boolean // io.ktor.server.htmx/isHtmx.<get-isHtmx>|<get-isHtmx>@io.ktor.server.routing.RoutingRequest(){}[0]

final fun (io.ktor.server.routing/Route).io.ktor.server.htmx/hx(kotlin/Function1<io.ktor.server.htmx/HxRoute, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.htmx/hx|[email protected](kotlin.Function1<io.ktor.server.htmx.HxRoute,kotlin.Unit>){}[0]
14 changes: 14 additions & 0 deletions ktor-server/ktor-server-plugins/ktor-server-htmx/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
kotlin.sourceSets {
commonMain {
dependencies {
api(project(":ktor-shared:ktor-htmx"))
implementation(project(":ktor-utils"))
}
}
commonTest {
dependencies {
implementation(project(":ktor-server:ktor-server-plugins:ktor-server-html-builder"))
implementation(project(":ktor-shared:ktor-htmx:ktor-htmx-html"))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.ktor.server.htmx

import io.ktor.htmx.*
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.collections.*
import io.ktor.utils.io.InternalAPI
import kotlin.jvm.JvmInline

@ExperimentalHtmxApi
public val RoutingRequest.hx: HXRequestHeaders get() = HXRequestHeaders(headers)

@ExperimentalHtmxApi
public val RoutingResponse.hx: HXResponseHeaders get() = HXResponseHeaders(headers)

public val RoutingRequest.isHtmx: Boolean get() = headers[HxRequestHeaders.Request] == "true"

@ExperimentalHtmxApi
@JvmInline
public value class HXRequestHeaders(private val headers: Headers) {

/** Indicates that the request is via an element using hx-boost */
public val isBoosted: Boolean get() = headers[HxRequestHeaders.Boosted]?.toBoolean() == true

/** "true" if the request is for history restoration after a miss in the local history cache */
public val isHistoryRestore: Boolean get() = headers[HxRequestHeaders.HistoryRestoreRequest]?.toBoolean() == true

/** The current URL of the browser */
public val currentUrl: Url? get() = headers[HxRequestHeaders.CurrentUrl]?.let { Url(it) }

/** The user response to an hx-prompt */
public val prompt: String? get() = headers[HxRequestHeaders.Prompt]

/** The id of the target element if it exists */
public val targetId: String? get() = headers[HxRequestHeaders.Target]

/** The id of the triggered element if it exists */
public val triggerId: String? get() = headers[HxRequestHeaders.Trigger]

/** The name of the triggered element if it exists */
public val triggerName: String? get() = headers[HxRequestHeaders.TriggerName]
}

@ExperimentalHtmxApi
@OptIn(InternalAPI::class)
public class HXResponseHeaders(private val headers: ResponseHeaders) : StringMap {

public var location: String? by HxResponseHeaders.Location
public var pushUrl: String? by HxResponseHeaders.PushUrl
public var redirect: String? by HxResponseHeaders.Redirect
public var refresh: Boolean? by HxResponseHeaders.Refresh.asBoolean()
public val replaceUrl: String? by HxResponseHeaders.ReplaceUrl

override fun set(key: String, value: String): Unit =
headers.append(key, value)

override fun get(key: String): String? =
headers[key]

override fun remove(key: String): String? =
throw IllegalStateException("Not implemented")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.htmx

import io.ktor.htmx.*
import io.ktor.server.routing.*
import io.ktor.utils.io.*
import kotlin.jvm.JvmInline

/**
* Property for scoping routes to HTMX (e.g., `hx.get { ... }`
*/
@ExperimentalHtmxApi
public val Route.hx: HxRoute get() = HxRoute.wrap(this)

/**
* Scope child routes to apply when `HX-Request` header is supplied.
*/
@ExperimentalHtmxApi
public fun Route.hx(configuration: HxRoute.() -> Unit): Route = with(HxRoute.wrap(this)) {
header(HxRequestHeaders.Request, "true") {
configuration()
}
}

/**
* Provides custom routes based on common HTMX headers.
*/
@ExperimentalHtmxApi
@KtorDsl
@JvmInline
public value class HxRoute internal constructor(private val route: Route) : Route by route {
internal companion object {
internal fun wrap(route: Route) =
HxRoute(route.createChild(HttpHeaderRouteSelector(HxRequestHeaders.Request, "true")))
}

/**
* Sub-routes only apply to a specific HX-Target header.
*/
public fun target(expectedTarget: String, body: Route.() -> Unit): Route {
return header(HxRequestHeaders.Target, expectedTarget, body)
}

/**
* Sub-routes only apply to a specific HX-Trigger header.
*/
public fun trigger(expectedTrigger: String, body: Route.() -> Unit): Route =
header(HxRequestHeaders.Trigger, expectedTrigger, body)
}
Loading

0 comments on commit 985f31c

Please sign in to comment.