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

org.jetbrains.kotlinx.jupyter.api.Results.kt Maven / Gradle / Ivy

There is a newer version: 0.12.0-335
Show newest version
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 - 2024 Weber Informatics LLC | Privacy Policy