All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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.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
}

/**
 * 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) = 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
}

/**
 * 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),
        isolatedHtml,
        id,
    )

open class MimeTypedResultEx(
    private val mimeData: JsonElement,
    var isolatedHtml: Boolean = false,
    override val id: String? = null,
) : DisplayResult {
    override fun toJson(additionalMetadata: JsonObject, overrideId: String?): JsonObject {
        val metadata = HashMap().apply {
            if (isolatedHtml) put(MimeTypes.HTML, Json.encodeToJsonElement(mapOf("isolated" to true)))
            additionalMetadata.forEach { key, value ->
                put(key, value)
            }
        }

        val result: MutableJsonObject = hashMapOf(
            "data" to mimeData,
            "metadata" to Json.encodeToJsonElement(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 + isolatedHtml.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 }
fun JSON(json: JsonElement, isolated: Boolean = false) = MimeTypedResultEx(
    buildJsonObject {
        put(MimeTypes.JSON, json)
        put(MimeTypes.PLAIN_TEXT, JsonPrimitive(jsonPrettyPrinter.encodeToString(json)))
    },
    isolated,
)

@Suppress("unused", "FunctionName")
fun JSON(jsonText: String, isolated: Boolean = false) = JSON(Json.parseToJsonElement(jsonText), isolated)

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) = mimeResult(MimeTypes.HTML to text).also { it.isolatedHtml = 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"
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy