Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions webview/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ kotlin {
implementation(libs.kotlin.serialization.json)
}

commonTest.dependencies {
implementation(libs.kotlin.test)
}

androidMain.dependencies {
api(libs.android.activity.compose)
api(libs.android.webkit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import com.multiplatform.webview.web.IWebView
import com.multiplatform.webview.web.WebViewNavigator
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

/**
* Created By Kevin Zou On 2023/10/31
Expand Down Expand Up @@ -39,9 +41,35 @@ open class WebViewJsBridge(
private fun onCallback(
data: String,
callbackId: Int,
) = webView?.evaluateJavaScript("window.$jsBridgeName.onCallback($callbackId, '$data')")
) = webView?.evaluateJavaScript(callbackScript(jsBridgeName, callbackId, data))
}

/**
* The script that hands [data] to the web side's pending callback [callbackId].
*
* [data] is spliced in as a JavaScript string literal rather than pasted between quotes, so
* quotes, backslashes and line breaks in the payload reach the callback unchanged instead of
* breaking the script.
*/
internal fun callbackScript(
jsBridgeName: String,
callbackId: Int,
data: String,
): String = "window.$jsBridgeName.onCallback($callbackId, ${data.toJsStringLiteral()})"

/**
* Renders this string as a JavaScript string literal.
*
* JSON string escaping is a subset of JavaScript's, so the JSON encoding is a valid
* double-quoted literal. U+2028 and U+2029 are escaped as well: JSON allows them raw, but
* JavaScript treated them as line terminators before ES2019.
*/
internal fun String.toJsStringLiteral(): String =
Json
.encodeToString(this)
.replace("\u2028", "\\u2028")
.replace("\u2029", "\\u2029")

/**
* Create a [WebViewJsBridge] that is remembered across Compositions.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.multiplatform.webview.jsbridge

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse

class WebViewJsBridgeTest {
@Test
fun plainTextIsQuoted() {
assertEquals("\"hello\"", "hello".toJsStringLiteral())
}

@Test
fun quotesAndBackslashesAreEscaped() {
assertEquals(
"\"it's \\\"quoted\\\" and \\\\ escaped\"",
"it's \"quoted\" and \\ escaped".toJsStringLiteral(),
)
}

@Test
fun lineTerminatorsAreEscaped() {
val literal = "line 1\nline 2\r\n\u2028\u2029".toJsStringLiteral()
assertEquals("\"line 1\\nline 2\\r\\n\\u2028\\u2029\"", literal)
assertFalse(literal.any { it == '\n' || it == '\r' || it == '\u2028' || it == '\u2029' })
}

@Test
fun jsonPayloadKeepsItsOwnEscapes() {
// #347: a JSON payload whose string field holds a newline. The encoder wrote that
// newline as the two characters `\n`; the literal must carry both so JSON.parse on
// the web side sees an escape sequence rather than a raw control character.
val payload = """{"text":"line 1\nline 2"}"""
assertEquals("\"{\\\"text\\\":\\\"line 1\\\\nline 2\\\"}\"", payload.toJsStringLiteral())
}

@Test
fun callbackScriptSplicesTheLiteral() {
assertEquals(
"window.kmpJsBridge.onCallback(7, \"it's\\ndone\")",
callbackScript("kmpJsBridge", 7, "it's\ndone"),
)
}
}