Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
out/
build/
.gradle/
.kotlin/
kotlin-js-store/
.idea/*
.idea/workspace.xml
*.iml
Expand Down
31 changes: 26 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
plugins {
id 'org.jetbrains.kotlin.js' version '1.6.10'
alias(libs.plugins.kotlin.multiplatform)
}

repositories {
mavenCentral()
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-js"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.6.0"
kotlin {
js(IR) {
browser()
binaries.executable()
}

sourceSets {
jsMain {
kotlin.srcDir("src/main/kotlin")
resources.srcDir("src/main/resources")
dependencies {
implementation libs.coroutines.core
}
}
}
}

kotlin.target.browser {}
def externalScriptsEnabled = providers.gradleProperty("enableExternalScripts").map { it == "true" }.orElse(false)
def externalScriptsFile = file("src/main/resources/external-scripts.html")
def externalScriptsMarker = "@EXTERNAL_SCRIPTS@"

tasks.withType(ProcessResources).configureEach {
filesMatching("index.html") {
def scripts = externalScriptsEnabled.get() ? externalScriptsFile.getText("UTF-8") : ""
filter { line -> line.replace(externalScriptsMarker, scripts) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import com.github.rougsig.flowmarbles.core.Component
import com.github.rougsig.flowmarbles.core.html
import com.github.rougsig.flowmarbles.extensions.toCamelKebabCase

private const val DOCS_BASE_URL = "/docs/"
private const val KOTLIN_DOCS_URL = "https://kotlin.github.io/kotlinx.coroutines/"

class KotlinDocs : Component {
fun setModel(model: String) {
content.setAttribute("height", "0")
content.setAttribute("src", "https://flowmarbles.com/docs/$model")
content.setAttribute("src", "$DOCS_BASE_URL$model")
linkToOriginal.setAttribute(
"href",
"${KOTLIN_DOCS_URL}kotlinx-coroutines-core/kotlinx.coroutines.flow/${model.toCamelKebabCase()}.html"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class Menu(

val nothingFound = listOf(Item.NothingFound("operators not found"))
search.onTextChangeListener = { query ->
val filteredItems = items.filter { it.label.toLowerCase().contains(query.toLowerCase()) && it !is Item.Header || query.isBlank() }
val filteredItems = items.filter { it.label.lowercase().contains(query.lowercase()) && it !is Item.Header || query.isBlank() }
list.data = list.data?.copy(items = if (filteredItems.isNullOrEmpty()) nothingFound else filteredItems)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ class SandBox<T : Any> : Component {
private val docs = KotlinDocs()
val kotlinVersion = html("p") {
attr("class", "version version--first")
text = "kotlinx.coroutines version is 1.6.0"
text = "kotlinx.coroutines version is 1.10.2"
}
val coroutinesVersion = html("p") {
attr("class", "version")
text = "kotlin version is 1.6.10"
text = "kotlin version is 2.3.0"
}
override val rootNode = html("div") {
attr("class", "sandbox")
Expand Down Expand Up @@ -61,7 +61,7 @@ class SandBox<T : Any> : Component {
val virtualTimeDispatcher = VirtualTimeDispatcher()
virtualTimeDispatcher.pauseDispatcher()
job?.cancel()
job = (GlobalScope + virtualTimeDispatcher).launch {
job = CoroutineScope(virtualTimeDispatcher + SupervisorJob()).launch {
output.setModel(
transformer(input.map { it.toTimedFlow() })
.map { it.copy(time = virtualTimeDispatcher.currentTime) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Marble<T : Any>(model: Model<T>, posY: Long = 1) : Component {
val time: Long,
val value: T
) {
@Suppress("UNCHECKED_CAST")
operator fun plus(other: Model<T>): Model<T> {
val newValue: T = if (this.value as? Int != null && other.value as? Int != null) {
(value + other.value) as T
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.github.rougsig.flowmarbles.extensions
fun String.toCamelKebabCase(): String {
val builder = StringBuilder()
this.forEach {
if (it.toUpperCase() == it) {
if (it.isUpperCase()) {
builder.append("-")
builder.append(it.toLowerCase())
builder.append(it.lowercaseChar())
} else {
builder.append(it)
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/com/github/rougsig/flowmarbles/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import com.github.rougsig.flowmarbles.operators.operators
import org.w3c.dom.HashChangeEvent
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview

@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
fun main() {
val app = document.getElementById("app")!!
val header = html("header") {
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/resources/external-scripts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] = m[i] || function () {
(m[i].a = m[i].a || []).push(arguments)
};
m[i].l = 1 * new Date();
k = e.createElement(t), a = e.getElementsByTagName(t)[0], k.async = 1, k.src = r, a.parentNode.insertBefore(k, a)
})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");

ym(60960760, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
})
</script>
<noscript>
<div><img src="https://mc.yandex.ru/watch/60960760" style="position:absolute; left:-9999px;" alt=""/></div>
</noscript>
<!-- /Yandex.Metrika counter -->
22 changes: 1 addition & 21 deletions app/src/main/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,10 @@
class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a>
</body>
<script type="text/javascript" src="app.js"></script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] = m[i] || function () {
(m[i].a = m[i].a || []).push(arguments)
};
m[i].l = 1 * new Date();
k = e.createElement(t), a = e.getElementsByTagName(t)[0], k.async = 1, k.src = r, a.parentNode.insertBefore(k, a)
})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");

ym(60960760, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
})

function calcIframeHeight(iframeElement) {
iframeElement.height = iframeElement.contentWindow.document.body.scrollHeight
}
</script>
<noscript>
<div><img src="https://mc.yandex.ru/watch/60960760" style="position:absolute; left:-9999px;" alt=""/></div>
</noscript>
<!-- /Yandex.Metrika counter -->
@EXTERNAL_SCRIPTS@
</html>
10 changes: 10 additions & 0 deletions app/webpack.config.d/docs-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
config.devServer = config.devServer || {};
const existingProxy = Array.isArray(config.devServer.proxy) ? config.devServer.proxy : [];
config.devServer.proxy = existingProxy.concat([
{
context: ["/docs"],
target: "http://localhost:8000",
changeOrigin: true,
pathRewrite: { "^/docs": "" }
}
]);
2 changes: 1 addition & 1 deletion docs/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM openjdk:8-alpine
FROM eclipse-temurin:21-jre

COPY ./build/libs /usr/app
WORKDIR /usr/app
Expand Down
99 changes: 67 additions & 32 deletions docs/build.gradle
Original file line number Diff line number Diff line change
@@ -1,55 +1,90 @@
buildscript {
ext.kotlin_version = '1.5.10'
ext.coroutines_version = '1.5.0'
ext.ktor_version = '1.6.0'
ext.logback_version = '1.2.3'
ext.jsoup_version = '1.13.1'

repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.kapt)
}

repositories {
mavenCentral()
}

def jvmVersion = libs.versions.jvm.get()

def docsBaseUrl = 'https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/'
def docsSourceDir = file("$rootDir/app/src/main/kotlin/com/github/rougsig/flowmarbles/operators")
def docsOutputDir = layout.buildDirectory.dir('generated/docs')
def fetchDocs = providers.gradleProperty('fetchDocs').map { it == 'true' }.orElse(true)

def toKebabCase = { String value ->
value.replaceAll(/([a-z0-9])([A-Z])/, '$1-$2').toLowerCase(Locale.ROOT)
}

def docsNamesProvider = providers.provider {
def names = new LinkedHashSet<String>()
if (docsSourceDir.exists()) {
fileTree(docsSourceDir).matching { include '**/*.kt' }.files.each { file ->
def matcher = file.text =~ /sandbox\(\s*"([^"]+)"/
matcher.each { match ->
names << match[1]
}
}
}
return names as List
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
kotlin {
jvmToolchain(jvmVersion.toInteger())
}

repositories {
jcenter()
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions.jvmTarget = jvmVersion
}

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
tasks.register('downloadDocs') {
inputs.property('docsBaseUrl', docsBaseUrl)
inputs.property('docsNames', docsNamesProvider)
outputs.dir(docsOutputDir)
onlyIf { fetchDocs.get() }
doLast {
def outDir = docsOutputDir.get().asFile
outDir.mkdirs()
docsNamesProvider.get().each { name ->
def kebab = toKebabCase(name)
def target = new File(outDir, "${kebab}.html")
if (!target.exists()) {
target.text = new URL("${docsBaseUrl}${kebab}.html").getText('UTF-8')
}
}
new File(outDir, 'pages.txt').text = docsNamesProvider.get().join('\n')
}
}

compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
sourceSets.main.resources.srcDir(docsOutputDir)
tasks.named('processResources') {
dependsOn tasks.named('downloadDocs')
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jsoup:jsoup:$jsoup_version"
implementation platform(libs.ktor.bom)
implementation libs.coroutines.core
implementation libs.jsoup

implementation "io.ktor:ktor-server-core:$ktor_version"
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-cio:$ktor_version"
implementation libs.ktor.server.core
implementation libs.ktor.server.netty
implementation libs.ktor.client.core
implementation libs.ktor.client.cio

implementation "ch.qos.logback:logback-classic:$logback_version"
implementation libs.logback.classic
}

task fatJar(type: Jar) {
baseName = 'server'
tasks.register('fatJar', Jar) {
archiveBaseName.set('server')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest {
attributes 'Implementation-Title': 'FlowmarblesKotlinDocs'
attributes 'Main-Class': 'com.github.rougsig.kotlindocs.MainKt'
}
archiveClassifier = "all"
archiveClassifier.set('all')
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
with jar
Expand Down
28 changes: 19 additions & 9 deletions docs/src/main/kotlin/com/github/rougsig/kotlindocs/main.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.github.rougsig.kotlindocs

import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
Expand All @@ -23,11 +23,12 @@ fun main() {
get("/{page}") {
try {
val page = call.parameters["page"]!!
val pageName = page.toCamelKebabCase()
val uri = URI(BASE_DOCS_URL)
val doc = getDocsHtml("$BASE_DOCS_URL${page.toCamelKebabCase()}.html")
println("$BASE_DOCS_URL${call.parameters["page"]}.html")
val doc = getDocsHtml(pageName)
println("$BASE_DOCS_URL$pageName.html")

val pageContents = doc.select(".content")
val pageContents = doc.select("#content, .content")
if (pageContents.isEmpty()) error("Content not found for: '$page'")
pageContents.select("a").forEach { link ->
link.attr("target", "_blank")
Expand All @@ -36,7 +37,7 @@ fun main() {
link.attr("href", absoluteHref + href.dropWhile { it != '#' })
}
val template = Jsoup.parse(Resource.read("/template.html"))
val root = template.selectFirst("body")
val root = template.selectFirst("body") ?: error("Template body element missing.")
pageContents.forEach { root.appendChild(it) }
root.select(".top-right-position").remove()
call.respondText(template.html(), ContentType.Text.Html)
Expand All @@ -49,10 +50,15 @@ fun main() {
}.start(wait = true)
}

private suspend fun getDocsHtml(url: String): Document = withContext(Dispatchers.IO) {
private suspend fun getDocsHtml(pageName: String): Document = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
try {
cont.resume(Jsoup.connect(url).get())
val local = Resource.readOrNull("/docs/$pageName.html")
if (local != null) {
cont.resume(Jsoup.parse(local))
return@suspendCoroutine
}
cont.resume(Jsoup.connect("$BASE_DOCS_URL$pageName.html").get())
} catch (e: Throwable) {
cont.resumeWithException(e)
}
Expand All @@ -76,4 +82,8 @@ private object Resource {
fun read(name: String): String {
return Resource::class.java.getResource(name)!!.readText()
}

fun readOrNull(name: String): String? {
return Resource::class.java.getResource(name)?.readText()
}
}
Loading