
org.jetbrains.kotlinx.jupyter.api.Results.kt Maven / Gradle / Ivy
package org.jetbrains.kotlinx.jupyter.api
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlinx.jupyter.api.libraries.ColorScheme
import org.jetbrains.kotlinx.jupyter.api.outputs.IsolatedHtmlMarker
import org.jetbrains.kotlinx.jupyter.api.outputs.MetadataModifier
import org.jetbrains.kotlinx.jupyter.api.outputs.hasModifier
import org.jetbrains.kotlinx.jupyter.api.outputs.isExpandedJson
import org.jetbrains.kotlinx.jupyter.api.outputs.isIsolatedHtml
import org.jetbrains.kotlinx.jupyter.api.outputs.standardMetadataModifiers
import org.jetbrains.kotlinx.jupyter.util.EMPTY
import org.jetbrains.kotlinx.jupyter.util.escapeForIframe
import java.util.concurrent.atomic.AtomicLong
/**
* Type alias for FQNs - fully qualified names of classes
*/
typealias TypeName = String
/**
* Type alias for plain code ready for execution
*/
typealias Code = String
/**
* Object that should be rendered to [DisplayResult] if
* it is the result of code cell
*/
interface Renderable {
/**
* Render to display result
*
* @param notebook Current notebook
* @return Display result
*/
fun render(notebook: Notebook): DisplayResult
}
/**
* Wrapper for in-memory results that also tracks its corresponding mime-type.
*/
data class InMemoryResult(val mimeType: String, val result: Any?)
/**
* Display result that may be converted to JSON for `display_data`
* kernel response
*/
interface DisplayResult : Renderable {
/**
* Unique id that may be used for updating display data
*/
val id: String? get() = null
/**
* Converts display data to JSON object for `display_data` response
*
* @param additionalMetadata Additional reply metadata
* @return Display JSON
*/
fun toJson(
additionalMetadata: JsonObject = Json.EMPTY,
overrideId: String? = null,
): JsonObject
@Deprecated("Use full version instead", ReplaceWith("toJson(additionalMetadata, null)"))
fun toJson(additionalMetadata: JsonObject = Json.EMPTY): JsonObject = toJson(additionalMetadata, null)
/**
* Renders display result, generally should return `this`
*/
override fun render(notebook: Notebook): DisplayResult = this
}
/**
* Display result that holds the reference to related cell
*/
interface DisplayResultWithCell : DisplayResult {
val cell: CodeCell
}
/**
* Container that holds all notebook display results
*/
interface DisplayContainer {
fun getAll(): List
fun getById(id: String?): List
}
typealias MutableJsonObject = MutableMap
/**
* Convenience method for converting nullable [DisplayResult] to JSON
*
* @return JSON for `display_data` response
*/
@Suppress("unused")
fun DisplayResult?.toJson(): JsonObject {
if (this != null) return this.toJson(Json.EMPTY, null)
return Json.encodeToJsonElement(mapOf("data" to null, "metadata" to JsonObject(mapOf()))) as JsonObject
}
@Suppress("unused")
fun DisplayResult.withId(id: String) =
if (id == this.id) {
this
} else {
object : DisplayResult {
override fun toJson(
additionalMetadata: JsonObject,
overrideId: String?,
) = [email protected](additionalMetadata, overrideId ?: id)
override val id = id
}
}
/**
* Sets display ID to JSON.
* If ID was not set, sets it to [id] and returns it back
* If ID was set and [force] is false, just returns old ID
* If ID was set, [force] is true and [id] is `null`, just returns old ID
* If ID was set, [force] is true and [id] is not `null`, sets ID to [id] and returns it back
*/
fun MutableJsonObject.setDisplayId(
id: String? = null,
force: Boolean = false,
): String? {
val transient = get("transient")?.let { Json.decodeFromJsonElement(it) }
val oldId = (transient?.get("display_id") as? JsonPrimitive)?.content
if (id == null) return oldId
if (oldId != null && !force) return oldId
val newTransient = transient ?: mutableMapOf()
newTransient["display_id"] = JsonPrimitive(id)
this["transient"] = Json.encodeToJsonElement(newTransient)
return id
}
/**
* Check if the JSON object contains a `display_id` entry.
*/
fun JsonObject.containsDisplayId(id: String): Boolean {
val transient: JsonObject? = get("transient") as? JsonObject
return (transient?.get("display_id") as? JsonPrimitive)?.content == id
}
/**
* Wrapper for [DisplayResult]s that contain in memory results.
* This is only applicable to the embedded server.
*
* @param inMemoryOutput the in-memory result + its mime-type that tells the client how to render it.
* @param fallbackResult fallback output the client can use as a placeholder for the in-memory result, if it is
* no longer available. Like when saving the notebook to disk.
*/
class InMemoryMimeTypedResult(
val inMemoryOutput: InMemoryResult,
val fallbackResult: Map,
) : DisplayResult {
override fun toJson(
additionalMetadata: JsonObject,
overrideId: String?,
): JsonObject {
throw UnsupportedOperationException("This method is not supported for in-memory values")
}
}
/**
* Convenient implementation of [DisplayResult],
* supposed to be used almost always.
*/
class MimeTypedResult(
private val mimeData: Map,
isolatedHtml: Boolean = false,
id: String? = null,
) : Map by mimeData,
MimeTypedResultEx(Json.encodeToJsonElement(mimeData), id, standardMetadataModifiers(isolatedHtml = isolatedHtml))
open class MimeTypedResultEx(
private val mimeData: JsonElement,
override val id: String? = null,
metadataModifiers: List = emptyList(),
) : DisplayResult {
private val metadataModifiers: MutableSet = mutableSetOf()
fun addMetadataModifier(metadataModifier: MetadataModifier) {
metadataModifiers.add(metadataModifier)
}
fun removeMetadataModifier(metadataModifier: MetadataModifier) {
metadataModifiers.remove(metadataModifier)
}
fun hasMetadataModifiers(predicate: (MetadataModifier) -> Boolean): Boolean = metadataModifiers.any(predicate)
init {
for (metadataModifier in metadataModifiers) {
addMetadataModifier(metadataModifier)
}
}
@Deprecated(
"Use primary constructor instead",
replaceWith =
ReplaceWith(
"MimeTypedResultEx(mimeData, id, standardMetadataModifiers(isolatedHtml = isolatedHtml))",
"org.jetbrains.kotlinx.jupyter.api.outputs.standardMetadataModifiers",
),
)
constructor(
mimeData: JsonElement,
isolatedHtml: Boolean = false,
id: String? = null,
) : this(
mimeData,
id,
standardMetadataModifiers(isolatedHtml = isolatedHtml),
)
@Deprecated(
"Use metadata modifiers instead",
replaceWith = ReplaceWith("isIsolated", "org.jetbrains.kotlinx.jupyter.api.outputs.isIsolated"),
)
@Suppress("unused") // Left for binary compatibility
var isolatedHtml by hasModifier(IsolatedHtmlMarker)
override fun toJson(
additionalMetadata: JsonObject,
overrideId: String?,
): JsonObject {
val metadata =
buildJsonObject {
for (modifier in metadataModifiers) {
with(modifier) { modifyMetadata() }
}
additionalMetadata.forEach { key, value ->
put(key, value)
}
}
val result: MutableJsonObject =
hashMapOf(
"data" to mimeData,
"metadata" to metadata,
)
result.setDisplayId(overrideId ?: id)
return Json.encodeToJsonElement(result) as JsonObject
}
override fun toString(): String {
return jsonPrettyPrinter.encodeToString(toJson(Json.EMPTY, null))
}
override fun equals(other: Any?): Boolean {
if (other !is MimeTypedResultEx) return false
return toJson(Json.EMPTY, null) == other.toJson(Json.EMPTY, null)
}
override fun hashCode(): Int {
var result = mimeData.hashCode()
result = 31 * result + isIsolatedHtml.hashCode()
result = 31 * result + isExpandedJson.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
return result
}
}
// Convenience methods for displaying results
@Suppress("unused", "FunctionName")
fun MIME(vararg mimeToData: Pair): MimeTypedResult = mimeResult(*mimeToData)
@Suppress("unused", "FunctionName")
fun HTML(
text: String,
isolated: Boolean = false,
) = htmlResult(text, isolated)
private val jsonPrettyPrinter = Json { prettyPrint = true }
@Suppress("FunctionName")
fun JSON(
json: JsonElement,
isolated: Boolean = false,
expanded: Boolean = true,
) = MimeTypedResultEx(
buildJsonObject {
val encodedJson = jsonPrettyPrinter.encodeToString(json)
put(MimeTypes.JSON, json)
put(MimeTypes.PLAIN_TEXT, JsonPrimitive(encodedJson))
val backticks = "`".repeat(markdownCodeBlockBackticksCount(encodedJson))
put(MimeTypes.MARKDOWN, JsonPrimitive("${backticks}json\n$encodedJson\n$backticks"))
},
null,
standardMetadataModifiers(
isolatedHtml = isolated,
expandedJson = expanded,
),
)
private val markdownBackticksRegex = Regex("`{3,}")
/** Return minimum number of backticks that are required to wrap [code] in Markdown code block without escaping it. */
private fun markdownCodeBlockBackticksCount(code: String): Int {
return markdownBackticksRegex.findAll(code).maxOfOrNull { it.value.length + 1 } ?: 3
}
@Suppress("unused", "FunctionName")
fun JSON(
jsonText: String,
isolated: Boolean = false,
expanded: Boolean = true,
) = JSON(Json.parseToJsonElement(jsonText), isolated, expanded)
fun mimeResult(vararg mimeToData: Pair): MimeTypedResult = MimeTypedResult(mapOf(*mimeToData))
fun textResult(text: String): MimeTypedResult = mimeResult(MimeTypes.PLAIN_TEXT to text)
fun htmlResult(
text: String,
isolated: Boolean = false,
) = MimeTypedResult(mapOf(MimeTypes.HTML to text), isolated)
data class HtmlData(val style: String, val body: String, val script: String) {
override fun toString(): String {
return toString(null)
}
@Language("html")
fun toString(colorScheme: ColorScheme?): String =
"""
$body
""".trimIndent()
fun toSimpleHtml(
colorScheme: ColorScheme?,
isolated: Boolean = false,
): MimeTypedResult = HTML(toString(colorScheme), isolated)
fun toIFrame(colorScheme: ColorScheme?): MimeTypedResult {
val iFramedText = generateIframePlaneText(colorScheme)
return htmlResult(iFramedText, false)
}
fun generateIframePlaneText(colorScheme: ColorScheme?): String {
@Suppress("CssUnresolvedCustomProperty")
@Language("css")
val styleData =
HtmlData(
"""
:root {
--scroll-bg: #f5f5f5;
--scroll-fg: #b3b3b3;
}
:root[theme="dark"], :root [data-jp-theme-light="false"]{
--scroll-bg: #3c3c3c;
--scroll-fg: #97e1fb;
}
body {
scrollbar-color: var(--scroll-fg) var(--scroll-bg);
}
body::-webkit-scrollbar {
width: 10px; /* Mostly for vertical scrollbars */
height: 10px; /* Mostly for horizontal scrollbars */
}
body::-webkit-scrollbar-thumb {
background-color: var(--scroll-fg);
}
body::-webkit-scrollbar-track {
background-color: var(--scroll-bg);
}
""".trimIndent(),
"",
"",
)
val wholeData = this + styleData
val text = wholeData.toString(colorScheme)
val id = "iframe_out_${iframeCounter.incrementAndGet()}"
val fName = "resize_$id"
val cleanText = text.escapeForIframe()
@Language("html")
val iFramedText =
"""
""".trimIndent()
return iFramedText
}
operator fun plus(other: HtmlData): HtmlData =
HtmlData(
style + "\n" + other.style,
body + "\n" + other.body,
script + "\n" + other.script,
)
companion object {
private val iframeCounter = AtomicLong()
}
}
/**
* Renders HTML as iframe in Kotlin Notebook or simply in other clients
*
* @param data
*/
fun Notebook.renderHtmlAsIFrameIfNeeded(data: HtmlData): MimeTypedResult {
return if (jupyterClientType == JupyterClientType.KOTLIN_NOTEBOOK) {
data.toIFrame(currentColorScheme)
} else {
data.toSimpleHtml(currentColorScheme)
}
}
object MimeTypes {
const val HTML = "text/html"
const val PLAIN_TEXT = "text/plain"
const val MARKDOWN = "text/markdown"
const val JSON = "application/json"
const val PNG = "image/png"
const val JPEG = "image/jpeg"
const val SVG = "image/svg+xml"
}
/**
* Mimetypes for in-memory output.
*/
object InMemoryMimeTypes {
const val SWING = "application/vnd.idea.swing"
const val COMPOSE = "application/vnd.idea.compose"
val allTypes = listOf(SWING, COMPOSE)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy